Release the project
This commit is contained in:
commit
e20b055642
16 changed files with 1350 additions and 0 deletions
163
.gitignore
vendored
Normal file
163
.gitignore
vendored
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
config.json
|
||||||
|
*.db
|
16
.pre-commit-config.yaml
Normal file
16
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.5.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 23.10.1
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.1.4
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [--fix, --exit-non-zero-on-fix]
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 BobMaster
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
19
README.md
Normal file
19
README.md
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# wangfu-register
|
||||||
|
Edit `config.json` and templates/*.html(turnstile site keys)
|
||||||
|
|
||||||
|
## development
|
||||||
|
Create virtual environment and install dependencies
|
||||||
|
```
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
Debug the code
|
||||||
|
```
|
||||||
|
uvicorn src.main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## deployment
|
||||||
|
```
|
||||||
|
uvicorn src.main:app --host "127.0.0.1" --port 10010 --proxy-headers --forwarded-allow-ips "*"
|
||||||
|
```
|
2
assets/jquery-3.7.1.min.js
vendored
Normal file
2
assets/jquery-3.7.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
assets/logo-removebg.png
Normal file
BIN
assets/logo-removebg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 112 KiB |
1
assets/mdui.css
Normal file
1
assets/mdui.css
Normal file
File diff suppressed because one or more lines are too long
22
assets/mdui.global.js
Normal file
22
assets/mdui.global.js
Normal file
File diff suppressed because one or more lines are too long
8
config.json.example
Normal file
8
config.json.example
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"turnstile_secret_key": "1x0000000000000000000000000000000AA",
|
||||||
|
"keycloak_server_url": "xxxxxxxxx",
|
||||||
|
"keycloak_admin_username": "xxxxxxx",
|
||||||
|
"keycloak_admin_password": "xxxxxxxx",
|
||||||
|
"keycloak_realm_name": "master",
|
||||||
|
"open_registration": true
|
||||||
|
}
|
39
nginx.conf
Normal file
39
nginx.conf
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name "registry.csuwf.com";
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
http2 on;
|
||||||
|
http3 on;
|
||||||
|
http3_hq on;
|
||||||
|
add_header Alt-Svc 'h3=":443"; ma=86400';
|
||||||
|
server_name "registry.csuwf.com";
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/ssl/registry.csuwf.com.cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/registry.csuwf.com.key.pem;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_conf_command Ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
|
||||||
|
ssl_ecdh_curve X25519:secp384r1;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_stapling on;
|
||||||
|
ssl_stapling_verify on;
|
||||||
|
ssl_trusted_certificate /etc/nginx/ssl/registry.csuwf.com.cert.pem;
|
||||||
|
resolver 8.8.8.8 1.1.1.1;
|
||||||
|
# ssl related config
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:10010;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
6
requirements.txt
Normal file
6
requirements.txt
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
python-multipart
|
||||||
|
jinja2
|
||||||
|
email-validator
|
||||||
|
python-keycloak
|
181
src/db.py
Normal file
181
src/db.py
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import random
|
||||||
|
from .log import getlogger
|
||||||
|
|
||||||
|
logger = getlogger()
|
||||||
|
|
||||||
|
|
||||||
|
class DBManager:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
try:
|
||||||
|
self.conn = sqlite3.connect("user.db")
|
||||||
|
self.cursor = self.conn.cursor()
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
seed TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
db_version = self.get_db_version()
|
||||||
|
if db_version == 0:
|
||||||
|
self.migrate_0to1()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def add_user(self, username, email):
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO users (username, email, seed)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(username, email, hex(random.getrandbits(64))[2:]),
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
def get_user_by_name(self, username):
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM users WHERE username = ?
|
||||||
|
""",
|
||||||
|
(username,),
|
||||||
|
)
|
||||||
|
return self.cursor.fetchone()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
def get_user_by_email(self, email) -> str:
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT username FROM users WHERE email = ?
|
||||||
|
""",
|
||||||
|
(email,),
|
||||||
|
)
|
||||||
|
return self.cursor.fetchone()[0]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
# if db version not exist, initialize and create meta table and set db_version to 0
|
||||||
|
def get_db_version(self):
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT db_version FROM meta
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return self.cursor.fetchone()[0]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
|
db_version INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
logger.info("meta table created")
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO meta (db_version) VALUES (0)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
logger.info("db_version set to 0")
|
||||||
|
self.conn.commit()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_user_seed_by_username(self, username) -> str:
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT seed FROM users WHERE username = ?
|
||||||
|
""",
|
||||||
|
(username,),
|
||||||
|
)
|
||||||
|
return self.cursor.fetchone()[0]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
def get_user_seed_by_email(self, email) -> str:
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT seed FROM users WHERE email = ?
|
||||||
|
""",
|
||||||
|
(email,),
|
||||||
|
)
|
||||||
|
return self.cursor.fetchone()[0]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
def update_user_seed_by_username(self, username):
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users SET seed = ? WHERE username = ?
|
||||||
|
""",
|
||||||
|
(hex(random.getrandbits(64))[2:], username),
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
def update_user_seed_by_email(self, email):
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users SET seed = ? WHERE email = ?
|
||||||
|
""",
|
||||||
|
(hex(random.getrandbits(64))[2:], email),
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
# db migration from 0 to 1
|
||||||
|
# add column seed and set random seed to existing users
|
||||||
|
# seed: 16 hex numbers
|
||||||
|
def migrate_0to1(self):
|
||||||
|
try:
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
ALTER TABLE users ADD COLUMN seed TEXT
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
logger.info("seed column added")
|
||||||
|
# iterate over all users and set random seed
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM users
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
users = self.cursor.fetchall()
|
||||||
|
for user in users:
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users SET seed = ? WHERE id = ?
|
||||||
|
""",
|
||||||
|
(hex(random.getrandbits(64))[2:], user[0]),
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
logger.info("seed set for all users")
|
||||||
|
# update db_version to 1
|
||||||
|
self.cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE meta SET db_version = 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
sys.exit(1)
|
40
src/log.py
Normal file
40
src/log.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
log_path = Path(os.path.dirname(__file__)).parent / "bot.log"
|
||||||
|
|
||||||
|
|
||||||
|
def getlogger():
|
||||||
|
# create a custom logger if no log handler
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
if not logger.hasHandlers():
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
# create handlers
|
||||||
|
warn_handler = logging.StreamHandler()
|
||||||
|
info_handler = logging.StreamHandler()
|
||||||
|
error_handler = logging.FileHandler("bot.log", mode="a")
|
||||||
|
warn_handler.setLevel(logging.WARNING)
|
||||||
|
error_handler.setLevel(logging.ERROR)
|
||||||
|
info_handler.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# create formatters
|
||||||
|
warn_format = logging.Formatter(
|
||||||
|
"%(asctime)s - %(funcName)s - %(levelname)s - %(message)s",
|
||||||
|
)
|
||||||
|
error_format = logging.Formatter(
|
||||||
|
"%(asctime)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s",
|
||||||
|
)
|
||||||
|
info_format = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||||
|
|
||||||
|
# set formatter
|
||||||
|
warn_handler.setFormatter(warn_format)
|
||||||
|
error_handler.setFormatter(error_format)
|
||||||
|
info_handler.setFormatter(info_format)
|
||||||
|
|
||||||
|
# add handlers to logger
|
||||||
|
logger.addHandler(warn_handler)
|
||||||
|
logger.addHandler(error_handler)
|
||||||
|
logger.addHandler(info_handler)
|
||||||
|
|
||||||
|
return logger
|
307
src/main.py
Normal file
307
src/main.py
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
import json
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.responses import HTMLResponse, PlainTextResponse, JSONResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from email_validator import validate_email, EmailNotValidError
|
||||||
|
from keycloak import KeycloakAdmin, KeycloakGetError
|
||||||
|
from keycloak import KeycloakOpenIDConnection
|
||||||
|
from .log import getlogger
|
||||||
|
from .db import DBManager
|
||||||
|
import subprocess
|
||||||
|
import requests
|
||||||
|
from shlex import quote as shellquote
|
||||||
|
|
||||||
|
reserved_users = [
|
||||||
|
"admin",
|
||||||
|
"administrator",
|
||||||
|
"root",
|
||||||
|
"postmaster",
|
||||||
|
"webmaster",
|
||||||
|
"wf",
|
||||||
|
"csu",
|
||||||
|
"csuwf",
|
||||||
|
]
|
||||||
|
|
||||||
|
reserved_emails = [
|
||||||
|
"admin@csuwf.com",
|
||||||
|
"administrator@csuwf.com",
|
||||||
|
"root@csuwf.com",
|
||||||
|
"postmaster@csuwf.com",
|
||||||
|
"webmaster@csuwf.com",
|
||||||
|
"wf@csuwf.com",
|
||||||
|
"csu@csuwf.com",
|
||||||
|
"csuwf@csuwf.com",
|
||||||
|
"rocketchat-bot@csuwf.com",
|
||||||
|
"nextcloud-bot@csuwf.com",
|
||||||
|
"dmarc.report@csuwf.com",
|
||||||
|
"keycloak@csuwf.com",
|
||||||
|
]
|
||||||
|
|
||||||
|
logger = getlogger()
|
||||||
|
dbmanager = DBManager()
|
||||||
|
|
||||||
|
|
||||||
|
class Register_Request_Body(BaseModel):
|
||||||
|
username: str
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class Password_Reset_Request_Body(BaseModel):
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
authcode: str
|
||||||
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class checkemailexistRequest(BaseModel):
|
||||||
|
email: str
|
||||||
|
|
||||||
|
|
||||||
|
class checkusernameexistRequest(BaseModel):
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(openapi_url=None)
|
||||||
|
app.mount(
|
||||||
|
"/assets",
|
||||||
|
StaticFiles(directory="assets"),
|
||||||
|
name="assets",
|
||||||
|
)
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
with open("config.json", "r") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
turnstile_secret_key = config["turnstile_secret_key"]
|
||||||
|
keycloak_server_url = config["keycloak_server_url"]
|
||||||
|
keycloak_admin_username = config["keycloak_admin_username"]
|
||||||
|
keycloak_admin_password = config["keycloak_admin_password"]
|
||||||
|
keycloak_realm_name = config["keycloak_realm_name"]
|
||||||
|
open_registration = config["open_registration"]
|
||||||
|
|
||||||
|
keycloak_connection = KeycloakOpenIDConnection(
|
||||||
|
server_url=keycloak_server_url,
|
||||||
|
username=keycloak_admin_username,
|
||||||
|
password=keycloak_admin_password,
|
||||||
|
realm_name=keycloak_realm_name,
|
||||||
|
verify=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
keycloak_admin = KeycloakAdmin(connection=keycloak_connection)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
async def root(request: Request):
|
||||||
|
return templates.TemplateResponse("index.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/update-password", response_class=HTMLResponse)
|
||||||
|
async def update_password(request: Request):
|
||||||
|
return templates.TemplateResponse("password-update.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/checkemailexist")
|
||||||
|
async def checkemailexist(request: Request, request_body: checkemailexistRequest):
|
||||||
|
request_body = request_body.model_dump()
|
||||||
|
if request_body["email"] in reserved_emails:
|
||||||
|
return JSONResponse(content={"exist": True}, status_code=200)
|
||||||
|
if dbmanager.get_user_by_email(request_body["email"]):
|
||||||
|
return JSONResponse(content={"exist": True}, status_code=200)
|
||||||
|
else:
|
||||||
|
return JSONResponse(content={"exist": False}, status_code=200)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/checkusernameexist")
|
||||||
|
async def checkusernameexist(request: Request, request_body: checkusernameexistRequest):
|
||||||
|
request_body = request_body.model_dump()
|
||||||
|
if request_body["username"] in reserved_users:
|
||||||
|
return JSONResponse(content={"exist": True}, status_code=200)
|
||||||
|
if dbmanager.get_user_by_name(request_body["username"]):
|
||||||
|
return JSONResponse(content={"exist": True}, status_code=200)
|
||||||
|
else:
|
||||||
|
return JSONResponse(content={"exist": False}, status_code=200)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/register", response_class=PlainTextResponse)
|
||||||
|
async def register(request: Request, request_body: Register_Request_Body):
|
||||||
|
try:
|
||||||
|
# check if open register
|
||||||
|
if not open_registration:
|
||||||
|
return "注册功能已关闭,请联系管理员。"
|
||||||
|
|
||||||
|
request_body = request_body.model_dump()
|
||||||
|
|
||||||
|
# verify captcha
|
||||||
|
captcha_verify_response = requests.post(
|
||||||
|
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||||
|
data={
|
||||||
|
"secret": turnstile_secret_key,
|
||||||
|
"response": request_body["token"],
|
||||||
|
"remoteip": request.client.host,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
captcha_verify_response_json = captcha_verify_response.json()
|
||||||
|
if not captcha_verify_response_json["success"]:
|
||||||
|
return "验证码校验出错,请刷新网页重试,如果问题依旧,请联系管理员。"
|
||||||
|
|
||||||
|
# validate email
|
||||||
|
try:
|
||||||
|
email = request_body["email"]
|
||||||
|
emailinfo = validate_email(email, check_deliverability=False)
|
||||||
|
email = emailinfo.normalized
|
||||||
|
except EmailNotValidError as e:
|
||||||
|
return "邮件地址出错: " + str(e)
|
||||||
|
|
||||||
|
# check if email is reserved
|
||||||
|
if email in reserved_emails:
|
||||||
|
return "该邮件地址为保留地址,请更换。"
|
||||||
|
|
||||||
|
# check if email exists
|
||||||
|
if dbmanager.get_user_by_email(email):
|
||||||
|
return "邮件地址已被注册。"
|
||||||
|
|
||||||
|
# check if username is reserved
|
||||||
|
if request_body["username"] in reserved_users:
|
||||||
|
return "该用户名为保留用户名,请更换。"
|
||||||
|
|
||||||
|
# check if username exists
|
||||||
|
if dbmanager.get_user_by_name(request_body["username"]):
|
||||||
|
return "用户名已被注册。"
|
||||||
|
|
||||||
|
# create user in keycloak
|
||||||
|
try:
|
||||||
|
keycloak_admin.create_user(
|
||||||
|
{
|
||||||
|
"email": email,
|
||||||
|
"username": request_body["username"],
|
||||||
|
"emailVerified": True,
|
||||||
|
"enabled": True,
|
||||||
|
"credentials": [
|
||||||
|
{
|
||||||
|
"type": "password",
|
||||||
|
"value": request_body["password"],
|
||||||
|
"temporary": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attributes": {
|
||||||
|
"quota": "10G",
|
||||||
|
},
|
||||||
|
"groups": [
|
||||||
|
"网服队员",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except KeycloakGetError:
|
||||||
|
return "用户名已被注册,请更改。"
|
||||||
|
|
||||||
|
# use subprocess create user in docker-mailserver
|
||||||
|
try:
|
||||||
|
# docker exec -it dms setup email add <EMAIL ADDRESS> [<PASSWORD>]
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"exec",
|
||||||
|
"-it",
|
||||||
|
"dms",
|
||||||
|
"setup",
|
||||||
|
"email",
|
||||||
|
"add",
|
||||||
|
email,
|
||||||
|
shellquote(request_body["password"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return "出错了,请刷新网页重试,如果问题依旧,请联系管理员。"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return "出错了,请刷新网页重试,如果问题依旧,请联系管理员。"
|
||||||
|
|
||||||
|
dbmanager.add_user(request_body["username"], email)
|
||||||
|
return "注册成功。"
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/passwordreset", response_class=PlainTextResponse)
|
||||||
|
async def passwordreset(request: Request, request_body: Password_Reset_Request_Body):
|
||||||
|
try:
|
||||||
|
request_body = request_body.model_dump()
|
||||||
|
|
||||||
|
# verify captcha
|
||||||
|
captcha_verify_response = requests.post(
|
||||||
|
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||||
|
data={
|
||||||
|
"secret": turnstile_secret_key,
|
||||||
|
"response": request_body["token"],
|
||||||
|
"remoteip": request.client.host,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
captcha_verify_response_json = captcha_verify_response.json()
|
||||||
|
if not captcha_verify_response_json["success"]:
|
||||||
|
return "验证码校验出错,请刷新网页重试,如果问题依旧,请联系管理员。"
|
||||||
|
|
||||||
|
# validate email
|
||||||
|
try:
|
||||||
|
email = request_body["email"]
|
||||||
|
emailinfo = validate_email(email, check_deliverability=False)
|
||||||
|
email = emailinfo.normalized
|
||||||
|
except EmailNotValidError as e:
|
||||||
|
return "邮件地址出错: " + str(e)
|
||||||
|
|
||||||
|
# check if email exists
|
||||||
|
if not dbmanager.get_user_by_email(email):
|
||||||
|
return "邮件地址不存在。"
|
||||||
|
|
||||||
|
# validate authcode
|
||||||
|
authcode = request_body["authcode"]
|
||||||
|
real_authcode = dbmanager.get_user_seed_by_email(email)
|
||||||
|
if authcode != real_authcode:
|
||||||
|
return "校验码错误,请仔细核对或联系管理员"
|
||||||
|
|
||||||
|
# update user in keycloak
|
||||||
|
try:
|
||||||
|
# get username by email
|
||||||
|
username = dbmanager.get_user_by_email(email)
|
||||||
|
# get user_id by username
|
||||||
|
user_id = keycloak_admin.get_user_id(username)
|
||||||
|
keycloak_admin.set_user_password(
|
||||||
|
user_id=user_id,
|
||||||
|
password=request_body["password"],
|
||||||
|
temporary=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
except KeycloakGetError as e:
|
||||||
|
logger.error(e)
|
||||||
|
return "Keycloak密码更新失败,请联系管理员"
|
||||||
|
|
||||||
|
# use subprocess to update user password in docker-mailserver
|
||||||
|
try:
|
||||||
|
# docker exec -it dms setup email add <EMAIL ADDRESS> [<PASSWORD>]
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"exec",
|
||||||
|
"-it",
|
||||||
|
"dms",
|
||||||
|
"setup",
|
||||||
|
"email",
|
||||||
|
"update",
|
||||||
|
email,
|
||||||
|
shellquote(request_body["password"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return "出错了,请刷新网页重试"
|
||||||
|
|
||||||
|
# generate new authcode
|
||||||
|
dbmanager.update_user_seed_by_email(email)
|
||||||
|
|
||||||
|
return "密码修改成功"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return "出错了,请刷新网页重试"
|
262
templates/index.html
Normal file
262
templates/index.html
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no"
|
||||||
|
/>
|
||||||
|
<meta name="renderer" content="webkit" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
href="{{ url_for('assets', path='logo-removebg.png') }}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ url_for('assets', path='/mdui.css') }}" />
|
||||||
|
<script src="{{ url_for('assets', path='/mdui.global.js') }}"></script>
|
||||||
|
<script src="{{ url_for('assets', path='/jquery-3.7.1.min.js') }}"></script>
|
||||||
|
<script
|
||||||
|
src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=_turnstileCb"
|
||||||
|
async
|
||||||
|
defer
|
||||||
|
></script>
|
||||||
|
<style>
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.center {
|
||||||
|
margin: auto;
|
||||||
|
width: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 480px) {
|
||||||
|
.center {
|
||||||
|
margin: auto;
|
||||||
|
width: 480px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<title>网服注册服务</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="mdui-prose center">
|
||||||
|
<mdui-avatar src="../assets/logo-removebg.png"></mdui-avatar>
|
||||||
|
<h1 style="margin-bottom: 10px; margin-top: 5px">网服注册服务</h1>
|
||||||
|
<mdui-text-field
|
||||||
|
id="username"
|
||||||
|
label="用户名"
|
||||||
|
autofocus="true"
|
||||||
|
style="margin-bottom: 10px"
|
||||||
|
>
|
||||||
|
<span slot="helper" style="color: red" id="username-helper"></span>
|
||||||
|
</mdui-text-field>
|
||||||
|
<mdui-text-field
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
label="邮箱"
|
||||||
|
placeholder="xxxx@csuwf.com"
|
||||||
|
>
|
||||||
|
<span slot="helper" style="color: red" id="email-helper"></span>
|
||||||
|
</mdui-text-field>
|
||||||
|
<mdui-text-field
|
||||||
|
id="password"
|
||||||
|
label="密码"
|
||||||
|
toggle-password
|
||||||
|
type="password"
|
||||||
|
>
|
||||||
|
<span slot="helper" style="color: red" id="password-helper"></span>
|
||||||
|
</mdui-text-field>
|
||||||
|
|
||||||
|
<span
|
||||||
|
><a href="./update-password" style="float: right; margin-top: 10px"
|
||||||
|
>重置密码</a
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
|
||||||
|
<div id="turnstile_widgit"></div>
|
||||||
|
<mdui-button
|
||||||
|
id="btn1"
|
||||||
|
variant="tonal"
|
||||||
|
style="border-radius: 5%"
|
||||||
|
onclick="register()"
|
||||||
|
>注册</mdui-button
|
||||||
|
>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let token = "";
|
||||||
|
function _turnstileCb() {
|
||||||
|
console.log("_turnstileCb called");
|
||||||
|
turnstile.render("#turnstile_widgit", {
|
||||||
|
sitekey: "1x00000000000000000000AA",
|
||||||
|
theme: "light",
|
||||||
|
callback: function (t) {
|
||||||
|
token = t;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajaxSetup({
|
||||||
|
global: false,
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
const registerBtn = document.getElementById("btn1");
|
||||||
|
|
||||||
|
const usernameElement = document.getElementById("username");
|
||||||
|
usernameElement.addEventListener("change", () => {
|
||||||
|
const usernameHelper = document.getElementById("username-helper");
|
||||||
|
const username = usernameElement.value;
|
||||||
|
if (username !== "") {
|
||||||
|
const promise = $.ajax({
|
||||||
|
url: "/checkusernameexist",
|
||||||
|
method: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
data: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
}),
|
||||||
|
success: function (response) {
|
||||||
|
if (response.exist) {
|
||||||
|
usernameHelper.innerText = "用户名已被注册";
|
||||||
|
} else {
|
||||||
|
usernameHelper.innerText = "该用户名可用";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
usernameHelper.innerText = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emailElement = document.getElementById("email");
|
||||||
|
const emailRegex = /@csuwf\.com$/;
|
||||||
|
|
||||||
|
emailElement.addEventListener("change", () => {
|
||||||
|
const emailHelper = document.getElementById("email-helper");
|
||||||
|
const email = emailElement.value;
|
||||||
|
if (email === "") {
|
||||||
|
emailHelper.innerText = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (email !== "" && !emailRegex.test(email)) {
|
||||||
|
emailHelper.innerText = "邮箱必须以@csuwf.com结尾";
|
||||||
|
} else {
|
||||||
|
// emailHelper.innerText = "";
|
||||||
|
const promise = $.ajax({
|
||||||
|
url: "/checkemailexist",
|
||||||
|
method: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
data: JSON.stringify({
|
||||||
|
email: email,
|
||||||
|
}),
|
||||||
|
success: function (response) {
|
||||||
|
if (response.exist) {
|
||||||
|
emailHelper.innerText = "邮箱已被注册";
|
||||||
|
} else {
|
||||||
|
emailHelper.innerText = "该邮箱可用";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordElement = document.getElementById("password");
|
||||||
|
const passwordRegex = /[&\\;]/;
|
||||||
|
passwordElement.addEventListener("change", () => {
|
||||||
|
const password = passwordElement.value;
|
||||||
|
const passwordHelper = document.getElementById("password-helper");
|
||||||
|
if (password === "") {
|
||||||
|
passwordHelper.innerText = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password !== "" && passwordRegex.test(password)) {
|
||||||
|
passwordHelper.innerText = "密码不能包含(&)或分号(;)或反斜杠(\\)";
|
||||||
|
} else {
|
||||||
|
passwordHelper.innerText = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册逻辑
|
||||||
|
async function register() {
|
||||||
|
if (usernameElement.value === "") {
|
||||||
|
mdui.snackbar({
|
||||||
|
message: "用户名不能为空",
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (emailElement.value === "") {
|
||||||
|
mdui.snackbar({
|
||||||
|
message: "邮箱不能为空",
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (passwordElement.value === "") {
|
||||||
|
mdui.snackbar({
|
||||||
|
message: "密码不能为空",
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (emailElement.value !== "" && !emailRegex.test(emailElement.value)) {
|
||||||
|
mdui.snackbar({
|
||||||
|
message: "邮箱必须以@csuwf.com结尾",
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// if (token === "") tell user to complete the captcha
|
||||||
|
if (token === "") {
|
||||||
|
mdui.snackbar({
|
||||||
|
message: "请完成人机验证",
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registerBtn.setAttribute("loading", "");
|
||||||
|
registerBtn.setAttribute("disabled", "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: usernameElement.value,
|
||||||
|
email: emailElement.value,
|
||||||
|
password: passwordElement.value,
|
||||||
|
token: token,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("HTTP error " + response.status);
|
||||||
|
}
|
||||||
|
const result = await response.text();
|
||||||
|
mdui.alert({
|
||||||
|
headline: "注册结果",
|
||||||
|
description: result,
|
||||||
|
confirmText: "了解",
|
||||||
|
onConfirm: () => {
|
||||||
|
registerBtn.removeAttribute("loading");
|
||||||
|
registerBtn.removeAttribute("disabled");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
mdui.alert({
|
||||||
|
headline: "注册结果",
|
||||||
|
description: "出错了,请尝试刷新或联系管理员",
|
||||||
|
confirmText: "了解",
|
||||||
|
onConfirm: () => {
|
||||||
|
registerBtn.removeAttribute("loading");
|
||||||
|
registerBtn.removeAttribute("disabled");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
263
templates/password-update.html
Normal file
263
templates/password-update.html
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no"
|
||||||
|
/>
|
||||||
|
<meta name="renderer" content="webkit" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
href="{{ url_for('assets', path='logo-removebg.png') }}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ url_for('assets', path='/mdui.css') }}" />
|
||||||
|
<script src="{{ url_for('assets', path='/mdui.global.js') }}"></script>
|
||||||
|
<script src="{{ url_for('assets', path='/jquery-3.7.1.min.js') }}"></script>
|
||||||
|
<script
|
||||||
|
src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=_turnstileCb"
|
||||||
|
async
|
||||||
|
defer
|
||||||
|
></script>
|
||||||
|
<style>
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.center {
|
||||||
|
margin: auto;
|
||||||
|
width: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 480px) {
|
||||||
|
.center {
|
||||||
|
margin: auto;
|
||||||
|
width: 480px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<title>网服密码重置服务</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="mdui-prose center">
|
||||||
|
<mdui-avatar src="../assets/logo-removebg.png"></mdui-avatar>
|
||||||
|
<h1 style="margin-bottom: 10px; margin-top: 5px">网服密码重置服务</h1>
|
||||||
|
|
||||||
|
<mdui-text-field
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
label="邮箱"
|
||||||
|
autofocus="true"
|
||||||
|
placeholder="xxxx@csuwf.com"
|
||||||
|
>
|
||||||
|
<span slot="helper" style="color: red" id="email-helper"></span>
|
||||||
|
</mdui-text-field>
|
||||||
|
|
||||||
|
<mdui-text-field
|
||||||
|
id="password"
|
||||||
|
label="新密码"
|
||||||
|
toggle-password
|
||||||
|
type="password"
|
||||||
|
>
|
||||||
|
<span slot="helper" style="color: red" id="password-helper"></span>
|
||||||
|
</mdui-text-field>
|
||||||
|
|
||||||
|
<mdui-text-field
|
||||||
|
id="password2"
|
||||||
|
label="确认新密码"
|
||||||
|
toggle-password
|
||||||
|
type="password"
|
||||||
|
></mdui-text-field>
|
||||||
|
|
||||||
|
<mdui-text-field
|
||||||
|
id="authcode"
|
||||||
|
label="校验码"
|
||||||
|
></mdui-text-field>
|
||||||
|
|
||||||
|
<div id="turnstile_widgit"></div>
|
||||||
|
<mdui-button
|
||||||
|
id="btn1"
|
||||||
|
variant="tonal"
|
||||||
|
style="border-radius: 5%"
|
||||||
|
onclick="passwordReset()"
|
||||||
|
>修改密码</mdui-button
|
||||||
|
>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let token = "";
|
||||||
|
function _turnstileCb() {
|
||||||
|
console.log("_turnstileCb called");
|
||||||
|
turnstile.render("#turnstile_widgit", {
|
||||||
|
sitekey: "1x00000000000000000000AA",
|
||||||
|
theme: "light",
|
||||||
|
callback: function (t) {
|
||||||
|
token = t;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajaxSetup({
|
||||||
|
global: false,
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetBtn = document.getElementById("btn1");
|
||||||
|
|
||||||
|
const authcodeElement = document.getElementById("authcode");
|
||||||
|
|
||||||
|
const emailElement = document.getElementById("email");
|
||||||
|
const emailRegex = /@csuwf\.com$/;
|
||||||
|
|
||||||
|
emailElement.addEventListener("change", () => {
|
||||||
|
const emailHelper = document.getElementById("email-helper");
|
||||||
|
const email = emailElement.value;
|
||||||
|
if (email === "") {
|
||||||
|
emailHelper.innerText = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (email !== "" && !emailRegex.test(email)) {
|
||||||
|
emailHelper.innerText = "网服的邮箱是以@csuwf.com结尾的哦!";
|
||||||
|
} else {
|
||||||
|
// emailHelper.innerText = "";
|
||||||
|
const promise = $.ajax({
|
||||||
|
url: "/checkemailexist",
|
||||||
|
method: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
data: JSON.stringify({
|
||||||
|
email: email,
|
||||||
|
}),
|
||||||
|
success: function (response) {
|
||||||
|
if (response.exist) {
|
||||||
|
emailHelper.innerText = "";
|
||||||
|
} else {
|
||||||
|
emailHelper.innerText = "该邮箱不存在";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordElement = document.getElementById("password");
|
||||||
|
const passwordRegex = /[&\\;]/;
|
||||||
|
passwordElement.addEventListener("change", () => {
|
||||||
|
const password = passwordElement.value;
|
||||||
|
const passwordHelper = document.getElementById("password-helper");
|
||||||
|
if (password === "") {
|
||||||
|
passwordHelper.innerText = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password !== "" && passwordRegex.test(password)) {
|
||||||
|
passwordHelper.innerText = "密码不能包含(&)或分号(;)或反斜杠(\\)";
|
||||||
|
} else {
|
||||||
|
passwordHelper.innerText = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const password2Element = document.getElementById("password2");
|
||||||
|
password2Element.addEventListener("change", () => {
|
||||||
|
const password = passwordElement.value;
|
||||||
|
const password2 = password2Element.value;
|
||||||
|
const passwordHelper = document.getElementById("password-helper");
|
||||||
|
if (password2 === "") {
|
||||||
|
passwordHelper.innerText = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password !== password2) {
|
||||||
|
passwordHelper.innerText = "两次输入的密码不一致";
|
||||||
|
} else {
|
||||||
|
passwordHelper.innerText = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 密码重置逻辑
|
||||||
|
async function passwordReset() {
|
||||||
|
if (emailElement.value === "") {
|
||||||
|
mdui.snackbar({
|
||||||
|
message: "邮箱不能为空",
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (passwordElement.value === "") {
|
||||||
|
mdui.snackbar({
|
||||||
|
message: "密码不能为空",
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (emailElement.value !== "" && !emailRegex.test(emailElement.value)) {
|
||||||
|
mdui.snackbar({
|
||||||
|
message: "邮箱必须以@csuwf.com结尾",
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (passwordElement.value !== password2Element.value) {
|
||||||
|
mdui.snackbar({
|
||||||
|
message: "两次输入的密码不一致",
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (authcodeElement.value === "") {
|
||||||
|
mdui.snackbar({
|
||||||
|
message: "校验码不能为空",
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// if (token === "") tell user to complete the captcha
|
||||||
|
if (token === "") {
|
||||||
|
mdui.snackbar({
|
||||||
|
message: "请完成人机验证",
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetBtn.setAttribute("loading", "");
|
||||||
|
resetBtn.setAttribute("disabled", "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/passwordreset", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: emailElement.value,
|
||||||
|
password: passwordElement.value,
|
||||||
|
authcode: authcodeElement.value,
|
||||||
|
token: token,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("HTTP error " + response.status);
|
||||||
|
}
|
||||||
|
const result = await response.text();
|
||||||
|
mdui.alert({
|
||||||
|
headline: "重置结果",
|
||||||
|
description: result,
|
||||||
|
confirmText: "了解",
|
||||||
|
onConfirm: () => {
|
||||||
|
resetBtn.removeAttribute("loading");
|
||||||
|
resetBtn.removeAttribute("disabled");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
mdui.alert({
|
||||||
|
headline: "重置结果",
|
||||||
|
description: "出错了,请重试或联系管理员",
|
||||||
|
confirmText: "了解",
|
||||||
|
onConfirm: () => {
|
||||||
|
resetBtn.removeAttribute("loading");
|
||||||
|
resetBtn.removeAttribute("disabled");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue