はじめに
Djnagoとdjango-allauthを使ってGoogleの2段階認証を実装します。
通常、Googleのアカウントを持っていると誰でもログイン(2段階認証)できますが、今回の仕様上、予めDjangoの管理画面でCustomUserに登録したメールアドレスを持つユーザにログインを許可します。
また、管理側でログインを許可するユーザを制御する関係上、ログイン、ログアウトのみテンプレートを用意し、それ以外は404エラーとして処理します。
Googleの2段階認証とDjangoが作るCookieの有効期限は異なるので、Cookieの期限はsettings.pyで制御します。
ログ機能も付けているので、不要なら削除します。
作業手順
Djangoのプロジェクトを作って、
mkdir ProjectName cd ProjectName django-admin startproject config .
ログを記録するなら、下記のsettings.pyに設定したようにディレクトリを作成して、
mkdir log
ライブラリをインストールして、
pipenv install django-allauth
CustomUserを作成して、
python manage.py startapp users
今回、掲載した複数のファイルを修正して、makemigrations、migrateを実行して、
python manage.py makemigrations python manage.py migrate
管理ユーザを作成して、
python manage.py createsuperuser
Djangoの管理画面で、Usersテーブルに新規ユーザ(メールアドレスを登録)を作成してからテストします。
python manage.py runserver 192.168.10.100
構成
実際はpipenvで作った仮想環境で行っています。
Debian GNU Linux(Bookworm) Python 3.10.13 Django 5.1.2 django-allauth 65.0.2 pipenv 2023.10.3 google-api-core 2.21.0 google-api-python-client 2.149.0 google-auth 2.35.0 google-auth-httplib2 0.2.0 google-auth-oauthlib 1.2.1 googleapis-common-protos 1.65.0
ディレクトリ構成
django-allauthのルーティングをCustomUserのルーティング(urls.py)で上書きして使いますが、ひとつだけ制御できなかったので、ローカルにdjango-allauthのテンプレート構成を再現してファイルを置いています。
ただ、urls.pyで制御できなかったファイルが
.venv/lib/python3.10/site-packages/allauth/templates/socialaccount/login_cancelled.html
と分かったので、以下のように対処しました。
・ローカルにdjango-allauthのテンプレート用ディレクトリを作成。
・404.htmlをコピーしてlogin_cancelled.htmlに名前変更。
・settings.pyのTEMPLATESにローカルのallauthテンプレートを参照するようにパスを追加。
今回、関係ありそうなファイルは以下の通りです。
Bootstrapはダウンロード・解凍して、設置しています。
project/ - config/ settings.py - templates/ - 404.html - home.html - base.html - allauth/ - socialaccount/ login_cancelled.html - users login.html logout.html - users/ admin.py forms.py models.py urls.py views.py - static - css bootstrap.min.css bootstrap.min.css.map - js bootstrap.min.js bootstrap.min.js.map - favicon favicon.ico
django-allauthのインストール
pipの場合は、
pip install django-allauth
pipenvを使っている場合は、
cd project pipenv shell pipenv install django-allauth
settings.py
django-allauthに関連する部分の抜粋です。
デバッグ中はデータベースを削除、作成を繰り返すことが多く、Webで毎回設定するのが面倒なのでGoogleのclient_id、secretは直書きします。
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', # django-allauth 'allauth', # django-allauth 'allauth.account', # django-allauth 'allauth.socialaccount', # django-allauth 'allauth.socialaccount.providers.google', # django-allauth 'users.apps.UsersConfig', # CustomUser 省略 ] # カスタムユーザーモデル AUTH_USER_MODEL = 'users.CustomUser' SITE_ID = 1 AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'allauth.account.auth_backends.AuthenticationBackend', # allauth用 ) ACCOUNT_AUTHENTICATION_METHOD = 'email' ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_USERNAME_REQUIRED = False # メール認証無し ACCOUNT_EMAIL_VERIFICATION = 'none' # CustomUserモデルにusernameは不要 ACCOUNT_USER_MODEL_USERNAME_FIELD = None ACCOUNT_UNIQUE_EMAIL = True SOCIALACCOUNT_EMAIL_REQUIRED = True SOCIALACCOUNT_AUTO_SIGNUP = True # トークンを保存する SOCIALACCOUNT_STORE_TOKENS = True # home.html LOGIN_URL = 'home' LOGOUT_REDIRECT_URL = 'home' ACCOUNT_LOGOUT_REDIRECT_URL = 'home' LOGIN_REDIRECT_URL = 'home' SOCIALACCOUNT_ADAPTER = 'users.views.CustomSocialAccountAdapter' # Googleのclient_idとsecretを直に記述 SOCIALACCOUNT_PROVIDERS = { 'google': { "APPS": [ { "client_id": 'xxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.apps.googleusercontent.com', "secret": 'xxxxxx-xxxxxx-xxxxxx', "key": '', "sites": '127.0.0.1', }, ], 'SCOPE': [ 'profile', 'email', ], 'AUTH_PARAMS': { 'access_type': 'online', }, # PKCEを有効化 'OAUTH_PKCE_ENABLED': True, } } ''' Cookieのセッション管理 優先順位 1. SESSION_EXPIRE_AT_BROWSER_CLOSE 2. SESSION_COOKIE_AGE 3. SESSION_SAVE_EVERY_REQUEST ''' # Cookie有効期間(秒) SESSION_COOKIE_AGE = 24 * 60 * 60 * 7 # 設定された期限に関係なく、ブラウザを閉じたらセッションを終了 #SESSION_EXPIRE_AT_BROWSER_CLOSE = True # 全てのリクエストでセッションを保存する。 #SESSION_SAVE_EVERY_REQUEST = True ''' infoとerrorで出力ファイルを分別する。 CRITICAL > ERROR > WARNING > INFO > DEBUG > NOTSET ''' LOGGING = { 'version': 1, 'disable_existing_loggers': False, # 時刻を自動的に挿入する。 # 例: # 2024-06-25 15:19:48,384 : users.views : INFO : Login emailaddress 'formatters': { 'verbose': { 'format': '%(asctime)s : %(name)s : %(levelname)s : %(message)s' }, }, 'handlers': { 'info_handlers': { #'level': 'INFO', 'level': 'WARNING', 'class': 'logging.handlers.RotatingFileHandler', 'filename': BASE_DIR / 'log/info.log', 'maxBytes': 1024*1024*5, # 5 MB 'backupCount': 5, 'formatter': 'verbose', }, 'error_handlers': { 'level': 'ERROR', 'class': 'logging.handlers.RotatingFileHandler', 'filename': BASE_DIR / 'log/error.log', 'maxBytes': 1024*1024*5, # 5 MB 'backupCount': 5, 'formatter': 'verbose', }, }, 'loggers': { '': { 'level': 'INFO', #'handlers': ['info_handlers', 'error_handlers'], 'handlers': ['info_handlers',], }, }, }
login_cancelled.html
「構成」に記載した理由から仕方なく作成。
{% extends 'base.html' %} {% block title %} ページが見つかりませんでした {% endblock title %} {% block h1_title %} It is just a 404 Error ! {% endblock h1_title %} {% block error_404 %} <p class="text-center"> Sorry, the requested URL was not found on this server. <br> <a href="{% url 'account_login' %}" class="btn btn-outline-danger">Login</a> </p> {% endblock error_404 %}
models.py
AbstractBaseUserを使ってCustomUserを作ります。
usenameは使わないのでemailで代用します。
また、ユーザデータは今回の仕様上、管理者のみ操作します。
from django.db import models from django.contrib import auth from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager from django.core.mail import send_mail from django.contrib.auth.hashers import make_password from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import PermissionsMixin import uuid class UserManager(BaseUserManager): use_in_migrations = True def _create_user(self, email, password=None, **extra_fields): if not email: raise ValueError('The given email must be set') email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.password = make_password(password) user.save(using=self._db) return user def create_user(self, email, password=None, **extra_fields): extra_fields.setdefault('is_staff', False) extra_fields.setdefault('is_superuser', False) return self._create_user(email, password, **extra_fields) def create_superuser(self, email=None, password=None, **extra_fields): extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', True) if extra_fields.get('is_staff') is not True: raise ValueError('Superuser must have is_staff=True.') if extra_fields.get('is_superuser') is not True: raise ValueError('Superuser must have is_superuser=True.') if not password: raise ValueError('Password is required for super users.') return self._create_user(email, password, **extra_fields) def with_perm(self, perm, is_active=True, include_superusers=True, backend=None, obj=None): if backend is None: backends = auth._get_backends(return_tuples=True) if len(backends) == 1: backend, _ = backends[0] else: raise ValueError( 'You have multiple authentication backends configured and ' 'therefore must provide the `backend` argument.' ) elif not isinstance(backend, str): raise TypeError( 'backend must be a dotted import path string (got %r).' % backend ) else: backend = auth.load_backend(backend) if hasattr(backend, 'with_perm'): return backend.with_perm( perm, is_active=is_active, include_superusers=include_superusers, obj=obj, ) return self.none() ''' カスタムユーザ定義 ''' class CustomUser(AbstractBaseUser, PermissionsMixin): # Primary KeyをUUIDに変更 id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False) email = models.EmailField(verbose_name='Email address', unique=True) is_staff = models.BooleanField( _('staff status'), default=False, help_text=_('Designates whether the user can log into this admin site.'), ) is_active = models.BooleanField( _("active"), default=True, help_text=_( "Designates whether this user should be treated as active. " "Unselect this instead of deleting accounts." ), ) # 追加フィールド created_time = models.DateTimeField(verbose_name='作成日時', auto_now_add=True) updated_time = models.DateTimeField(verbose_name='更新日時', auto_now=True) name = models.CharField(verbose_name='氏名', max_length=512) section = models.CharField(verbose_name='所属', max_length=1024) remarks = models.TextField(verbose_name='備考', blank=True, null=True) objects = UserManager() EMAIL_FIELD = 'email' USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] class Meta: verbose_name = 'ユーザー' verbose_name_plural = 'ユーザー' ''' ACCOUNT_USER_MODEL_USERNAME_FIELDを設定しているが、django-allauthでusernameが必要なため usernameとしてemailを返すように設定。 ''' @property def username(self): return self.email def clean(self): super().clean() self.email = self.__class__.objects.normalize_email(self.email)
views.py
ログインとログアウトの処理をオーバーライドします。
登録ユーザの有無を検査してからGoogleの2段階認証を行っているので、EmailAddressテーブルにメールアドレスを登録し、CustomUserとリンクを設定するのは自前で行います。
from django.contrib.auth import get_user_model from django.contrib.auth.signals import user_logged_in from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages from django.dispatch import receiver from django.conf import settings from django.shortcuts import render from django.core.exceptions import ValidationError from allauth.account.views import LoginView, LogoutView from allauth.account.models import EmailAddress from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter from allauth.socialaccount.providers.oauth2.views import OAuth2LoginView from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from .forms import CustomLoginForm from .models import CustomUser import logging logger = logging.getLogger(__name__) # Googleの2段階認証に伴う処理 class CustomSocialAccountAdapter(DefaultSocialAccountAdapter): def pre_social_login(self, request, sociallogin): email = sociallogin.user.email try: # CustomUserに登録されているか確認 user = CustomUser.objects.get(email=email) # CustomUserモデルのインスタンスを保存する(ルーティングに必要) sociallogin.user = user sociallogin.user.save() # メールアドレスを登録してCustomUserモデルと紐づける。 email_address, created = EmailAddress.objects.get_or_create( user=user, email=email, defaults={'primary': True, 'verified': True} ) if created: logger.info(f'{request.META.get("REMOTE_ADDR")} {email} email successfully created. (EmailAddress table)') except CustomUser.DoesNotExist: # 未登録の場合はエラーを発生させる messages.error(request, 'Your email is not registered. #1') raise ValidationError("This email is not registered.") # Googleの2段階認証が失敗した場合に、EmailAddressテーブルからメールアドレスを削除する。 def authentication_failed(self, request, sociallogin): email = sociallogin.user.email try: email_address = EmailAddress.objects.get(email=email) email_address.delete() logger.info(f'{request.META.get("REMOTE_ADDR")} {email} email removed due to authentication failure.') except EmailAddress.DoesNotExist: logger.info(f'{request.META.get("REMOTE_ADDR")} {email} No email found to remove on authentication failure.') # Googleの2段階認証でログイン処理を行う。 class CustomLoginView(LoginView): template_name = 'users/login.html' form_class = CustomLoginForm def post(self, request, *args, **kwargs): form = self.form_class(request.POST) next_url = request.POST.get('next', 'home') if form.is_valid(): email = form.cleaned_data['email'] try: # CustomUserに存在するか確認 user = CustomUser.objects.get(email=email) # Google OAuth2 ログインを開始 oauth2_login = OAuth2LoginView.adapter_view(GoogleOAuth2Adapter) return oauth2_login(request) except CustomUser.DoesNotExist: messages.error(request, 'Your email is not registered. #2') return render(request, self.template_name, {'form': form}) except Exception as e: logger.error(f'{request.META.get("REMOTE_ADDR")} {email} Error during login: {str(e)}') messages.error(request, f'An error has occurred: {str(e)}') return render(request, self.template_name, {'form': form}) return render(request, self.template_name, {'form': form}) # ログアウト処理を行う。 class CustomLogoutView(LoginRequiredMixin, LogoutView): template_name = 'users/logout.html'
forms.py
from allauth.account.forms import LoginForm from django import forms from .models import CustomUser ''' views.pyで使う。 ユーザのログイン ''' class CustomLoginForm(LoginForm): email = forms.EmailField( widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'Enter your email'}), label="Email", required=True ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) del self.fields['login'] del self.fields['password'] # Bootstrapクラスを適用 for field in self.fields.values(): field.widget.attrs['class'] = 'form-control' def clean(self): return self.cleaned_data def login(self, request, redirect_url=None): pass ''' admin.pyで使う。 パスワードフィールドを除外したカスタムのユーザー作成フォーム ''' class CustomUserCreationForm(forms.ModelForm): class Meta: model = CustomUser # パスワードフィールドを除外 fields = ('email', 'name', 'section', 'remarks', 'is_staff', 'is_active') def save(self, commit=True): user = super().save(commit=False) # パスワードを使用不可に設定 user.set_unusable_password() if commit: user.save() return user ''' admin.pyで使う。 パスワードフィールドを除外したカスタムのユーザー変更フォーム ''' class CustomUserChangeForm(forms.ModelForm): class Meta: model = CustomUser # パスワードフィールドを除外 fields = ('email', 'name', 'section', 'remarks', 'is_staff', 'is_active')
urls.py
ログインとログアウト以外のdjango-allauthのルーティングは、全て404.htmlを表示させます。
ここでlogin_cancelled.htmlのルーティングを制御できれば良かったのですが・・・。
from django.urls import path, include from django.shortcuts import render from django.views.generic import TemplateView from .views import CustomLoginView, CustomLogoutView def redirect_to_404(request, exception=None): return render(request, '404.html', status=404) urlpatterns = [ # django-allauthのログインとログアウトを上書き path('account/login/', CustomLoginView.as_view(), name='account_login'), path('account/logout/', CustomLogoutView.as_view(), name='account_logout'), # Googleの2段階認証で使う。 path('account/social/login/google/', include('allauth.socialaccount.providers.google.urls')), # django-allauthのログインとログアウト以外は404エラーにリダイレクト # 確認のためnameは同じだが、URLが異なるものも記述。 path('account/login/cancelled/', redirect_to_404, name='account_login_cancelled'), path('account/login/confirm/', redirect_to_404, name='account_confirm_login_code'), path('account/signup/passkey', redirect_to_404, name='account_signup_by_passkey'), path('account/login/code/', redirect_to_404, name='account_request_login_code'), path('account/signup/', redirect_to_404, name='account_signup'), path('account/signup/', redirect_to_404, name='account_reauthenticate'), path('account/password/reset/', redirect_to_404, name='account_reset_password'), path('account/password/reset/done/', redirect_to_404, name='account_reset_password_done'), path('account/password/reset/key/', redirect_to_404, name='account_reset_password_from_key'), path('account/password/reset/key/done/', redirect_to_404, name='account_reset_password_from_key_done'), path('account/password/change/', redirect_to_404, name='account_change_password'), path('account/password/set', redirect_to_404, name='account_set_password'), path('account/email/', redirect_to_404, name='account_email'), path('account/inactive/', redirect_to_404, name='account_inactive'), path('account/confirm-email/', redirect_to_404, name='account_confirm_email'), path('account/email-verification-sent/', redirect_to_404, name='account_email_verification_sent'), path('account/social/', redirect_to_404, name='socialaccount_connections'), path('account/social/signup/', redirect_to_404, name='socialaccount_signup'), path('account/signup/', redirect_to_404, name='socialaccount_signup'), path('account/social/login/cancelled/', redirect_to_404, name='socialaccount_login_cancelled'), path('account/login/cancelled/', redirect_to_404, name='socialaccount_login_cancelled'), path('account/social/login/redirect/', redirect_to_404, name='socialaccount_login_redirect'), path('account/login/error/', redirect_to_404, name='socialaccount_login_error'), # django-allauthの定義 path('account/', include('allauth.urls')), ] handler404 = redirect_to_404
admin.py
from django.contrib import admin from django.contrib.auth import get_user_model from django.contrib.auth.admin import UserAdmin from django.utils.translation import gettext_lazy as _ from .models import CustomUser from .forms import CustomUserCreationForm, CustomUserChangeForm ''' カスタムユーザー ''' CustomUser = get_user_model() class CustomUserAdmin(UserAdmin): save_on_top = True save_as = True ''' ユーザー作成フォーム forms.pyで定義 ''' add_form = CustomUserCreationForm ''' ユーザー変更フォーム forms.pyで定義 ''' form = CustomUserChangeForm # ユーザーの詳細画面、編集画面で表示するフィールド fieldsets = [ (None, {'fields': ('email',)}), (_('Personal info'), {'fields':('name', 'section', 'remarks')}), (_('Permissions'), {'fields':('is_active', 'is_staff', 'is_superuser', 'groups','user_permissions')}), (_('Important dates'), {'fields':('last_login',)}), ] # ユーザー追加フォームで入力するフィールド add_fieldsets = ( (None, { 'classes': ('wide',), 'fields': ( 'email', 'name', 'section', 'remarks', ), },), ) list_display = ( 'email', 'name', 'section', 'is_staff', 'is_superuser', 'is_active', 'created_time', 'updated_time', ) list_filter = ('is_staff', 'is_superuser', 'groups') search_fields = ('email', 'name', 'section') ordering = ('is_active',) filter_horizontal = ( 'groups', 'user_permissions', ) admin.site.register(CustomUser, CustomUserAdmin)
login.html
メールアドレスを入力して、Googleマークのボタンをクリックすると、Googleの2段階認証が起動します。
{% extends 'base.html' %} {% load static %} {% block title %} Login {% endblock title %} {% block body %} <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-4"> <h2 class="text-center">Login</h2> <p>Only users who have been pre-registered by the administrator can use it.</p> <p>Please enter your email address to continue.</p> <form method="POST" action="{% url 'account_login' %}"> {% csrf_token %} <input type="hidden" name="next" value="{% url 'home' %}"> <div class="form-group mb-3"> <input type="email" class="form-control" id="email" name="email" placeholder="Enter your email address" required> </div> <button type="submit" class="btn btn-light w-100 d-flex align-items-center justify-content-center"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="24px" height="24px"> <path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/> <path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/> <path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/> <path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"/> </svg> <span class="ms-2">Sign in with Google</span> </button> </form> {% if messages %} <div class="mt-3"> {% for message in messages %} <div class="alert alert-{{ message.tags }}">{{ message }}</div> {% endfor %} </div> {% endif %} </div> </div> </div> {% endblock body %}
logout.html
{% extends 'base.html' %} {% load static %} {% block title %} Logout {% endblock title %} {% block h1_title %} Logout {% endblock h1_title %} {% block body %} <p>You are logged out.</p> <p><a href="{% url 'account_login' %}">Login</a></p> {% endblock body %}
base.html
login.html、logout.htmlの最初で読み込みます。
Bootstrapは、static/cssに置いています。
{% load i18n static %} <!doctype html> <html lang="ja" data-bs-theme="dark"> <head> <meta charset="utf-8"> <link rel="shortcut icon" type="image/png" href="{% static '' %}"> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> <title> {% block title %} {% endblock %} </title> </head> <body> {# container: 表示幅を可変にする。#} <div class="container-fluid"> {# formのエラー表示 #} {% if form.errors and request.method == 'POST' %} {% for field, errors in form.errors.items %} {% for error in errors %} <div class="alert alert-danger" role="alert"> <p class="mb-0">{{ field }} {{ error }}</p> </div> {% endfor %} {% endfor %} {% endif %} {# 「form.errors」以外のエラー表示(カスタムバリデーションなど) #} {% if non_field_errors %} <div class="alert alert-danger" role="alert"> {% for error in form.non_field_errors %} <p{% if forloop.last %} class="mb-0"{% endif %}>{{ error }}</p> {% endfor %} </div> {% endif %} {# SuccessMessageMixinのメッセージ表示 #} {% if messages %} <div class="text-center messages alert alert-primary" role="alert"> {% for message in messages %} {{ message }} {% endfor %} </div> {% endif %} {% block error_404 %} {% endblock %} {% block body %} {% endblock %} {# フッタ #} <footer class="container-fluid"> <small> ©ほげ </small> </footer> </div> </body> </html>
404.html
django-allauthでルーティングされているログイン、ログアウト以外は、このページを表示します。
{% extends 'base.html' %} {% block title %} ページが見つかりませんでした {% endblock title %} {% block h1_title %} It is just a 404 Error ! {% endblock h1_title %} {% block brain_404 %} <p class="text-center"> Sorry, the requested URL was not found on this server. <br> <a href="{% url 'account_login' %}" class="btn btn-outline-danger">Login</a> </p> {% endblock brain_404 %}
home.html
ログインが成功した場合に表示します。
各種メニューを列挙します。
{% extends "base.html" %} {% load static %} {% block title %} Menu {% endblock title %} {% block h1_title %} Menu {% endblock h1_title %} {% block body %} {% if user.is_authenticated %} <div class="row justify-content-center"> <p> <div class="col-auto"> <a class="btn btn-success" href="{% url 'ほげ' %}">ほげほげ</a> </div> </p> </div> <div class="row justify-content-center"> <p> <div class="col-auto"> <form method="post" action="{% url 'account_logout' %}"> {% csrf_token %} <button class="btn btn-outline-secondary" type="submit">Logout</button> </form> </div> </p> </div> {% else %} <p class="text-center"><a class="btn btn-outline-success" href="{% url 'account_login' %}">Login</a></p> {% endif %} {% endblock body %}
Comments