0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

なんか、仮想マシンで簡易的な競プロローカルサーバ作りたくね? その3

Last updated at Posted at 2025-12-10

その1 URL https://qiita.com/Wing2C1/items/6b3a522803a0fc8eaab4
その2 URL https://qiita.com/Wing2C1/items/216691583c1767c60d70
その3 URL https://qiita.com/Wing2C1/items/dbb2c728a8300c3b2919
今回の記事にあたっての画像や記述は基本2025/11/28のときのものであることを断っておこう。

5.ホストマシンからサイト表示したいよね

前回ので、せっかくサイトをドメインで表示させられたのだからホストマシン上のブラウザで閲覧してみたい。VirtualBoxのホストマシンと仮想マシン間の通信ができるように、そのためホストオンリーアダプターの設定を行う。
VirtualBox Managerを開き、ツールバーからネットワークを選択する。
image.png
作成してみよう。
image.png
自動作成されて、こんな感じになった。 追加されたホストオンリーネットワークの名前とIPアドレス/マスクは必要だからメモするなりしよう。
仮想マシンを右クリックして設定を開く。
image.png
ネットワークの設定で、アダプター2を選択して、以下のようにした。
image.png
アダプター1の設定は変更しないこと。NAT(Network Address Translation)という設定になっているが、これはホストマシンのインターネット上の住所となるグローバルIPアドレスを仮想マシンの持つプライベートアドレスに変換する技術。これのおかげで外部ネットに仮想マシンからアクセスできる。

さて、ではここで仮想マシンを起動しよう。
image.png
有線設定を開く。
image.png
ここではenp0s3というネットワークインターフェースが最初から入っていたもの。つまりNATアダプターで、enp0s8がホストオンリーアダプターである。Debianだと、最初はどちらもONに出来ないっぽい。
image.png
どうやら、どちらも識別子の名前が「Wired connection 1」になっているからだということらしい。察して名前分けてほしいね。解決法としてはターミナルで

sudo nmcli connection add type ethernet ifname enp0s8 con-name "Wired connection 2"

をすれば、enp0s8を担当するWired connection 2の識別子が作れる。nmcliコマンドとは、NetworkManagerを操作するためのコマンドラインツール。各ネットワーク設定(接続プロファイル)が保存されているものをnmcli connection addで作成する。

cpserver@CP-server:~$ sudo nmcli connection add type ethernet ifname enp0s8 con-name "Wired connection 2"
[sudo] cpserver のパスワード:
接続 'Wired connection 2' (a92fb917-5f5e-4110-8155-edf2e20c5b3b) が正常に追加されました。

これで有線設定に戻ってみよう。
image.png
作成されて、どちらもONになっているみたいだ。次に、Wired connection 2を設定する。
image.png
IPv4について、手動に設定しホストオンリーアダプターと同じネットワークからIPアドレスを割り当てる。IPv4とは、「Internet Protocol version 4」の略で、インターネット上で機器を識別し通信するための「IPアドレス」の規格のこと。
さっきメモした理由はここに設定するためにある。
私の場合の例は
ホストオンリーアダプター(ホストマシン側) :192.168.165.1/24(255.255.255.0)
Ethernetインターフェース(仮想マシン側) :192.168.165.2/24(255.255.255.0)
→Ethernetインターフェース(仮想マシン側)の IPv4 は
アドレス  :192.168.45.2
ネットマスク:255.255.255.0
ゲートウェイ:192.168.45.1
Ethernetとは、主にパソコンやサーバーなどの機器を有線で接続し、データをやり取りするための通信規格のことね。
image.png
あと、IPv6を無効にする。仮想マシンのホストオンリーアダプターの設定ではIPv4の規格で書かれているため違う規格を有効化する。特にIPv6のほうが優先されるため、パフォーマンスの懸念がある。
さて、これを行った理由はホストマシンでサイトを表示することだった。
試しにホストマシンのEdgeにhttp://cp-server と入れてみる。
image.png
やったね。これで制限されたメモリの仮想マシンのブラウザを直接開かなくても見ることができるようになるし、その上より本格的なサーバーとしての役割を持っているように感じる。

2025/11/29 追記
enp0s8にWired connection 1, 2が共存すると、外部ネットの接続が切れているときにenp0s8が自動的にWired connection 1のプロファイル使い始めるから、enp0s8に使われているWired connection 1は取り除くこと。もしWired connection 1が削除されてenp0s3のプロファイルも一緒になくなった場合は、新しく作ること。デフォルトのままでいい。
(識別子の名前気になるならもう一度Wired connection 1)

6.TeraTermを用いたSSH接続

実際にサーバーをレンタルしたり、はたまたはスペック落ちしたノーパソなどをWebサーバーにする場合を考えよう。サーバーをレンタルする場合は実機がないから一見編集できなそうだし、ノーパソの場合も限られたメモリの中では普段使っているマウスを用いてファイルを開くGUI(Graphical User Interface)操作というのは表示させるだけでメモリを食ってしまう。
サーバーならクライアントからのリクエストのみに注力させるべきなのだ。そこで、SSHを用いて接続してそこから編集しよう。SSH(Secure Shell)接続は、ネットワーク上でリモートコンピュータに安全にアクセスするためのプロトコルのことだ。遠隔で編集することができ、いわゆるハッカーの遠隔操作というのはSSHから不正に侵入することなのではないだろうか。
仮想マシンのターミナルで

sudo apt install openssh-server

ダウンロード終わったら以下のコマンドで確認してみよう。

sudo systemctl status ssh
cpserver@CP-server:~$ sudo systemctl status ssh
● ssh.service - OpenBSD Secure Shell server
     Loaded: loaded (/usr/lib/systemd/system/ssh.service; enabled; preset: enab>
     Active: active (running) since Fri 2025-11-28 16:10:30 JST; 26s ago
 Invocation: 85724f530c7843339a327c31636d41b4
       Docs: man:sshd(8)
             man:sshd_config(5)
   Main PID: 5433 (sshd)
      Tasks: 1 (limit: 4620)
     Memory: 1.3M (peak: 2.1M)
        CPU: 93ms
     CGroup: /system.slice/ssh.service
             └─5433 "sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups"

11月 28 16:10:30 CP-server systemd[1]: Starting ssh.service - OpenBSD Secure Sh>
11月 28 16:10:30 CP-server sshd[5433]: Server listening on 0.0.0.0 port 22.
11月 28 16:10:30 CP-server sshd[5433]: Server listening on :: port 22.
11月 28 16:10:30 CP-server systemd[1]: Started ssh.service - OpenBSD Secure She>

ここで、遠隔で通信するためのソフトにTera Term(テラターム)がある。Tera Termは、Windows環境で利用できる端末エミュレーターのことだ。SSH(Secure Shell)を使用して、リモートのLinuxサーバーや他のUnix系システムに安全に接続することが可能である。
さあ、TeraTermをインストールしてみよう。
https://github.com/TeraTermProject/teraterm/releases
にアクセス。
image.png
11/28時点での最新版では、バージョン5.5.1のようだ。installerからteraterm-5.5.1-x64.exeをダウンロードする。自動でセットアップが開く。
こだわりなければデフォルトのままインストールする。
image.png
完了。さっそく実行してみる。
image.png
良さそう。マイホストにさっき設定したアドレスを入力する。私の場合は192.168.45.2。
image.png
続行する。
image.png
ユーザー名とパスフレーズは、いつも使っているユーザーネームとパスワードを入れる。
image.png
接続できた。これからはTera Termでターミナル操作しかすることはないでしょう。仮想マシンが起動さえしていれば、あとはもう仮想マシン内でログインしていなくてもここからターミナルをいじれるようになったのだ。
最後に、teratermは特殊のためコピペがいつもと違う。teraterm内では、Alt+Cでコピー、Alt+Vでペーストとなる。
セッションを終了したいときは

exit

でアプリを終わらせられる。

7.SSHにおけるセキュリティ

さて、ハッカー云々の話があったと思うが、SSHは不適切な設定だと容易にすべての権限を乗っ取られる可能性がある。セキュリティ対策はきちんとしておきたい。

rootログインの無効化

Linuxの共通の管理者権限の名前はrootであることから、名前が割れている時点であとはパスワードを総当りすればいいだけになる。その上、万が一乗っ取られたらすべての権限を奪われるためrootの直接ログインは封じるべきなのだ。

sudo nano /etc/ssh/sshd_config

で設定ファイルを開く。

- #PermitRootLogin prohibit-password
+ PermitRootLogin no

これでrootログインが無効になる。rootになりたければユーザーから入ってsudoなりで権限持てばいいため必要ないのだ。

ポート番号変更

ハッキングは何も人間が行うものでもない。世界中のbotがいろいろなサイトに数撃ちゃ当たる戦法で試している。そこで、ポートを変更しよう。
ポートとは、IPアドレスという住所に届いたデータを、正しいアプリケーション(Web閲覧、メール、ファイル転送など)に振り分けるための「扉」や「部屋番号」の役割を果たすものだ。
ポート番号は0番から65535番まであり、特定のサービスに割り当てられている。Webページ閲覧(HTTP)には80番、暗号化されたWebページ閲覧(HTTPS)には443番など、決まった番号が使われる。
SSHは普通22番ポートを使用しているが、裏返せばその番号を攻撃すればSSHを乗っ取れる可能性があることを意味する。そのため、botによる攻撃をおおよそシャットアウトするにはポート番号を変えればそのアクセスも減るだろう。これは、本質的なセキュリティ対策ではないがログが荒らされるなどを軽減できる。
同じように設定ファイル

- #Port 22
+ Port 2222

これはあくまで例であるので好きな数字で結構だ。注意するならteratermでのポートの指定を対応する番号に変更すること。しないとログインができない。
ここまで終わったら一度設定を反映しておこう。

sudo systemctl restart sshd

今度は、ポート番号を変更してログインをしてみよう。
image.png
TCPポート#(P)を2222にした。そしたら私はログインできた。
22のまま接続しようとすると
image.png
大丈夫そうだ。

証明書によるログイン

パスワードによるSSHのログインは、実は危険であるということをよく知って置かなければならない。いわゆる総当たり攻撃(ブルートフォース)は一般的なPCだと毎秒数百から、高性能なGPUでは毎秒数十億まで試すことも可能だと言われている。そこで、公開鍵暗号方式を用いたログインを行おう。
「暗号」自体は、データをある規則に従って違うデータに変換することといえばいいだろう。その規則さえ知っていればその中身を理解することができるというように。
コンピュータ自体はその暗号データを復元するため用いられるデータは一般的に「鍵」という。
その中で、公開鍵暗号方式はみんなにばら撒く用の「公開鍵」と自分だけが持つ「秘密鍵」を用意する。公開鍵と秘密鍵の関係は、簡単に言えば「一方で暗号化したデータは、もう一方でしか復号できない」というものである。つまり、秘密鍵を自分だけが持つ限り、誰かにコピーされたりしなければ安全性が保たれるというものだ。
SSHでいうならば、ログインする際に仮想マシンの持つ公開鍵で暗号化したチャレンジを送りつけて、ホストマシンの持つ秘密鍵で答えを送信。復号が確認されたら通すというようにすれば最高に嬉しいのだ。
早速手順を試していこう。ホストマシンのターミナルを開く。WindowsならPowershellを開き、以下を打つ。

ssh-keygen -t rsa
PS C:\Users\Wing2C1> ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (C:\Users\<username>/.ssh/id_rsa):
...
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in C:\Users\<username>/.ssh/id_rsa
Your public key has been saved in C:\Users\<username>/.ssh/id_rsa.pub
...

パスフレーズなどを求められるが、特に設定しないならそのままEnterで構わない。
作り終わると.ssh/のディレクトリ以下にid_rsaとid_rsa.pubの2つが作成される。
id_rsaが秘密鍵で絶対に公開しないもの、id_rsa.pubが公開鍵でサーバに登録するものである。
続けて

scp -P 2222 $env:USERPROFILE\.ssh\id_rsa.pub cpserver@192.168.45.2:~/id_rsa.pub
PS C:\Users\Wing2C1> scp -P 2222 $env:USERPROFILE\.ssh\id_rsa.pub cpserver@192.168.45.2:~/id_rsa.pub
cpserver@192.168.45.2's password:
id_rsa.pub                                  100%  573    93.3KB/s   00:00

cpserverは仮想マシンのユーザー名。192.168.45.2は仮想マシンのIPアドレス。-Pオプションでポートを指定。scp(secure copy)はSSHを使用して通信経路を暗号化してファイルの転送を行うコマンド。ここで、先程の公開鍵を送りつけてやる。パスワードを入力することをお忘れなく。

さあ、次は仮想マシンのターミナルに戻ろう。lsでホームディレクトリの中身を確認する。

cpserver@CP-server:~$ ls
Desktop    Downloads  Pictures  Templates  id_rsa.pub
Documents  Music      Public    Videos     workdir

この公開鍵を設定ファイルに追記する。

cat id_rsa.pub >> ~/.ssh/authorized_keys

catコマンドはファイルの内容を表示したり、上書きしたり、追記したりするコマンドだ。
書いたらいらないしrmコマンドでid_rsa.pubは消してしまおう。
これが終わったらteratermの接続方法を秘密鍵で指定して接続してみよう。
image.png
パスフレーズは私の場合、設定しなかったためなしで。パスフレーズ入れた人はそれを入力。これでOKを押すと接続できる。

パスワードいる?

鍵認証によるログインができたため、パスワードによるログインはいりませんよね。設定を変更しましょう。

- #PasswordAuthentication yes
+ PasswordAuthentication no

書き終わったら反映させる。

sudo systemctl restart sshd

これでteraterm閉じてパスワードによるログインを試みよう。
image.png
出来てそうだね。

fail2banを用いたbot排除

ポート番号を変更したり、公開鍵暗号方式を採用したりして、ある程度はbotの数が減るものの使えるポートをスキャンするなどbotは攻撃の手を緩めない。そうすると失敗ログが荒らされたり、CPU仕様率増加だったりメモリが圧迫されたりなどまだまだ対策は必要そうです。そこで、fail2banを導入しよう。
fail2banは、Pythonで実装されているオープンソースのセキュリティツールのことだ。システムが出力するログファイルを基にフィルターを適用し、ログイン失敗回数などの条件により不正とみなされるIPアドレスを検出して、そのIPアドレスをブロックする仕組みがある。これは、SSHに対しても、nginxのサーバーに対して(Dos,DDos攻撃など)も有用のため、使うと良さそう。

sudo apt install fail2ban

さっさと、有効化もしましょう。

sudo systemctl start fail2ban
sudo systemctl enable fail2ban
sudo systemctl status fail2ban
cpserver@CP-server:~$ sudo systemctl start fail2ban
sudo systemctl enable fail2ban
sudo systemctl status fail2ban
Synchronizing state of fail2ban.service with SysV service script with /usr/lib/systemd/systemd-sysv-install.
Executing: /usr/lib/systemd/systemd-sysv-install enable fail2ban
● fail2ban.service - Fail2Ban Service
     Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled; preset:>
     Active: active (running) since Fri 2025-11-28 19:42:51 JST; 8min ago
 Invocation: ede4d562c347430f85b528c1328e2603
       Docs: man:fail2ban(1)
   Main PID: 7358 (fail2ban-server)
      Tasks: 5 (limit: 4620)
     Memory: 16M (peak: 16.5M)
        CPU: 3.283s
     CGroup: /system.slice/fail2ban.service
             mq7358 /usr/bin/python3 /usr/bin/fail2ban-server -xf start

11月 28 19:42:51 CP-server systemd[1]: Started fail2ban.service - Fail2Ban Serv>
11月 28 19:42:52 CP-server fail2ban-server[7358]: Server ready

設定ファイルで編集してしまおう。ついでに、nginxのサーバーに大量アクセスするbotのようなものもBANできるようにする。nginxの設定は新規のフィルターを用いるため、それから作ろう。

sudo nano /etc/fail2ban/filter.d/nginx-badbots.conf
[Definition]
failregex = ^<HOST> -.*"(GET|POST).*HTTP.*"(?:Mozilla|libwww|python|curl|php|wget|java|ruby|scrapy|bot).*$
ignoreregex =

これで、HTTPリクエストをボットっぽいスクリプトを検知してフィルタリングする。APIも用いないため、今は大丈夫だろう。(拡張する場合は見直し)。
例外処理は今のところなしのため、ignoreregexは空欄。

sudo nano /etc/fail2ban/jail.local
[DEFAULT]
ignoreip = 127.0.0.1/8 192.168.45.0/24

[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
bantime = 600
findtime = 600

[nginx-badbots]
enabled = true
filter = nginx-badbots
logpath  = /var/log/nginx/access.log
findtime = 60
maxretry = 50
bantime = 60
action = iptables-multiport[name=badbots, port="http,https", protocol=tcp]

[]で囲っている部分は定義。ignoreipは指定されたipならフィルタリングをしない。今回はlocalhostとホストオンリーアダプター関連のip。filterは/etc/fail2ban/filter.d/以下の定義を指定。maxretryは最大フィルターに引っかかる回数、これを超えたらbantime秒だけbanされる。findtimeを超えるとmaxretryはリセットされる。actionはbanに対して行うもので、nginx-badbotsは新規のため必要。この場合は、http,httpsリクエストをすべて遮断する。

sudo systemctl restart fail2ban

これで再起動したら終わり。

8.Djangoでアカウント作ろう

長々としたSSH編は終わり。
Djangoの説明入るけど関数の説明は省く。挙動だけは解説する。関数は自分で調べて。
アカウントを登録したりログインしたりすることができるサイトを考えよう。
その2で作ったindex.htmlのページにユーザー名が表示されたら最高だ。manage.pyと同じディレクトリ内で

python3 manage.py startapp accounts

そしたら、djangoのsetting.pyのINSTALLED_APPに追記しよう。

CPsite/CPsite/setting.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'home',
    'accounts', # <- accountsアプリ追加
]
...

次にurls.pyにも追記する。

CPsite/CPsite/urls.py
...
urlpatterns = [
    path('admin/', admin.site.urls),  # Django 管理サイト(admin) へのルート
    # ルートURL ("") にアクセスしたとき、home アプリの URL 設定を読み込む
    path('', include('home.urls')),
    # /accounts/ 以下のURLにアクセスしたとき、accounts アプリの URL 設定を読み込む
    path('accounts/', include('accounts.urls')),
]

...

今回、アカウント。つまりユーザーを作成するため特別にsetting.pyに以下を追記する。

CPsite/CPsite/setting.py
AUTH_USER_MODEL = 'accounts.User'

さて、アカウント登録ページやログインページを作成するには、ユーザーのテーブルモデルが必要だ。accountsに移動する。

nano models.py

でユーザーモデルを定義しよう。

CPsite/accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser

# Django 標準のユーザーモデル(AbstractUser)を拡張したカスタムユーザーモデル
class User(AbstractUser):
    # バトルポイント。初期値は0
    battle_point = models.IntegerField(default=0)
    # 自己紹介文。空でもOK(blank=True)、DBでNULLも許可(null=True)
    bio = models.TextField(blank=True, null=True)
    # ユーザー作成日時(自動で保存時にセットされる)
    created_at = models.DateTimeField(auto_now_add=True)
    # 更新日時(保存するたび自動更新)
    updated_at = models.DateTimeField(auto_now=True)
    def __str__(self):
        # 管理画面などでユーザー名が表示されるようにする
        return self.username

Djangoは優秀のため、ユーザーモデルの雛形がすでに定義されている。今回作成するのは競技プログラミングサイトのため簡易的なレートがほしい。問題が解けたらポイントを加算するようにしよう。そのための要素を定義することにする。名前はうちの学校の某先生にちなんでbattle_pointにしよう。
次に管理者画面から要素をいじれるものを作ろう。admin.pyで定義する。

CPsite/accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User

# User モデルを Django 管理画面に登録し、カスタマイズする
@admin.register(User)
class CustomUserAdmin(UserAdmin):
    # 一覧画面で表示するカラム
    list_display = ('username', 'email', 'is_staff', 'is_active', 'battle_point')
    # 管理画面で編集できない(読み取り専用)フィールド
    readonly_fields = ('battle_point', 'bio')    
    # 既存の UserAdmin.fieldsets に追加で独自フィールドを組み込む
    fieldsets = UserAdmin.fieldsets + (
        ('Points info', {'fields': ('battle_point',)}),  # バトルポイント関連のブロック
        ('Profile', {'fields': ('bio',)}),               # プロフィール関連のブロック
    )

これでモデルユーザーから読み込んだテーブルを基に、表示させてくれる。
これで、username、email、権限持ちか?、
アカウントにログインさせれるか?(BANとか)、battle_pointを表示させられる。
次にurls.pyを考える。今回アカウントアプリで作りたいページはサインアップフォーム、ログインフォーム、プロフィールページ、アカウント削除ページ、ユーザー検索ページである。

CPsite/accounts/urls.py
from django.urls import path
from . import views

app_name = 'accounts'  # URL の名前空間(accounts:login などで参照できる)

urlpatterns = [
    # 新規ユーザー登録ページ
    path('signup/', views.signup_view, name='signup'),
    # ログインページ
    path('login/', views.login_view, name='login'),
    # ログアウト処理
    path('logout/', views.logout_view, name='logout'),    
    # 特定ユーザーのプロフィールページ(例: /accounts/profile/hogehoge/)
    path('profile/<str:username>/', views.user_profile_view, name='profile'),
    # 自分のプロフィール編集ページ
    path('profile/my/edit/', views.edit_profile_view, name='edit_profile'),
    # アカウント削除ページ
    path('profile/my/delete/', views.delete_account_view, name='delete_account'),
    # ユーザー検索ページ
    path('search/', views.user_search, name='user_search'),
]

つぎにviews.pyを定めよう。

CPsite/accounts/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.http import Http404
from django.contrib.auth import login, logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.decorators import login_required
from django.contrib.auth import get_user_model
from django.contrib import messages
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

from .forms import SignUpForm, ProfileEditForm, DeleteAccountForm

User = get_user_model()  # カスタムユーザーモデルを取得

def signup_view(request):
    # サインアップ処理
    if request.method == 'POST':
        form = SignUpForm(request.POST)  # フォームにPOSTデータを渡す
        if form.is_valid():
            user = form.save()  # 新規ユーザー作成
            login(request, user)  # 自動ログイン
            messages.success(request, 'アカウントを作成しました')
            return redirect('accounts:profile', username=user.username)
    else:
        form = SignUpForm()  # GETなら空のフォームを表示

    return render(request, 'accounts/signup.html', {'form': form})

def login_view(request):
    # ログイン処理
    if request.method == 'POST':
        form = AuthenticationForm(request, data=request.POST)
        if form.is_valid():
            user = form.get_user()  # ログイン対象のユーザー
            login(request, user)
            messages.success(request, 'ログインしました')
            return redirect('accounts:profile', username=user.username)
    else:
        form = AuthenticationForm()

    return render(request, 'accounts/login.html', {'form': form})

def logout_view(request):
    # ログアウト処理
    logout(request)
    messages.info(request, 'ログアウトしました')
    return redirect('accounts:login')

def user_profile_view(request, username):
    profile_user = get_object_or_404(User, username=username)

    # superuserの場合、staffではないユーザーからの閲覧を禁止
    if profile_user.is_superuser and not (request.user.is_authenticated and request.user.is_staff):
        raise Http404("User profile does not exist")
    context = {
        'profile_user': profile_user,
        'request_user': request.user,
    }
    return render(request, 'accounts/profile.html', context)

@login_required
def edit_profile_view(request):
    # 自分のプロフィール編集ビュー
    user = request.user

    if request.method == 'POST':
        form = ProfileEditForm(request.POST, instance=user)
        if form.is_valid():
            form.save()  # ユーザー情報を更新
            messages.success(request, 'プロフィールを更新しました')
            return redirect('accounts:profile', username=user.username)
    else:
        form = ProfileEditForm(instance=user)  # 現在の値をフォームにセット

    return render(request, 'accounts/edit_profile.html', {'form': form})

@login_required
def delete_account_view(request):
    # アカウント削除ビュー(本人確認のためパスワード入力)
    user = request.user

    if request.method == 'POST':
        form = DeleteAccountForm(request.POST)
        if form.is_valid():
            password = form.cleaned_data['password']
            if user.check_password(password):  # パスワード一致確認
                user.delete()  # アカウント削除
                logout(request)  # ログアウト
                messages.success(request, 'アカウントを削除しました')
                return redirect('index')
            else:
                messages.error(request, 'パスワードが間違っています')
    else:
        form = DeleteAccountForm()

    return render(request, 'accounts/delete_account.html', {'form': form})

def user_search(request):
    # ユーザー検索機能(admin は検索から除外)
    query = request.GET.get("q")
    results = []
    if query:
        results = User.objects.filter(
            username__icontains=query
        ).exclude(
            username="admin"
        )
    return render(request, "accounts/user_search.html", {
        "query": query,
        "results": results
    })

@ login_requiredを用いることにより、loginしていないと到達することが出来ないようにすることができる。基本的なサインアップ、ログイン、プロフィール、アカウント削除、ユーザー検索の流れが書かれている。
次にforms.pyを定める。ここで、提出されたフォームがviews.pyから送られてデータを整形して整合性をチェックする。

CPsite/accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model

User = get_user_model()  # カスタムユーザーモデルを取得

class SignUpForm(UserCreationForm):
    # サインアップ時に必須のメールアドレス
    email = forms.EmailField(required=True)

    class Meta:
        model = User
        fields = ('username', 'email', 'password1', 'password2')  # 入力フィールド順序

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # フィールドのヘルプテキストを削除
        for field in self.fields.values():
            field.help_text = ""

    def clean_email(self):
        # メールアドレスの重複チェック
        email = self.cleaned_data.get('email')
        if User.objects.filter(email=email).exists():
            raise forms.ValidationError("このメールアドレスは既に登録されています")
        return email

class ProfileEditForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('username', 'bio')  # 編集可能なフィールド

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # フィールドのヘルプテキストを削除
        for field in self.fields.values():
            field.help_text = ""

    # bio 用のテキストエリアの高さを指定
    widgets = {
        'bio': forms.Textarea(attrs={'rows': 4}),
    }

class DeleteAccountForm(forms.Form):
    # 現在のパスワード確認用フィールド
    password = forms.CharField(widget=forms.PasswordInput, label='現在のパスワード')

これで、設定は終了。つぎにhtmlファイルを作成していく。
htmlはそのアプリ以下のtemplatesディレクトリに作成するため、そのようにする。
アカウントアプリ内然り、そのアプリ内に共通するhtmlファイルがあるとまとまって良いよね。共通ヘッダーと、アプリ内共通htmlファイルとしてbase.htmlというのを作成して、それを機能にあわせたページへ加えよう。一気に書いていく。あと、index.htmlも少し変更しよう。
Django特有のテンプレートタグは自身で調べてほしい。キリがないので

CPsite/templates/includes/header.html
<header>
    <nav>
        <a href="{% url 'home:index'%}">ホーム</a>
        {% if user.is_authenticated %}
            <a href="{% url 'accounts:profile' username=user.username %}">プロフィール</a>
            <a href="{% url 'accounts:logout' %}">ログアウト</a>
        {% else %}
            <a href="{% url 'accounts:login' %}">ログイン</a>
            <a href="{% url 'accounts:signup' %}">サインアップ</a>
        {% endif %}
        <form action="{% url 'accounts:user_search' %}" method="get" style="display:inline;">
            <input type="text" name="q" placeholder="ユーザー検索" required>
            <button type="submit">検索</button>
        </form>
    </nav>
</header>
CPsite/accounts/templates/accounts/base.html
{% load static %}
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <link rel="stylesheet" href="{% static 'accounts/static/css/base.css' %}">
        <title>{% block title %}アカウントアプリ{% endblock %}</title>
    </head>
    <body>
        {% include 'includes/header.html' %}
        <main>

            {% block content %}{% endblock %}

        </main>
    </body>
</html>
CPsite/accounts/templates/accounts/signup.html
{% extends 'accounts/base.html' %}
{% load static %}
{% block title %}サインアップ{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="{% static 'accounts/static/css/signup.css' %}">
{% endblock %}

{% block content %}
<h1>サインアップ</h1>
<form method="post">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>
    <tr>
        <td colspan="2">
            <button type="submit">登録</button>
        </td>
    </tr>
</form>
{% endblock %}
CPsite/accounts/templates/accounts/login.html
{% extends 'accounts/base.html' %}
{% load static %}
{% block title %}ログイン{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="{% static 'accounts/static/css/login.css' %}">
{% endblock %}

{% block content %}
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">ログイン</button>
</form>
{% endblock %}
CPsite/accounts/templates/accounts/profile.html
{% extends 'accounts/base.html' %}
{% load static %}
{% block title %}{{ profile_user.username }}{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="{% static 'accounts/static/css/profile.css' %}">
{% endblock %}

{% block content %}
<h1>{{ profile_user.username }} のプロフィール</h1>

<div class="profile-meta">
  <p>アカウント作成日: {{ profile_user.date_joined|date:"Y年m月d日 H:i" }}</p>
  <p>bio: {{ profile_user.bio|default:"(未設定)" }}</p>
  <p>battle point: {{ profile_user.battle_point }}</p>

  {% if request_user.is_authenticated and request_user.username == profile_user.username %}
      <a href="{% url 'accounts:edit_profile' %}">編集</a>
      <a href="{% url 'accounts:delete_account' %}">削除</a>
  {% endif %}
</div>
{% endblock %}
CPsite/accounts/templates/accounts/edit_profile.html
{% extends 'accounts/base.html' %}
{% load static %}
{% block title %}プロフィール編集{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="{% static 'accounts/static/css/edit_profile.css' %}">
{% endblock %}

{% block content %}
<h1>プロフィール編集</h1>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">更新</button>
</form>
{% endblock %}
CPsite/accounts/templates/accounts/delete_account.html
{% extends 'accounts/base.html' %}
{% load static %}
{% block title %}アカウント削除{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="{% static 'accounts/static/css/delete_account.css' %}">
{% endblock %}

{% block content %}
<h1>プロフィール編集</h1>
<h1>アカウント削除</h1>
<p>アカウントを完全に削除します。元に戻せません。</p>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">削除する</button>
</form>
{% endblock %}
CPsite/accounts/templates/accounts/user_search.html
{% extends 'accounts/base.html' %}
{% load static %}
{% block title %}ユーザー検索{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="{% static 'accounts/static/css/user_search.css' %}">
{% endblock %}

{% block content %}
<h1>ユーザー検索結果</h1>

{% if query %}
    <p>"{{ query }}" の検索結果:</p>
{% endif %}

<ul>
    {% for user in results %}
        <li>
            <a href="{% url 'accounts:profile' user.username %}">
                {{ user.username }}
            </a>
        </li>
    {% empty %}
        <p>該当ユーザーはいません。</p>
    {% endfor %}
</ul>
{% endblock %}

ついでに、その2で作成したindex.htmlも書き換えておこう。

CPsite/home/templates/home/index.html
{% load static %}
<!DOCTYPE HTML>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <link rel="stylesheet" href="{% static 'home/static/css/base.css' %}">
        <title>CPsite</title>
    </head>
    <body>
        {% include 'includes/header.html' %}
    </body>
</html>

modelも作成し終わったため、これをマイグレーションする。manage.pyと同じディレクトリ内で以下を実行する。まず、venvを有効化することもお忘れなく。

python3 manage.py makemigrations accounts
python3 manage.py migrate
(venv) cpserver@CP-server:~/workdir/CPsite$ python3 manage.py makemigrations accounts
Migrations for 'accounts':
  accounts/migrations/0001_initial.py
    + Create model User
(venv) cpserver@CP-server:~/workdir/CPsite$ python3 manage.py migrate
Operations to perform:
  Apply all migrations: accounts, admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying accounts.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying sessions.0001_initial... OK

制作した後、以下のコマンドでgunicornに読み込ませる。

sudo systemctl restart gunicorn

これで、サイトにアクセスしてみてほしい。サインアップページだと以下のようになった。適当なユーザーでも作って遊んでみよう。
image.png

管理者ユーザー作成しよう

次に、スーパーユーザーを登録する。これを行うことで、「ドメイン/admin」に接続すると管理画面を参照し、データベースの手動操作を行うことができる。manage.pyと同じディレクトリ内で

python3 manage.py createsuperuser

そうすると、以下の要素の設定が求められる。

要素 説明
ユーザー名 (任意)
メールアドレス (任意)
パスワード (8文字以上なら任意)

私は、ユーザー名はadminとしメールアドレスは適当にadmin@example.comとした。パスワードはデータベースと同様の理由で別々の強力なパスワードのほうが望ましい。メールアドレスを適当にした理由は後々変更できるためである。制作した後、以下のコマンドでgunicornに読み込ませる。

sudo systemctl restart gunicorn

早速、urlにcpserver/adminとつけてみてほしい。
image.png
こんな感じになれば成功しているということ。hogehogeはさっき適当に作った名前。
adminでログインしてみる。
image.png
こんな感じにログインできたら、立派な管理者権限をもつ運営者だ。
image.png
ユーザーとかを押せば、こういう感じになると思う。
hogehogeを押してダウンスクロールすれば
image.png
こことかを好きに閲覧できる。さながら神。
今日はアカウント作れたところで、終わり!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?