-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
173 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -127,3 +127,5 @@ dmypy.json | |
|
||
# Pyre type checker | ||
.pyre/ | ||
|
||
*.sqlite3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
https://docs.djangoproject.com/en/4.1/ref/settings/ | ||
""" | ||
|
||
from os import getenv | ||
from pathlib import Path | ||
|
||
# Build paths inside the project like this: BASE_DIR / 'subdir'. | ||
|
@@ -20,12 +21,18 @@ | |
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ | ||
|
||
# SECURITY WARNING: keep the secret key used in production secret! | ||
SECRET_KEY = 'django-insecure-yhe(15ptdmm(cbczejzf4x-h)f!=b$vyfw@68+t2y6i_ic!(w@' | ||
SECRET_KEY = getenv( | ||
'DJANGO_SECRET_KEY', | ||
'django-insecure-yhe(15ptdmm(cbczejzf4x-h)f!=b$vyfw@68+t2y6i_ic!(w@', | ||
) | ||
|
||
# SECURITY WARNING: don't run with debug turned on in production! | ||
DEBUG = True | ||
DEBUG = bool(int(getenv('DJANGO_DEBUG', 1))) | ||
|
||
ALLOWED_HOSTS = [] | ||
if getenv('DJANGO_ALLOWED_HOSTS'): | ||
ALLOWED_HOSTS = getenv('DJANGO_ALLOWED_HOSTS').split(',') | ||
else: | ||
ALLOWED_HOSTS = ['*'] | ||
|
||
|
||
# Application definition | ||
|
@@ -37,8 +44,49 @@ | |
'django.contrib.sessions', | ||
'django.contrib.messages', | ||
'django.contrib.staticfiles', | ||
'rest_framework', | ||
'rest_framework.authtoken', | ||
'drfpasswordless', | ||
] | ||
|
||
REST_FRAMEWORK = { | ||
'DEFAULT_AUTHENTICATION_CLASSES': [ | ||
'rest_framework.authentication.TokenAuthentication', | ||
], | ||
} | ||
|
||
PASSWORDLESS_AUTH = { | ||
'PASSWORDLESS_AUTH_TYPES': ['EMAIL'], | ||
'PASSWORDLESS_EMAIL_NOREPLY_ADDRESS': getenv('OTP_EMAIL_ADDRESS', '[email protected]'), | ||
'PASSWORDLESS_TOKEN_EXPIRE_TIME': int(getenv('OTP_TOKEN_EXPIRE_SECONDS', 5 * 60)), | ||
} | ||
if getenv('OTP_EMAIL_SUBJECT'): | ||
PASSWORDLESS_AUTH['PASSWORDLESS_EMAIL_SUBJECT'] = getenv('OTP_EMAIL_SUBJECT') | ||
if getenv('OTP_EMAIL_PLAINTEXT'): | ||
PASSWORDLESS_AUTH['PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE'] = getenv('OTP_EMAIL_PLAINTEXT') | ||
if getenv('OTP_EMAIL_HTML'): | ||
PASSWORDLESS_AUTH['PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME'] = getenv('OTP_EMAIL_HTML') | ||
|
||
if getenv('EMAIL_BACKEND_TEST'): | ||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' | ||
else: | ||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' | ||
EMAIL_USE_SSL = bool(int(getenv('EMAIL_USE_SSL', 0))) | ||
EMAIL_USE_TLS = bool(int(getenv('EMAIL_USE_TLS', 0))) | ||
EMAIL_HOST = getenv('EMAIL_HOST', 'smtp.mydomain.com') | ||
EMAIL_HOST_USER = getenv('EMAIL_HOST_USER', '[email protected]') | ||
EMAIL_HOST_PASSWORD = getenv('EMAIL_HOST_PASSWORD', 'password') | ||
EMAIL_PORT = int(getenv('EMAIL_PORT', 465)) | ||
EMAIL_FROM = getenv('EMAIL_FROM', '[email protected]') | ||
EMAIL_TIMEOUT = int(getenv('EMAIL_TIMEOUT', 3)) | ||
EMAIL_WHITE_LIST = getenv('EMAIL_WHITE_LIST', r'.*') | ||
EMAIL_WHITE_LIST_MESSAGE = getenv('EMAIL_WHITE_LIST_MESSAGE', | ||
'email address not in white list') | ||
|
||
JWT_SECRET = getenv('JWT_SECRET', 'your secret key') | ||
JWT_EXPIRE_SECONDS = int(getenv('JWT_EXPIRE_SECONDS', 60 * 60 * 24 * 30)) | ||
|
||
|
||
MIDDLEWARE = [ | ||
'django.middleware.security.SecurityMiddleware', | ||
'django.contrib.sessions.middleware.SessionMiddleware', | ||
|
@@ -54,7 +102,7 @@ | |
TEMPLATES = [ | ||
{ | ||
'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||
'DIRS': [], | ||
'DIRS': [BASE_DIR / 'templates'], | ||
'APP_DIRS': True, | ||
'OPTIONS': { | ||
'context_processors': [ | ||
|
@@ -75,10 +123,21 @@ | |
|
||
DATABASES = { | ||
'default': { | ||
'ENGINE': 'django.db.backends.sqlite3', | ||
'NAME': BASE_DIR / 'db.sqlite3', | ||
'ENGINE': getenv('DB_ENGINE', 'django.db.backends.sqlite3'), | ||
'NAME': getenv('DB_NAME', BASE_DIR / 'db.sqlite3'), | ||
'USER': getenv('DB_USER', 'postgres'), | ||
'PASSWORD': getenv('DB_PASSWORD', ''), | ||
'HOST': getenv('DB_HOST', ''), | ||
'PORT': getenv('DB_PORT', ''), | ||
} | ||
} | ||
if 'mysql' in DATABASES['default']['ENGINE']: | ||
DATABASES['default']['OPTIONS'] = { | ||
# fix mysql error 1452 | ||
"init_command": "SET foreign_key_checks = 0;", | ||
# fix mysql emoji issue | ||
'charset': 'utf8mb4', | ||
} | ||
|
||
|
||
# Password validation | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
from datetime import datetime, timedelta, timezone | ||
|
||
import jwt | ||
from django.conf import settings | ||
|
||
|
||
def generate_jwt(email): | ||
payload = {"email": email} | ||
exp = timedelta(seconds=settings.JWT_EXPIRE_SECONDS) | ||
payload['exp'] = datetime.now(tz=timezone.utc) + exp | ||
token = jwt.encode(payload, settings.JWT_SECRET, algorithm="HS256") | ||
return token | ||
|
||
|
||
def decode_jwt(token): | ||
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=["HS256"]) | ||
ts = payload['exp'] | ||
payload['exp'] = datetime.fromtimestamp(ts, timezone.utc) | ||
return payload |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import jwt | ||
from drfpasswordless.views import ObtainAuthTokenFromCallbackToken | ||
from rest_framework import serializers, status | ||
from rest_framework.permissions import AllowAny | ||
from rest_framework.response import Response | ||
from rest_framework.views import APIView | ||
|
||
from .utils import decode_jwt, generate_jwt | ||
|
||
from drfpasswordless.views import ObtainEmailCallbackToken | ||
from drfpasswordless.serializers import EmailAuthSerializer | ||
from django.conf import settings | ||
from django.core.validators import RegexValidator | ||
|
||
|
||
class EmailAuthWhiteListSerializer(EmailAuthSerializer): | ||
email_regex = RegexValidator( | ||
regex=settings.EMAIL_WHITE_LIST, | ||
message=settings.EMAIL_WHITE_LIST_MESSAGE, | ||
) | ||
email = serializers.EmailField(validators=[email_regex]) | ||
|
||
|
||
class ObtainEmailWhiteListCallbackToken(ObtainEmailCallbackToken): | ||
serializer_class = EmailAuthWhiteListSerializer | ||
|
||
|
||
class ObtainJWTFromCallbackToken(ObtainAuthTokenFromCallbackToken): | ||
def post(self, request, *args, **kwargs): | ||
email = request.data['email'] | ||
resp = super(ObtainJWTFromCallbackToken, self).post(request, *args, | ||
**kwargs) | ||
token = generate_jwt(email) | ||
resp.data['email'] = email | ||
resp.data['token'] = token | ||
return resp | ||
|
||
|
||
class JWTSerializer(serializers.Serializer): | ||
token = serializers.CharField() | ||
|
||
def validate_token(self, value): | ||
try: | ||
value = decode_jwt(value) | ||
except jwt.ExpiredSignatureError: | ||
raise serializers.ValidationError('token expired') | ||
return value | ||
|
||
|
||
class VerifyJWT(APIView): | ||
permission_classes = [AllowAny] | ||
serializer_class = JWTSerializer | ||
|
||
def post(self, request, *args, **kwargs): | ||
serializer = self.serializer_class(data=request.data, | ||
context={'request': request}) | ||
if serializer.is_valid(raise_exception=False): | ||
return Response( | ||
serializer.validated_data['token'], | ||
status=status.HTTP_200_OK, | ||
) | ||
|
||
return Response(status=status.HTTP_401_UNAUTHORIZED) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Django | ||
djangorestframework | ||
drfpasswordless-gstr169 | ||
PyJWT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<title>你的登录验证码</title> | ||
</head> | ||
<body> | ||
<h2>你的登录验证码是 {{ callback_token }}。本条验证码有效期5分钟。</h2> | ||
</body> | ||
</html> |