【煩悩】大晦日1日でゲーム作って年内に滑り込みチャレンジ2024【前編】

# はじめに

大晦日の1日だけでゲームを作ってQiitaに記事を投稿できるかチャレンジ。

大晦日といえば、108つのアレですよね。

そうです。__煩悩__ です。

ということで(？)煩悩をぶっ壊す便利ツール「煩悩デストラクター」を作りました！！！

こちらから遊ぶことができます。遊んでみてね。
http://delphy8.s324.xrea.com/index.html


@Kumanoku と @Delphyilia の合作になります！

早速ゲームを作っていきます！と言いたいところですが、【前編】ではログイン機能を作ります。

ゲーム部分の記事を読みたい！という方は @Delphyilia の【後編】をこちらからどうぞ↓

https://qiita.com/Delphyilia/items/ae3799cd82728d9df7d6

# ログイン機能の実装（Kumanoku担当）
## 概要
PythonのCGIを使用して、ログイン(ユーザー登録とユーザー認証)の機能を実装します。
以下の2つのCGIを作成します。
- ユーザー登録用CGI(register.cgi)
	1. ユーザー名とパスワードの入力フォーム
	2. パスワードをSHA256でハッシュ化
	3. ユーザー名とハッシュ化パスワードをcsvに保存
- ユーザー認証用CGI(login.cgi)
	1. ユーザー名とパスワードの入力フォーム
	2. パスワードをSHA256でハッシュ化
	3. csvに保存されたユーザー名・パスワードと照合
	
## 環境
PythonのCGIが動作するWebサーバーを用意します。今回は無料のレンタルサーバーであるXREAを利用します。

https://www.xrea.com/

XREAのPythonの実行パスは/usr/bin/python3.6です。詳しいサーバー仕様は以下のXREAのページを参照してください。

https://www.xrea.com/spec/

## ユーザー登録用CGI
```cgi:register.cgi
#!/usr/bin/python3.6

import cgi
import hashlib
import csv
import os

DATA_FILE = "users.csv"

# ハッシュ化関数
def hash_password(password):
    return hashlib.sha256(password.encode()).hexdigest()

# 既存のユーザー名か？
def user_exists(username):
    if not os.path.exists(DATA_FILE):
        return False
    with open(DATA_FILE, "r") as f:
        reader = csv.reader(f)
        for row in reader:
            if row[0] == username:
                return True
    return False

# csvに保存
def register_user(username, password):
    hashed_password = hash_password(password)
    with open(DATA_FILE, "a", newline="") as f:
        writer = csv.writer(f)
        writer.writerow([username, hashed_password])

# ヘッダーの設定
print("Content-Type: text/html; charset=utf-8\n\n")

form = cgi.FieldStorage()
username = form.getvalue("username")
password = form.getvalue("password")
confirm_password = form.getvalue("confirm_password")

if username and password and confirm_password:
    if password != confirm_password:
        message = "パスワードが一致しません。"
    elif user_exists(username):
        message = "このユーザー名は既に登録されています。"
    else:
        register_user(username, password)
        message = "登録が完了しました。ログインページに戻ってログインしてください。"
else:
    message = "すべてのフィールドを入力してください。"

print(f"""
<html>
<head><title>ユーザー登録</title></head>
<body>
<h1>ユーザー登録</h1>
<form method="post" action="register.cgi">
    <label for="username">ユーザー名: </label><input type="text" id="username" name="username" required><br>
    <label for="password">パスワード: </label><input type="password" id="password" name="password" required><br>
    パスワード確認: <input type="password" name="confirm_password" required><br>
    <input type="submit" value="登録">
</form>
<p>{message}</p>
<p><a href="login.cgi">ログインページに戻る</a></p>
</body>
</html>
""")
```
<br>
ユーザー名が被らないように、入力されたユーザー名がcsvに既存であれば弾くようにしています。

実行すると以下のような表示になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3950983/14063b78-b38a-c417-8134-8658443d748c.png)


すべてのフィールドを入力して「登録」を押すと...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3950983/870edcb2-4c51-723f-9115-b123b4133848.png)

登録できました。

## ユーザー認証用CGI
```cgi:login.cgi
#!/usr/bin/python3.6

import cgi 
import hashlib 
import csv 
import os

DATA_FILE = "users.csv" 

# ハッシュ化関数
def hash_password(password): 
    return hashlib.sha256(password.encode()).hexdigest() 

# ユーザ認証関数
def authenticate_user(username, password): 
    hashed_password = hash_password(password) 
    if not os.path.exists(DATA_FILE):
        return False 
    with open(DATA_FILE, "r") as f: 
        reader = csv.reader(f) 
        for row in reader: 
            if row[0] == username and row[1] == hashed_password: 
                return True 
    return False 

# ヘッダーの設定
print("Content-Type: text/html; charset=utf-8\n\n") 

form = cgi.FieldStorage() 
username = form.getvalue("username") 
password = form.getvalue("password") 

if username and password: 
    if authenticate_user(username, password): 
        message = "ログイン成功！" 
    else: 
        message = "ユーザ名またはパスワードが間違っています。" 
else: 
    message = "ユーザ名とパスワードを入力してください" 

print(f"""
<html lang="ja">
<head><title>ログイン</title></head>
<body>
<h1>ログイン</h1>
<form method="post" action="login.cgi">
    <label for="username">ユーザー名: </label><input type="text" id="username" name="username" required><br>
    <label for="password">パスワード: </label><input type="password" id="password" name="password" required><br>
    <input type="submit" value="ログイン">
</form>
<p>{message}</p>
<p><a href="register.cgi">新規登録はこちら</a></p>
</body>
</html>
""")
```

実行すると以下のような表示になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3950983/b7394409-4307-b893-88ae-034460ec071a.png)

先ほど登録した内容を入力して「ログイン」を押すと...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3950983/6288cda5-94d9-d95f-b577-c05227006586.png)

ログイン成功しました！

## 余談

Microsoft Edgeで開くと、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3950983/e70cd07a-bb10-5815-d4b2-927320ef575a.png)

このようにパスワードのフォームに目のアイコン(パスワード表示ボタン)が表示されます。

これを削除したい場合は

```css
::-ms-reveal {
    display: none;
}
```

このようにアイコンのスタイルに手を加えれば良いです。

詳しくは以下を参照↓

https://learn.microsoft.com/ja-jp/microsoft-edge/web-platform/password-reveal

## おわりに
これが初めてのQiita投稿、初めての共同作業()です。

ゲーム本体を作ったのに私の名義で投稿するよう勧めてくれた友人の @Delphyilia 君、ありがとう。

バックエンドへの理解が浅く、Pythonで何となく書けるCGIしか手を出せていないのが現状です。
REST API？WebSocket？右も左も分からないので勉強していきたいです。