V1.2.0 alpha 1

- 支持使用 api 提交物品信息
- 支持 JWT 鉴权
This commit is contained in:
2025-03-28 04:53:37 +08:00
parent 3ff07b89de
commit 7d0d59677d
18 changed files with 695 additions and 247 deletions

7
JWT.py Normal file
View File

@@ -0,0 +1,7 @@
from fastapi.security import OAuth2PasswordBearer
from model import database
import asyncio
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
SECRET_KEY = asyncio.run(database.Database().get_setting('SECRET_KEY'))

270
auth.py Normal file
View File

@@ -0,0 +1,270 @@
from datetime import datetime, timedelta, timezone
from typing import Annotated
import jwt
import json
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt.exceptions import InvalidTokenError
from passlib.context import CryptContext
from pydantic import BaseModel
from nicegui import ui, app
# 得到这样的字符串 / to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# 模拟存储的用户名和密码(实际应用中应该从数据库或其他安全存储中获取)
# Emulate the username and password stored in the database (in real applications, you should get them from a database or other secure storage)
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
# FastAPI 鉴权模型
# FastAPI authentication model
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None
class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
disabled: bool | None = None
class UserInDB(User):
hashed_password: str
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except InvalidTokenError:
raise credentials_exception
user = get_user(fake_users_db, username=token_data.username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)],
):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
# FastAPI 登录路由 / FastAPI login route
@app.post("/api/token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> Token:
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return Token(access_token=access_token, token_type="bearer")
# FastAPI 获取个人信息路由 / FastAPI get personal information route
@app.get("/api/profile")
async def profile(
current_user: Annotated[User, Depends(get_current_active_user)],
):
"""
需要鉴权的路由示例
:param credentials: FastAPI 提供的用于解析HTTP Basic Auth头部的对象
:return: 成功时返回鉴权成功的消息否则返回鉴权失败的HTTP错误
"""
return current_user
# NiceGUI 获取个人信息路由 / NiceGUI get personal information route
@ui.page("/profile")
async def profile():
"""
需要鉴权的路由示例
:param credentials: FastAPI 提供的用于解析HTTP Basic Auth头部的对象
:return: 成功时返回鉴权成功的消息否则返回鉴权失败的HTTP错误
"""
ui.add_head_html("""
<script>
async function getProfile() {
const accessToken = localStorage.getItem('access_token');
if (!accessToken) {
return {'status': 'failed', 'detail': 'Access token not found'};
}
const url = '/api/profile';
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch profile');
}
const profile = await response.json();
return {'status': 'success', 'profile': profile};
} catch (error) {
return {'status': 'failed', 'detail': error.message};
}
}
function logout() {
localStorage.removeItem('access_token');
window.location.reload();
}
</script>
""")
await ui.context.client.connected()
result = await ui.run_javascript("getProfile()")
if result['status'] == 'success':
ui.label(f"Profile: {json.dumps(result['profile'])}")
ui.button("Logout", on_click=lambda: ui.run_javascript("logout()"))
else:
ui.label("Failed to fetch profile. Please login first.")
ui.button("Login", on_click=lambda: ui.navigate.to("/login"))
# 登录界面路由 / Login page route
@ui.page("/login")
async def login_page():
"""
登录界面路由示例
:return: 一个登录页面
"""
ui.add_head_html("""
<script>
async function login(username, password) {
const url = '/api/token';
const data = new URLSearchParams();
data.append('username', username);
data.append('password', password);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
});
if (!response.ok) {
throw new Error('Invalid username or password');
}
const result = await response.json();
// 处理登录成功后的数据返回access_token
localStorage.setItem('access_token', result.access_token);
return {'status': 'success'};
} catch (error) {
return {'status': 'failed', 'detail': error.message};
}
}
</script>
""")
async def try_login():
username = usrname.value
password = pwd.value
if username and password:
result = await ui.run_javascript(f"login('{username}', '{password}')")
if result['status'] == 'success':
ui.navigate.to("/profile")
else:
ui.notify("Login failed. Please try again.", type="negative")
usrname = ui.input("username")
pwd = ui.input("password", password=True)
ui.button("Login", on_click=try_login)
# 公开路由 / Public page route
@ui.page("/")
async def public_data():
"""
公开路由示例
:return: 一个公开的消息,无需鉴权
"""
ui.label("This is a public page.")
if __name__ in {"__main__", "__mp_main__"}:
ui.run(uvicorn_logging_level='debug')

68
main.py
View File

@@ -10,16 +10,13 @@ Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
'''
from nicegui import app, ui
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi.responses import RedirectResponse, JSONResponse
import traceback
import model.database
import notfound
import main_page
import found
import login
import admin
from routes.frontend import main_page
from routes.frontend import found
from routes.frontend import login
from routes.frontend import admin
from routes.backend import session
import model
import asyncio
import logging
@@ -30,53 +27,7 @@ found.create()
login.create()
admin.create()
# 中间件配置文件
AUTH_CONFIG = {
"restricted_routes": {'/admin'},
"login_url": "/login",
"cookie_name": "session",
"session_expire": 3600 # 会话过期时间
}
def is_restricted_route(path: str) -> bool:
"""判断路径是否为需要认证的受限路由"""
# NiceGUI 路由不受限制
if path.startswith('/_nicegui'):
return False
# 静态资源路径不受限制
if path.startswith('/static'):
return False
# 主题路径不受限制
if path.startswith('/theme'):
return False
# 后台路径始终受限
if path.startswith('/admin'):
return True
# 检查是否为受限的客户端页面路由
if path.startswith('/dash') or path.startswith('/user'):
return True
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
try:
if not app.storage.user.get('authenticated', False):
path = request.url.path
if is_restricted_route(path):
logging.warning(f"未认证用户尝试访问: {path}")
return RedirectResponse(f'/login?redirect_to={path}')
return await call_next(request)
except Exception as e:
logging.error(f"服务器错误 Server error: {str(traceback.format_exc())}")
return JSONResponse(status_code=500, content={"detail": e})
# 添加中间件 Add middleware
app.add_middleware(AuthMiddleware)
app.include_router(session.Router)
# 添加静态文件目录
try:
@@ -86,16 +37,15 @@ except RuntimeError:
# 启动函数 Startup function
def startup():
asyncio.run(model.Database().init_db())
asyncio.run(model.database.Database().init_db())
ui.run(
host='0.0.0.0',
favicon='🚀',
port=8080,
title='Findreve',
native=False,
storage_secret='findreve',
language='zh-CN',
fastapi_docs=False)
fastapi_docs=True)
if __name__ in {"__main__", "__mp_main__"}:
startup()

File diff suppressed because one or more lines are too long

1
model/__init__.py Normal file
View File

@@ -0,0 +1 @@
from . import token

View File

@@ -90,6 +90,15 @@ class Database:
logging.info("插入初始密码信息")
print(f"密码(请牢记,后续不再显示): {password}")
async with db.execute("SELECT name FROM fr_settings WHERE name = 'SECRET_KEY'") as cursor:
if not await cursor.fetchone():
secret_key = tool.generate_password(64)
await db.execute(
"INSERT INTO fr_settings (type, name, value) VALUES (?, ?, ?)",
('string', 'SECRET_KEY', secret_key)
)
logging.info("插入初始密钥信息")
await db.commit()
logging.info("数据库初始化完成并提交更改")
@@ -118,7 +127,6 @@ class Database:
"""更新对象信息
:param id: 对象ID
:param key: 序列号
:param kwargs: 更新字段
"""
set_values = ", ".join([f"{k} = ?" for k in kwargs.keys()])
@@ -147,6 +155,15 @@ class Database:
async with db.execute("SELECT * FROM fr_objects") as cursor:
return await cursor.fetchall()
async def delete_object(self, id: int):
"""删除对象
:param id: 对象ID
"""
async with aiosqlite.connect(self.db_path) as db:
await db.execute("DELETE FROM fr_objects WHERE id = ?", (id,))
await db.commit()
async def set_setting(self, name: str, value: str):
"""设置配置项

6
model/response.py Normal file
View File

@@ -0,0 +1,6 @@
from pydantic import BaseModel
class DefaultResponse(BaseModel):
code: int = 0
data: dict | list | None = None
msg: str = ""

13
model/token.py Normal file
View File

@@ -0,0 +1,13 @@
from pydantic import BaseModel
from passlib.context import CryptContext
# FastAPI 鉴权模型
# FastAPI authentication model
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

View File

@@ -0,0 +1,2 @@
from . import admin
from . import session

74
routes/backend/admin.py Normal file
View File

@@ -0,0 +1,74 @@
from nicegui import app
from typing import Annotated
from fastapi import Depends
from fastapi import HTTPException, status
from jwt import InvalidTokenError
import jwt, JWT
from model import database
from model import token as Token
from model.response import DefaultResponse
async def is_admin(token: Annotated[str, Depends(JWT.oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Login required",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, JWT.SECRET_KEY, algorithms=["HS256"])
username = payload.get("sub")
if username is None:
raise credentials_exception
except InvalidTokenError:
raise credentials_exception
if not username == await database.Database().get_setting('account'):
raise credentials_exception
token_data = Token.TokenData(username=username)
return True
@app.get('/api/items')
async def get_items(
is_admin: Annotated[str, Depends(is_admin)],
id: int = None,
key: str = None):
items = await database.Database().get_object(id=id, key=key)
return DefaultResponse(data=items)
@app.post('/api/items')
async def add_items(
is_admin: Annotated[str, Depends(is_admin)],
key: str,
name: str,
icon: str,
phone: str):
try:
await database.Database().add_object(
key=key, name=name, icon=icon, phone=phone)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse()
@app.patch('/api/items')
async def update_items(
is_admin: Annotated[str, Depends(is_admin)],
id: int,
**kwargs):
try:
await database.Database().update_object(
id=id, **kwargs)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse()
@app.delete('/api/items')
async def delete_items(
is_admin: Annotated[str, Depends(is_admin)],
id: int):
try:
await database.Database().delete_object(id=id)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse()

51
routes/backend/session.py Normal file
View File

@@ -0,0 +1,51 @@
from nicegui import app
from typing import Annotated
from datetime import datetime, timedelta, timezone
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from fastapi import APIRouter
import jwt, JWT
from model.token import Token
from model import database
from tool import verify_password
Router = APIRouter()
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, JWT.SECRET_KEY, algorithm='HS256')
return encoded_jwt
async def authenticate_user(username: str, password: str):
# 验证账号和密码
account = await database.Database().get_setting('account')
stored_password = await database.Database().get_setting('password')
if account != username or not verify_password(stored_password, password):
return False
return {'is_authenticated': True}
# FastAPI 登录路由 / FastAPI login route
@app.post("/api/token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> Token:
user = await authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(hours=1)
access_token = create_access_token(
data={"sub": form_data.username}, expires_delta=access_token_expires
)
return Token(access_token=access_token, token_type="bearer")

View File

@@ -16,6 +16,7 @@ import qrcode
import base64
from io import BytesIO
from fastapi import Request
import model.database
from tool import *
from datetime import datetime
@@ -112,7 +113,7 @@ def create():
object_key.set_value(generate_password())
try:
await model.Database().add_object(key=object_key.value, name=object_name.value, icon=object_icon.value, phone=object_phone.value)
await model.database.Database().add_object(key=object_key.value, name=object_name.value, icon=object_icon.value, phone=object_phone.value)
except ValueError as e:
ui.notify(str(e), color='negative')
else:
@@ -187,11 +188,11 @@ def create():
try:
# 获取选中物品
object_id = object_table.selected[0]['id']
await model.Database().update_object(id=object_id, status='lost')
await model.Database().update_object(id=object_id, lost_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
await model.database.Database().update_object(id=object_id, status='lost')
await model.database.Database().update_object(id=object_id, lost_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
# 如果设置了留言,则更新留言
if lostReason.value != "":
await model.Database().update_object(id=object_id, context=lostReason.value)
await model.database.Database().update_object(id=object_id, context=lostReason.value)
except Exception as e:
ui.notify(str(e), color='negative')
else:
@@ -227,10 +228,10 @@ def create():
async def findObject():
try:
object_id = object_table.selected[0]['id']
await model.Database().update_object(id=object_id, status='ok')
await model.Database().update_object(id=object_id, context=None)
await model.Database().update_object(id=object_id, find_ip=None)
await model.Database().update_object(id=object_id, lost_at=None)
await model.database.Database().update_object(id=object_id, status='ok')
await model.database.Database().update_object(id=object_id, context=None)
await model.database.Database().update_object(id=object_id, find_ip=None)
await model.database.Database().update_object(id=object_id, lost_at=None)
except Exception as e:
ui.notify(str(e), color='negative')
else:
@@ -261,7 +262,7 @@ def create():
async def fetch_and_process_objects():
# 获取所有物品
objects = [dict(zip(['id', 'key', 'name', 'icon', 'status', 'phone', 'context',
'find_ip', 'create_at', 'lost_at'], obj)) for obj in await model.Database().get_object()]
'find_ip', 'create_at', 'lost_at'], obj)) for obj in await model.database.Database().get_object()]
status_map = {'ok': '正常', 'lost': '丢失'}
for obj in objects:
obj['status'] = status_map.get(obj['status'], obj['status'])

View File

@@ -11,7 +11,7 @@ Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
from nicegui import ui
from fastapi import Request
from model import Database
from model.database import Database
from tool import format_phone
def create() -> None:

View File

@@ -0,0 +1,29 @@
from contextlib import contextmanager
from nicegui import ui
from fastapi import Request
@contextmanager
def frame(request: Request):
with ui.header() \
.classes('items-center duration-300 py-2 px-5 no-wrap') \
.style('box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1)'):
with ui.tabs(value='main_page') as tabs:
ui.button(icon='menu', on_click=lambda: left_drawer.toggle()).props('flat color=white round')
ui.button(text="Findreve 仪表盘").classes('text-lg').props('flat color=white no-caps')
with ui.left_drawer() as left_drawer:
with ui.column().classes('w-full'):
ui.image('/static/Findreve.png').classes('w-1/2 mx-auto')
ui.label('Findreve').classes('text-2xl text-bold')
ui.label("免费版,无需授权").classes('text-red-600 -mt-3')
ui.button('首页 & 信息', icon='fingerprint', on_click=lambda: ui.navigate.to('/admin/home')) \
.classes('w-full').props('flat no-caps')
ui.button('物品 & 库存', icon='settings', on_click=lambda: ui.navigate.to('/admin/item')) \
.classes('w-full').props('flat no-caps')
ui.button('产品 & 授权', icon='settings', on_click=lambda: ui.navigate.to('/admin/auth')) \
.classes('w-full').props('flat no-caps')
ui.button('关于 & 反馈', icon='settings', on_click=lambda: ui.navigate.to('/admin/about')) \
.classes('w-full').props('flat no-caps')
yield

View File

@@ -11,17 +11,46 @@ Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
from nicegui import ui, app
from typing import Optional
import model
import tool
from fastapi.responses import RedirectResponse
from fastapi import Request
def create() -> Optional[RedirectResponse]:
@ui.page('/login')
async def session(request: Request, redirect_to: str = "/"):
# 检测是否已登录
if app.storage.user.get('authenticated', False):
return ui.navigate.to(redirect_to)
ui.add_head_html("""
<script>
async function login(username, password) {
const url = '/api/token';
const data = new URLSearchParams();
data.append('username', username);
data.append('password', password);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
});
if (!response.ok) {
throw new Error('Invalid username or password');
}
const result = await response.json();
// 处理登录成功后的数据返回access_token
localStorage.setItem('access_token', result.access_token);
return {'status': 'success'};
} catch (error) {
return {'status': 'failed', 'detail': error.message};
}
}
</script>
""")
ui.page_title('登录 Findreve')
async def try_login() -> None:
@@ -34,18 +63,14 @@ def create() -> Optional[RedirectResponse]:
ui.notify('账号或密码不能为空', color='negative')
return
# 验证账号和密码
account = await model.Database().get_setting('account')
stored_password = await model.Database().get_setting('password')
if account != username.value or not tool.verify_password(stored_password, password.value, debug=True):
ui.notify('账号或密码错误', color='negative')
return
# 存储用户信息
app.storage.user.update({'authenticated': True})
# 跳转到用户上一页
try:
result = await ui.run_javascript(f"login('{username}', '{password}')")
if result['status'] == 'success':
ui.navigate.to(redirect_to)
else:
ui.notify("账号或密码错误", type="negative")
except Exception as e:
ui.notify(f"登录失败: {str(e)}", type="negative")
with ui.header() \
@@ -59,12 +84,12 @@ def create() -> Optional[RedirectResponse]:
with ui.card().classes('absolute-center round-lg').style('width: 70%; max-width: 500px'):
# 登录标签
ui.button(icon='lock').props('outline round').classes('mx-auto w-auto shadow-sm w-fill')
ui.label('登录 HeyPress').classes('text-h5 w-full text-center')
ui.label('登录 Findreve').classes('text-h5 w-full text-center')
# 用户名/密码框
username = ui.input('账号').on('keydown.enter', try_login) \
.classes('block w-full text-gray-900').props('rounded outlined')
.classes('block w-full text-gray-900').props('filled')
password = ui.input('密码', password=True, password_toggle_button=True) \
.on('keydown.enter', try_login).classes('block w-full text-gray-900').props('rounded outlined')
.on('keydown.enter', try_login).classes('block w-full text-gray-900').props('filled')
# 按钮布局
ui.button('登录', on_click=lambda: login()).classes('items-center w-full').props('rounded')

View File

@@ -0,0 +1,148 @@
'''
Author: 于小丘 海枫
Date: 2024-10-02 15:23:34
LastEditors: Yuerchu admin@yuxiaoqiu.cn
LastEditTime: 2024-11-29 20:04:24
FilePath: /Findreve/main_page.py
Description: Findreve 个人主页 main_page
Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
'''
from nicegui import ui
from fastapi import Request
import logging
import requests
import json
import model
def create() -> None:
@ui.page('/')
async def main_page(request: Request) -> None:
dark_mode = ui.dark_mode(value=True)
with ui.row(align_items='center').classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl p-24'):
with ui.column(align_items='center').classes('px-2 max-md:hidden'):
ui.chip('🐍 Python 是最好的语言').classes('text-xs -mt-1 -right-3').props('floating outline')
ui.chip('🎹 精通 FL Studio Mobile').classes('text-xs -mt-1').props('floating outline')
ui.chip('🎨 熟悉 Ps/Pr/Ae/Au/Ai').classes('text-xs -mt-1').props('floating outline')
ui.chip('🏎 热爱竞速(如地平线5)').classes('text-xs -mt-1 -right-3').props('floating outline')
with ui.avatar().classes('w-32 h-32 transition-transform duration-300 hover:scale-110 cursor-pointer'):
# 下面的这个是Base64格式你需要改成你自己的头像支持链接也可以用Base64本地化
ui.image('/static/heyfun.jpg').classes('w-32 h-32')
with ui.column().classes('px-2 max-md:hidden'):
ui.chip('喜欢去广州图书馆看书 📕').classes('text-xs -mt-1 -left-3').props('floating outline')
ui.chip('致力做安卓苹果开发者 📱').classes('text-xs -mt-1').props('floating outline')
ui.chip('正在自研全链个人生态 🔧').classes('text-xs -mt-1').props('floating outline')
ui.chip('致力与开源社区同发展 🤝').classes('text-xs -mt-1 -left-3').props('floating outline')
ui.label('关于本站').classes('w-full text-4xl text-bold text-center py-6 subpixel-antialiased')
with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl py-4'):
with ui.card().classes('w-full sm:w-1/5 lg:w-1/7 flex-grow p-8 bg-gradient-to-br from-indigo-700 to-blue-500'):
ui.label('你好,很高兴认识你👋').classes('text-md text-white')
with ui.row(align_items='center'):
ui.label('我叫').classes('text-4xl text-bold text-white -mt-1 subpixel-antialiased')
ui.label('于小丘').classes('text-4xl text-bold text-white -mt-1 subpixel-antialiased').tooltip('英文名叫Yuerchu也可以叫我海枫')
ui.label('是一名 开发者、音乐人').classes('text-md text-white -mt-1')
with ui.card().classes('w-full sm:w-1/2 lg:w-1/4 flex-grow flex flex-col justify-center'):
ui.code('void main() {\n printf("为了尚未完成的未来");\n}', language='c').classes('text-3xl max-[768px]:text-xl text-bold text-white flex-grow w-full h-full')
with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl -mt-3'):
with ui.card().classes('w-full sm:w-1/2 lg:w-1/4 flex-grow p-4'):
ui.label('技能').classes('text-md text-gray-500')
ui.label('开启创造力').classes('text-4xl text-bold -mt-1 right-4')
with ui.row().classes('items-center'):
ui.chip('Python', color='amber-400').classes('p-4').props('floating').tooltip('Python是世界上最好的语言')
ui.chip('Kotlin', color='violet-400').classes('p-4').props('floating').tooltip('Kotlin给安卓开发APP')
ui.chip('Golang', color='sky-400').classes('p-4').props('floating').tooltip('Golang写后端')
ui.chip('Lua', color='blue-900').classes('p-4').props('floating').tooltip('用aLua给安卓开发给罗技鼠标写鼠标宏')
ui.chip('c', color='red-400').classes('p-4').props('floating').tooltip('C写嵌入式开发')
ui.chip('FL Studio', color='orange-600').classes('p-4').props('floating').tooltip('FL Studio是世界上最好的宿主')
ui.chip('Photoshop', color='blue-950').classes('p-4').props('floating').tooltip('修图/抠图/画画一站通')
ui.chip('Premiere', color='indigo-900').classes('p-4').props('floating').tooltip('剪视频比较顺手,但是一开风扇狂转')
ui.chip('After Effects', color='indigo-950').classes('p-4').props('floating').tooltip('制作特效,电脑太烂了做不了太花的')
ui.chip('Audition', color='purple-900').classes('p-4').props('floating').tooltip('写歌做母带挺好用的')
ui.chip('Illustrator', color='amber-800').classes('p-4').props('floating').tooltip('自制字体和画动态SVG')
ui.chip('HTML', color='red-900').classes('p-4').props('floating').tooltip('前端入门三件套,不学这玩意其他学了没用')
ui.chip('CSS3', color='cyan-900').classes('p-4').props('floating').tooltip('. window{ show: none; }')
ui.chip('JavaScript', color='lime-900').classes('p-4').props('floating').tooltip('还在努力学习中,只会一些简单的')
ui.chip('git', color='amber-700').classes('p-4').props('floating').tooltip('版本管理是真好用')
ui.chip('Docker', color='sky-600').classes('p-4').props('floating').tooltip('容器化部署')
ui.chip('chatGPT', color='emerald-600').classes('p-4').props('floating').tooltip('文本助驾,写代码/写文章/写论文')
ui.chip('SAI2', color='gray-950').classes('p-4').props('floating').tooltip('入门绘画')
ui.chip('ips Draw', color='gray-900').classes('p-4').props('floating').tooltip('自认为是iOS端最佳绘画软件')
ui.chip('AutoCAD', color='gray-950').classes('p-4').props('floating').tooltip('画图/绘制电路图')
ui.chip('SolidWorks', color='gray-900').classes('p-4').props('floating').tooltip('画图/绘制3D模型')
ui.chip('EasyEDA', color='gray-950').classes('p-4').props('floating').tooltip('画图/绘制电路图')
ui.chip('KiCad', color='gray-900').classes('p-4').props('floating').tooltip('画图/绘制电路图')
ui.chip('Altium Designer', color='gray-950').classes('p-4').props('floating').tooltip('画图/绘制电路图')
ui.label('...').classes('text-md text-gray-500')
with ui.card().classes('w-full sm:w-1/3 lg:w-1/6 flex-grow flex flex-col justify-center'):
ui.label('生涯').classes('text-md text-gray-500')
ui.label('无限进步').classes('text-4xl text-bold -mt-1 right-4')
with ui.timeline(side='right', layout='comfortable'):
ui.timeline_entry('那天我买了第一台服务器并搭建了我第一个Wordpress站点',
title='梦开始的地方',
subtitle='2022年1月21日')
ui.timeline_entry('准备从Cloudreve项目脱离自建网盘系统DiskNext',
title='自建生态计划开始',
subtitle='2024年3月1日')
ui.timeline_entry('目前正在开发HeyAuth、Findreve、DiskNext',
title='项目框架仍在研发中',
subtitle='现在',
icon='rocket')
ui.label('我的作品').classes('w-full text-center text-2xl text-bold p-4')
with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl'):
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('DiskNext').classes('text-lg text-bold')
ui.chip('B端程序').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://pan.yxqi.cn'))).props('flat fab-mini')
ui.label('一个基于NiceGUI的网盘系统性能与Golang媲美').classes('text-sm -mt-3')
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('Findreve').classes('text-lg text-bold')
ui.chip('C端程序').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://i.yxqi.cn'))).props('flat fab-mini')
ui.label('一个基于NiceGUI的个人主页配合物品丢失找回系统').classes('text-sm -mt-3')
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('HeyAuth').classes('text-lg text-bold')
ui.chip('B端程序').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://auth.yxqi.cn'))).props('flat fab-mini')
ui.label('一个基于NiceGUI的B+C端多应用授权系统').classes('text-sm -mt-3')
with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl'):
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('与枫同奔 Run With Fun').classes('text-lg text-bold')
ui.chip('词曲').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://music.163.com/#/song?id=2148944359'))).props('flat fab-mini')
ui.label('我愿如流星赶月那样飞奔').classes('text-sm -mt-3')
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('HeyFun\'s Story').classes('text-lg text-bold')
ui.chip('自设印象曲').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://music.163.com/#/song?id=1889436124'))).props('flat fab-mini')
ui.label('飞奔在星辰大海之间的少年').classes('text-sm -mt-3')
with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'):
with ui.row().classes('items-center w-full -mt-2'):
ui.label('2020Fall').classes('text-lg text-bold')
ui.chip('年度纯音乐').classes('text-xs').props('floating')
ui.space()
ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://music.163.com/#/song?id=1863630345'))).props('flat fab-mini')
ui.label('耗时6个月完成的年度纯音乐').classes('text-sm -mt-3')

BIN
static/heyfun.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

18
tool.py
View File

@@ -9,8 +9,6 @@ Description: Findreve 小工具 tool
Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved.
'''
import string
import random
import hashlib
import binascii
import logging
@@ -44,11 +42,17 @@ def format_phone(phone: str, groups: list = None, separator: str = " ", private:
return separator.join(result)
def generate_password(length: int = 8) -> str:
# 定义密码字符集,大小写字母和数字
characters = string.ascii_letters + string.digits
# 随机选择length个字符生成密码
password = ''.join(random.choice(characters) for i in range(length))
return password
"""
生成指定长度的随机密码。
:param length: 密码长度
:type length: int
:return: 随机密码
:rtype: str
"""
import secrets
return secrets.token_hex(length)
def hash_password(password: str) -> str:
"""