2
4

More than 1 year has passed since last update.

Django ランダムかつ有効期限のあるURLを生成し、上位者に承認してもらいアカウントを発行する

Last updated at Posted at 2022-02-19

環境

Windows 11 Home
Python 3.10.2
Django 4.0.2
venv利用あり
※関連記事 第1回が完了していること

関連記事

Django 第1回:Django Custom User Model の作成
Django 第2回:Django 初回ログイン時にパスワード変更を強制する
Django 第3回:Django 一定期間パスワードを変更していないユーザにパスワード変更を強制する
Django 第4回:Django ランダムかつ有効期限のあるURLを生成し、上位者に承認してもらいアカウントを発行する 今回
Django 第5回:Django パスワード試行回数ロックとランダムかつ有効期限付きURLでの本人確認による解除

背景

アカウント作成をする際、管理者が1件ずつ処理するのは現実的ではない。
そこでユーザからのリクエストを本Appで受け付け、上位者がそれを承認することでアカウントを自動作成することを想定する。
上長はアカウント不要とし、その代わりに上長向けにランダムなURLを発行し、そこで承認か非承認かを選択いただく。
またURLには有効期間を設け、長時間放置されるような申請については自動的にURLを無効化とする。
今回は1次承認者は申請者が自由に記載できるようにするが、システム開発責任者等に固定してもよい。
(申請者→申請者上長→システム開発責任者)

手順1:App作成

以下コマンドでcreate_userAppを作成する。
python manage.py startapp create_user

settings.pyに作成したAppを追加する

mysite\settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'users',
    'create_user', # 追加
]

手順2:models.py

create_user\models.py
from django.db import models
import uuid as uuid_lib

class ApproveUser(models.Model):
    class Meta:
        verbose_name = 'アカウント申請ユーザ'
        verbose_name_plural = 'アカウント申請ユーザ'
    
    # ユーザ申請時に作成・更新
    uuid = models.UUIDField(default=uuid_lib.uuid4, primary_key=True, editable=False) # 管理ID
    username = models.CharField(unique=False, max_length=30) # ユーザ氏名、同姓同名があるためunique=False
    email = models.EmailField(unique=False) # ユーザメールアドレス、何度か申請することを考えunique=False
    application_date = models.DateTimeField() # 申請日時
    first_approver_name = models.CharField(max_length=30, unique=False) # 1次承認者氏名
    first_approver_email = models.EmailField(unique=False) # 1次承認者メールアドレス
    first_approver_token = models.CharField(max_length=50) # 1次承認者用URLトークン
    first_approver_expired_seconds = models.IntegerField() # 1次承認者用トークン有効時間
    first_approver_url = models.URLField() # 1次承認用URL

    # 1次承認時に更新
    first_approver_time = models.DateTimeField(blank=True, null=True) # 1次承認日時
    first_approver_result = models.BooleanField(blank=True, null=True) # 1次承認結果

以下2つのコマンドでDBに反映をする。
python manage.py makemigrations
image.png
(Aprooveuserになっていますが、正しくは記載のコードのとおりApproveuserです)

python manage.py migrate
image.png

手順2:forms.py

次に作成したモデルをベースにフォームを作成する。
create_user 配下に forms.pyを作成する。
image.png

以下のように記載する。

create_user\forms.py
from django import forms
from .models import ApproveUser # models.pyで作成したmodel

class AppricationForm(forms.ModelForm):
    """アカウント申請フォーム"""

    class Meta:
        model = ApproveUser
        fields = ['username', 'email', 'first_approver_name', 'first_approver_email',] # Formに表示される項目
        # Formに実際に表示される文字
        labels = {
            'username': '申請者 氏名',
            'email': '申請者 メールアドレス',
            'first_approver_name': '1次承認者 氏名',
            'first_approver_email': '1次承認者 メールアドレス',
        }
        Widgets = {
            'username': forms.TextInput(attrs={'placeholder': '山田 太郎'}),
            'email': forms.TextInput(attrs={'placeholder': 'xxxx.yyyy@gmail.com'}),
            'first_approver_name': forms.TextInput(attrs={'placeholder': '日本 花子'}),
            'first_approver_email': forms.TextInput(attrs={'placeholder': 'zzzz.zzzz@gmail.com'}),
        }
    def clean(self):
        """申請者と1次承認が同じメールアドレスは不可"""
        cleaned_data = super().clean()
        email = cleaned_data.get('email')
        first_approver_email = cleaned_data.get('first_approver_email')
        if email == first_approver_email:
            raise forms.ValidationError('申請者と1次承認者のメールアドレスを同じにはできません')
        return cleaned_data

class FirstApproveForm(forms.ModelForm):
    """1次承認フォーム"""
    # 1次承認時に変更できなくさせる(表示のみ)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['readonly'] = 'readonly'
        self.fields['email'].widget.attrs['readonly'] = 'readonly'
        self.fields['first_approver_name'].widget.attrs['readonly'] = 'readonly'
        self.fields['first_approver_email'].widget.attrs['readonly'] = 'readonly'


    class Meta:
        model = ApproveUser
        fields = ['username', 'email', 'first_approver_name', 'first_approver_email', 'first_approver_result',] # Formに表示される項目
        labels = {
            'username': '申請者 氏名',
            'email': '申請者 メールアドレス',
            'first_approver_name': '1次承認者 氏名',
            'first_approver_email': '1次承認者 メールアドレス',
            'first_approver_result': '1次承認結果',
        }

        first_approver_result = forms.BooleanField() # 1次承認の結果(True/False)

手順3:views.py

次に作成したViewで動作を決めていく。

create_user\views.py
from django.shortcuts import render
from datetime import timedelta
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.utils import timezone
from .forms import AppricationForm
from .models import ApproveUser
from users.models import User
import string
import random
import math
import secrets

Token_LENGTH = 30 # ランダムURLを作成するためのTOKEN
PASSWORD_LENGTH = 12 # パスワードの長さ
EXPIRE_SECONDS = 60 #ランダムURLの有効期限(秒)

def pass_gen(size=PASSWORD_LENGTH):
    """パスワードジェネレータ"""
    chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
    return "".join(secrets.choice(chars) for x in range(size))

def create_account(username, email):
    """アカウント作成"""

    # passoword生成
    def_pass = pass_gen(PASSWORD_LENGTH)

    try:
        # すでにパスワードがある場合
        us = User.objects.get(email=email)

        # password設定
        us.set_password(def_pass)
        us.password_changed = False # Falseであれば次回ログイン時にパスワード変更画面に強制リダイレクト

        # 権限付与
        us.is_active = True
        us.is_staff = True

        # save
        us.save()

        return def_pass
    
    except:
        # Userの作成
        us = User.objects.create_user(username=username, email=email, password=def_pass)

        # 権限付与
        us.is_active = True
        us.is_staff = True

        # save
        us.save()

        return def_pass

def get_random_chars(char_num=Token_LENGTH):
    """ランダムな文字列を作る"""
    return "".join([random.choice(string.ascii_letters + string.digits) for i in range(char_num)])


def create(request):
    """アカウント申請画面"""
    if request.method == 'POST':
        # POSTであればフォームからデータを受け取る
        form = AppricationForm(request.POST)
        if form.is_valid():
            obj = form.save(commit=False)

            try:
                # アカウントがすでにあるかどうか
                # ある場合も受け付けたように処理する(悪意のあるユーザにアカウント有無を判別されないようにする)
                queryset = User.objects.get(email=form.cleaned_data['email'])
                email = queryset.email
                is_active = queryset.is_active
                is_staff = queryset.is_staff

                if is_active == True and is_staff == True:
                    # すでに有効なアカウントがある場合
                    message = """
                    titte:アカウント申請
                    message:有効なアカウントが存在します。
                    """
                    print(message)
            except:
                # 有効なアカウントがないので処理を続ける
                pass

            # 期限付きURLの作成
            Timestamp_signer = TimestampSigner()
            context = {}
            context["expired_seconds"] = EXPIRE_SECONDS # URLの有効期限
            token = get_random_chars()
            token_signed = Timestamp_signer.sign(token) # ランダムURLの生成
            context["token_signed"] = token_signed

            obj.application_date = timezone.now() # 申請日時
            obj.first_approver_token = token
            obj.first_approver_url = f"http://127.0.0.1:8000/create_user/{token_signed}"
            obj.first_approver_expired_seconds = EXPIRE_SECONDS

            first_approver_name = form.cleaned_data['first_approver_name']
            first_approver_email = form.cleaned_data['first_approver_email']
            username = form.cleaned_data['username']
            email = form.cleaned_data['email']

            message =f"""
            title:アカウント1次承認依頼
            message:{first_approver_name} ({first_approver_email})さん 

            {username} ({email})さんよりアカウント承認依頼が届いています。
            以下のURLより承認を行ってください。

            {obj.first_approver_url}

            以上
            """
            print(message)
            context = {
                'title': 'アカウント申請完了',
                'message': 'アカウントを申請しました',
            }
            return render(request, 'create_user/create_request_done.html', context)
        else:
            # データが不正であればフォームを再描画する
            context = {'form': form}
            return render(request, 'create_user/create.html', context)

    else:
        # RequestがGET
        context = {
            'title': 'アカウント申請',
            'form': AppricationForm(),
        }
        return render(request, 'create_user/create.html', context)

def create_request_done(request):
    """アカウント申請完了画面"""
    context = {
        'title': 'アカウント申請完了',
        'message': 'アカウントを申請しました',
    }
    return render(request, 'create_user/create_request_done.html', context)

手順4:htmlの作成

描画するHTMLがないので作成する。
/mysite/create_user配下にtemplatesフォルダを作り、さらにその配下にcreate_userフォルダを作成する。
/mysite/create_user/templates/create_user配下に以下の二つの新規ファイルを作成する

  • create_request_done.html
  • create.html

image.png

/mysite/create_user/templates/create_user/create_request_done.html
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ title }}</h1>
    <p>{{ message }}</p>
</body>
</html>
/mysite/create_user/templates/create_user/create.html
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <form action="{% url 'create_user:create' %}"method="post">
        {% csrf_token %}
            <table class="table">
                {{ form.as_table }}
                    <tr><th><td>
                        <input type="submit" value="送信" class="btn">
                    </td></th></tr>

            </table>
    </form>
</body>
</html>

手順5:urls.py

ViewとURLを紐づける。
/mysite/create_user配下にurls.pyを作成する。
image.png

/mysite/create_user/urls.py
from django.urls import path
from . import views

app_name = "create_user" # URLパターンを逆引きできるように名前を付ける

urlpatterns = [
    path('create/', views.create, name = 'create'), # アカウント申請画面
    path('create/create_request_done', views.create_request_done, name="create_request_done"),
]

手順5:mysiteのurls.py

mysite配下のurls.pyを更新し、create_user配下のurls.pyを読み込むようにする。

/mysite/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('create_user/', include('create_user.urls'))
]

手順6:動作の確認(途中まで)

この時点で、申請のページまでは作成できたので動作を確認する。
python manage.py runserverでサーバを立ち上げ、
http://127.0.0.1:8000/create_user/create/ にアクセスする。
CSSを設定していないので簡素だがフォームが表示される。

image.png

適当に入力して送信を押す
image.png

申請完了ページが表示される。
image.png

一方、申請した内容は1次承認者に送られるが、ここではPrint文で出力しているのでそれを確認する。
実際にはここでメールを1次承認者に送信する。

image.png

申請まではできたので、次はこのランダムURLから1次承認者が承認し、アカウントを払い出す部分を追加していく。

手順7:views.pyの更新

ランダムURLにアクセスしたさいのViewを作成していく

create_user\views.py
from django.shortcuts import render
from datetime import timedelta
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.utils import timezone
from django.views.generic.base import TemplateView
from .forms import AppricationForm, FirstApproveForm
from .models import ApproveUser
from users.models import User
import string
import random
import math
import secrets

Token_LENGTH = 30 # ランダムURLを作成するためのTOKEN
PASSWORD_LENGTH = 12 # パスワードの長さ
EXPIRE_SECONDS = 60 #ランダムURLの有効期限(秒)

def pass_gen(size=PASSWORD_LENGTH):
    """パスワードジェネレータ"""
    chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
    return "".join(secrets.choice(chars) for x in range(size))

def create_account(username, email):
    """アカウント作成"""

    # passoword生成
    def_pass = pass_gen(PASSWORD_LENGTH)

    try:
        # すでにパスワードがある場合
        us = User.objects.get(email=email)

        # password設定
        us.set_password(def_pass)
        us.password_changed = False # Falseであれば次回ログイン時にパスワード変更画面に強制リダイレクト

        # 権限付与
        us.is_active = True
        us.is_staff = True

        # save
        us.save()

        return def_pass
    
    except:
        # Userの作成
        us = User.objects.create_user(username=username, email=email, password=def_pass)

        # 権限付与
        us.is_active = True
        us.is_staff = True

        # save
        us.save()

        return def_pass

def get_random_chars(char_num=Token_LENGTH):
    """ランダムな文字列を作る"""
    return "".join([random.choice(string.ascii_letters + string.digits) for i in range(char_num)])


def create(request):
    """アカウント申請画面"""
    if request.method == 'POST':
        # POSTであればフォームからデータを受け取る
        form = AppricationForm(request.POST)
        if form.is_valid():
            obj = form.save(commit=False)

            try:
                # アカウントがすでにあるかどうか
                # ある場合も受け付けたように処理する(悪意のあるユーザにアカウント有無を判別されないようにする)
                queryset = User.objects.get(email=form.cleaned_data['email'])
                email = queryset.email
                is_active = queryset.is_active
                is_staff = queryset.is_staff

                if is_active == True and is_staff == True:
                    # すでに有効なアカウントがある場合
                    message = """
                    titte:アカウント申請
                    message:有効なアカウントが存在します。
                    """
                    print(message)
            except:
                # 有効なアカウントがないので処理を続ける
                pass

            # 期限付きURLの作成
            Timestamp_signer = TimestampSigner()
            context = {}
            context["expired_seconds"] = EXPIRE_SECONDS # URLの有効期限
            token = get_random_chars()
            token_signed = Timestamp_signer.sign(token) # ランダムURLの生成
            context["token_signed"] = token_signed

            obj.application_date = timezone.now() # 申請日時
            obj.first_approver_token = token
            obj.first_approver_url = f"http://127.0.0.1:8000/create_user/{token_signed}"
            obj.first_approver_expired_seconds = EXPIRE_SECONDS

            first_approver_name = form.cleaned_data['first_approver_name']
            first_approver_email = form.cleaned_data['first_approver_email']
            username = form.cleaned_data['username']
            email = form.cleaned_data['email']

            message =f"""
            title:アカウント1次承認依頼
            message:{first_approver_name} ({first_approver_email})さん 

            {username} ({email})さんよりアカウント承認依頼が届いています。
            以下のURLより承認を行ってください。

            {obj.first_approver_url}

            以上
            """
            print(message)
            context = {
                'title': 'アカウント申請完了',
                'message': 'アカウントを申請しました',
            }
            return render(request, 'create_user/create_request_done.html', context)
        else:
            # データが不正であればフォームを再描画する
            context = {'form': form}
            return render(request, 'create_user/create.html', context)

    else:
        # RequestがGET
        context = {
            'title': 'アカウント申請',
            'form': AppricationForm(),
        }
        return render(request, 'create_user/create.html', context)

def create_request_done(request):
    """アカウント申請完了画面"""
    context = {
        'title': 'アカウント申請完了',
        'message': 'アカウントを申請しました',
    }
    return render(request, 'create_user/create_request_done.html', context)

class ApproveView(TemplateView):
    template_name = 'create_users/approve.html'

    def get(self, request, token):
        if request.method == "GET":
            # GET = ランダムURLにアクセスしてきた際
            timestamp_signer = TimestampSigner()
            if token:
                try:
                    # TOKENが有効なら
                    unsigned_token = timestamp_signer.unsign(token, max_age=timedelta(seconds=EXPIRE_SECONDS))

                    user = ApproveUser.objects.get(first_approver_token=unsigned_token)

                    if user.first_approver_time == None:
                        # TOKEN未使用ならば
                        initial_dict = {
                            'username': user.username,
                            'email': user.email,
                        }
                        params = {
                            'title': '承認ページ',
                            'form': FirstApproveForm(initial=initial_dict)
                        }
                        return render(request, 'create_user/approve.html', params)
                    else:
                        params = {
                            'title': '承認ページ',
                            'message': 'すでに承認あるいは否認済みです'
                        }
                        return render(request, 'create_user/approve_done.html', params)
                
                except SignatureExpired:
                    # TOKENが期限切れならば
                    params = {
                        'title': '承認ページ',
                        'message': '有効期限切れにより自動的に否認されました'
                    }
                    return render(request, 'create_user/approve_done.html', params)
    
                except BadSignature:
                    # TOKENが不正ならば(URLが間違っていたら)
                    params = {
                        'title': '承認ページ',
                        'message': 'URLが正しくありません'
                    }
                    return render(request, 'create_user/approve_done.html', params)

            else:
                # TOKENがない
                params = {
                    'title': '承認ページ',
                    'message': 'トークンが見つかりません'
                }
                return render(request, 'create_user/approve_done.html', params)
    
    def post(self, request, token):

        if request.method == "POST":
        # 承認・否認をする際
            timestamp_signer = TimestampSigner()
            unsigned_token = timestamp_signer.unsign(token, max_age=timedelta(seconds=EXPIRE_SECONDS))

            try:
                obj = ApproveUser.objects.get(first_approver_token=unsigned_token)
                form = FirstApproveForm(request.POST)

                if form.is_valid():
                    # パラメータ更新
                    obj.first_approver_time = timezone.now()
                    obj.first_approver_result = form.cleaned_data['first_approver_result']
                    obj.save()

                    # アカウント作成
                    username = obj.username
                    email = obj.email
                    password = create_account(username, email)

                    message = """
                    title:アカウント払い出し完了
                    message:{username} ({email})さん 

                    アカウントの払い出しが完了しました。

                    id : {email}
                    password : {password}
                    
                    以上
                    """
                    print(message)

                    params = {
                        'title': '承認ページ',
                        'message': '処理が完了しました'
                    }
                    return render(request, 'create_user/approve_done.html', params)
                
                else:
                    params = {
                        'title': '承認ページ',
                        'message': 'エラーが発生しました'
                    }
                    return render(request, 'create_user/approve_done.html', params)
            except:
                params = {
                    'title': '承認ページ',
                    'message': 'エラーが発生しました'
                }
                return render(request, 'create_user/approve_done.html', params)

手順8:承認用htmlの作成

/mysite/create_user/templates/create_user配下に以下の新規ファイルを作成する

  • approve_done.html
  • approve.html

actionの部分が先ほどと異なる

image.png

/mysite/create_user/templates/create_user/approve_done.html
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ title }}</h1>
    <p>{{ message }}</p>
</body>
</html>
/mysite/create_user/templates/create_user/approve.html
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <form action="" method="post">
        {% csrf_token %}
            <table class="table">
                {{ form.as_table }}
                    <tr><th><td>
                        <input type="submit" value="送信" class="btn">
                    </td></th></tr>

            </table>
    </form>
</body>
</html>

手順9:urls.pyの更新

ViewとランダムURLを紐づける。

/mysite/create_user/urls.py
from django.urls import path
from . import views

app_name = "create_user" # URLパターンを逆引きできるように名前を付ける

urlpatterns = [
    path('create/', views.create, name = 'create'), # アカウント申請画面
    path('create/create_request_done', views.create_request_done, name="create_request_done"),
    path('<str:token>/', views.ApproveView.as_view(), name = 'ApproveView'), # 追加
]

手順10:動作確認

アクセスして必要事項を入力
http://127.0.0.1:8000/create_user/create/

image.png

コンソールに(メールで送るべき)内容が表示されている。
image.png

記載のURLにアクセスする。
1次承認結果以外の欄はReadonly属性を指定しているので文字の変更はできない。
image.png

承認するかを選択
image.png

承認完了
image.png

コンソールにはPrint文でアカウントの払い出し完了が出力されている
image.png

承認(否認)後に再度アクセスすると
image.png

期間経過後
image.png

手順11:admin.py

管理画面で申請履歴を確認できるようにするには以下の手順を行う。
管理画面で見えなくていい場合は実施する必要はない。
なおアカウント申請履歴=重要な履歴として、アカウント申請履歴データは削除させないものとする。

/mysite/create_user/admin.py
from django.contrib import admin
from django.utils.translation import gettext, gettext_lazy as _
from .models import ApproveUser

@admin.register(ApproveUser)
class ApproveUserAdmin(admin.ModelAdmin):
    fieldsets = (
        (None, {'fields': ('username',)}),
        (_('Personal info'), {'fields': ('email', 'application_date',)}),
        (_('First Approver info'), {'fields': ('first_approver_name', 'first_approver_email', 'first_approver_url', 'first_approver_token', 'first_approver_expired_seconds',)}),
        (_('First Approver Result'), {'fields': ('first_approver_time', 'first_approver_result',)}),
    )
    list_display = ('username', 'email', 'application_date', 'first_approver_name', 'first_approver_email', 'first_approver_time', 'first_approver_result',)
    search_field = ('username', 'email',)
    ordering = ('-application_date',)

    def get_actions(self, request):
        # 一覧画面から削除できないようにする
        actions = super().get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

    def has_delete_permission(self, request, obj=None):
        # 詳細画面から削除できないようにする
        return False

http://127.0.0.1:8000/admin/ にアクセスする。
CREATE_USER アカウント申請ユーザ が追加されているのが確認できる。
image.png

クリックすると申請一覧が確認できる。(削除できないことも確認)
image.png

USERNAMEをクリックして詳細を確認できる。(詳細でも削除できないことも確認)
image.png
image.png

今後の改善点

create_user\forms.py

class FirstApproveForm(forms.ModelForm):
...
    """1次承認フォーム"""
    # 1次承認時に変更できなくさせる(表示のみ)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['readonly'] = 'readonly'
        self.fields['email'].widget.attrs['readonly'] = 'readonly'
        self.fields['first_approver_name'].widget.attrs['readonly'] = 'readonly'
        self.fields['first_approver_email'].widget.attrs['readonly'] = 'readonly'
...

の部分を

create_user\forms.py

class FirstApproveForm(forms.ModelForm):
...
    """1次承認フォーム"""
    # 1次承認時に変更できなくさせる(表示のみ)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['disabled'] = 'disabled'
        self.fields['email'].widget.attrs['disabled'] = 'disabled'
        self.fields['first_approver_name'].widget.attrs['disabled'] = 'disabled'
        self.fields['first_approver_email'].widget.attrs['disabled'] = 'disabled'
...

とすることで、承認者に選択できない個所を明確にすることができる。(グレーアウトできる)
しかしながらその状態ではい いいえを選択するとエラーになってしまったことからdisabledは不採用とした。

image.png

選択肢・ボタンも冴えない。CSS、Bootstrapなどを使用することで、雰囲気を変えることができる。
image.png

参考

Django ランダムなURLをUUIDで作成する方法
Djangoの標準機能だけで期限つきリンクを作ってみる。
Django 第1回:Django Custom User Model の作成
Django 第2回:Django 初回ログイン時にパスワード変更を強制する
Django 第3回:Django 一定期間パスワードを変更していないユーザにパスワード変更を強制する
Django 第4回:Django ランダムかつ有効期限のあるURLを生成し、上位者に承認してもらいアカウントを発行する 
Django 第5回:Django パスワード試行回数ロックとランダムかつ有効期限付きURLでの本人確認による解除

2
4
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
2
4