Browse Source

🎨 [pre-commit.ci] Auto format from pre-commit.com hooks

pull/13907/head
pre-commit-ci[bot] 2 weeks ago
parent
commit
d7f439e757
  1. 2
      .copier/update_dotenv.py
  2. 1
      backend/Dockerfile
  3. 2
      backend/app/api/main.py
  4. 67
      backend/app/api/routes/ip.py
  5. 2
      backend/app/email-templates/build/new_account.html
  6. 2
      backend/app/email-templates/build/reset_password.html
  7. 2
      backend/app/email-templates/build/test_email.html
  8. 12
      backend/app/tests/scripts/test_backend_pre_start.py
  9. 12
      backend/app/tests/scripts/test_test_pre_start.py
  10. 2
      development.md
  11. 2
      frontend/.gitignore
  12. 2
      frontend/src/components/Pending/PendingItems.tsx
  13. 2
      frontend/src/components/UserSettings/DeleteAccount.tsx
  14. 4
      frontend/src/components/ui/password-input.tsx
  15. 2
      frontend/src/hooks/useCustomToast.ts
  16. 6
      frontend/src/i18n/index.ts
  17. 2
      frontend/src/routes/_layout/admin.tsx
  18. 2
      frontend/src/routes/_layout/items.tsx
  19. 4
      frontend/src/routes/_layout/settings.tsx
  20. 1
      hooks/post_gen_project.py

2
.copier/update_dotenv.py

@ -1,5 +1,5 @@
from pathlib import Path
import json import json
from pathlib import Path
# Update the .env file with the answers from the .copier-answers.yml file # Update the .env file with the answers from the .copier-answers.yml file
# without using Jinja2 templates in the .env file, this way the code works as is # without using Jinja2 templates in the .env file, this way the code works as is

1
backend/Dockerfile

@ -42,4 +42,3 @@ RUN --mount=type=cache,target=/root/.cache/uv \
# CMD ["fastapi", "run", "--workers", "1", "app/main.py"] # CMD ["fastapi", "run", "--workers", "1", "app/main.py"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--proxy-headers", "--forwarded-allow-ips", "*"] CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--proxy-headers", "--forwarded-allow-ips", "*"]

2
backend/app/api/main.py

@ -1,6 +1,6 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.api.routes import items, login, private, users, utils, ip from app.api.routes import ip, items, login, private, users, utils
from app.core.config import settings from app.core.config import settings
api_router = APIRouter() api_router = APIRouter()

67
backend/app/api/routes/ip.py

@ -1,6 +1,7 @@
from fastapi import APIRouter, Request, Response, HTTPException
import ipaddress import ipaddress
from typing import Dict, Any from typing import Any
from fastapi import APIRouter, HTTPException, Request, Response
from app.api.deps import CurrentUser from app.api.deps import CurrentUser
@ -12,12 +13,12 @@ router = APIRouter(prefix="/ip", tags=["ip"])
async def get_client_ip(request: Request): async def get_client_ip(request: Request):
""" """
获取客户端IP地址 获取客户端IP地址
返回客户端的公网IP地址通常是通过代理转发的X-Forwarded-For头获取 返回客户端的公网IP地址通常是通过代理转发的X-Forwarded-For头获取
""" """
# 尝试从各种HTTP头获取IP地址 # 尝试从各种HTTP头获取IP地址
client_ip = _extract_client_ip(request) client_ip = _extract_client_ip(request)
return {"ip": client_ip} return {"ip": client_ip}
@ -25,12 +26,12 @@ async def get_client_ip(request: Request):
async def get_client_ip_text(request: Request): async def get_client_ip_text(request: Request):
""" """
获取客户端IP地址纯文本格式 获取客户端IP地址纯文本格式
返回客户端的公网IP地址以纯文本格式 返回客户端的公网IP地址以纯文本格式
""" """
# 尝试从各种HTTP头获取IP地址 # 尝试从各种HTTP头获取IP地址
client_ip = _extract_client_ip(request) client_ip = _extract_client_ip(request)
return Response(content=client_ip, media_type="text/plain") return Response(content=client_ip, media_type="text/plain")
@ -38,7 +39,7 @@ async def get_client_ip_text(request: Request):
async def get_request_headers(request: Request): async def get_request_headers(request: Request):
""" """
获取请求的所有头信息 获取请求的所有头信息
返回包含所有HTTP请求头的字典 返回包含所有HTTP请求头的字典
""" """
return {"headers": dict(request.headers)} return {"headers": dict(request.headers)}
@ -49,28 +50,28 @@ async def get_request_headers(request: Request):
async def get_secure_ip(request: Request, current_user: CurrentUser): async def get_secure_ip(request: Request, current_user: CurrentUser):
""" """
获取客户端IP地址需要登录 获取客户端IP地址需要登录
这是一个需要用户登录的示例端点 这是一个需要用户登录的示例端点
""" """
client_ip = _extract_client_ip(request) client_ip = _extract_client_ip(request)
return { return {
"ip": client_ip, "ip": client_ip,
"user_id": str(current_user.id), "user_id": str(current_user.id),
"user_email": current_user.email "user_email": current_user.email,
} }
@router.get("/analyze/{ip_address}", response_model=Dict[str, Any]) @router.get("/analyze/{ip_address}", response_model=dict[str, Any])
async def analyze_ip(ip_address: str): async def analyze_ip(ip_address: str):
""" """
分析指定的IP地址 分析指定的IP地址
返回IP地址的基本信息如版本是否私有等 返回IP地址的基本信息如版本是否私有等
""" """
try: try:
ip = ipaddress.ip_address(ip_address) ip = ipaddress.ip_address(ip_address)
result = { result = {
"ip": str(ip), "ip": str(ip),
"version": f"IPv{ip.version}", "version": f"IPv{ip.version}",
@ -80,51 +81,51 @@ async def analyze_ip(ip_address: str):
"is_loopback": ip.is_loopback, "is_loopback": ip.is_loopback,
"is_link_local": ip.is_link_local, "is_link_local": ip.is_link_local,
} }
# IPv6特有属性 # IPv6特有属性
if ip.version == 6: if ip.version == 6:
ipv6 = ipaddress.IPv6Address(ip_address) ipv6 = ipaddress.IPv6Address(ip_address)
result.update({ result.update(
"is_site_local": ipv6.is_site_local, {
"ipv4_mapped": ipv6.ipv4_mapped is not None, "is_site_local": ipv6.is_site_local,
"teredo": ipv6.teredo is not None, "ipv4_mapped": ipv6.ipv4_mapped is not None,
"sixtofour": ipv6.sixtofour is not None "teredo": ipv6.teredo is not None,
}) "sixtofour": ipv6.sixtofour is not None,
}
)
# 尝试获取反向DNS查询名称 # 尝试获取反向DNS查询名称
try: try:
import socket import socket
result["reverse_pointer"] = socket.gethostbyaddr(ip_address)[0] result["reverse_pointer"] = socket.gethostbyaddr(ip_address)[0]
except (socket.herror, socket.gaierror): except (socket.herror, socket.gaierror):
result["reverse_pointer"] = None result["reverse_pointer"] = None
return result return result
except ValueError: except ValueError:
raise HTTPException( raise HTTPException(status_code=400, detail=f"无效的IP地址: {ip_address}")
status_code=400,
detail=f"无效的IP地址: {ip_address}"
)
def _extract_client_ip(request: Request) -> str: def _extract_client_ip(request: Request) -> str:
""" """
从请求中提取客户端IP地址 从请求中提取客户端IP地址
按照优先级尝试从不同的HTTP头获取 按照优先级尝试从不同的HTTP头获取
""" """
# 检查常见的代理头 # 检查常见的代理头
for header in [ for header in [
"X-Forwarded-For", "X-Forwarded-For",
"X-Real-IP", "X-Real-IP",
"CF-Connecting-IP", # Cloudflare "CF-Connecting-IP", # Cloudflare
"True-Client-IP", # Akamai/Cloudflare "True-Client-IP", # Akamai/Cloudflare
"X-Client-IP" "X-Client-IP",
]: ]:
if header_value := request.headers.get(header): if header_value := request.headers.get(header):
# 如果是X-Forwarded-For,可能包含多个IP,取第一个 # 如果是X-Forwarded-For,可能包含多个IP,取第一个
if header == "X-Forwarded-For" and "," in header_value: if header == "X-Forwarded-For" and "," in header_value:
return header_value.split(",")[0].strip() return header_value.split(",")[0].strip()
return header_value.strip() return header_value.strip()
# 如果没有代理头,使用直接连接的客户端地址 # 如果没有代理头,使用直接连接的客户端地址
return request.client.host if request.client else "unknown" return request.client.host if request.client else "unknown"

2
backend/app/email-templates/build/new_account.html

@ -22,4 +22,4 @@
<![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) { <![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
.mj-column-per-100 { width:100% !important; max-width: 100%; } .mj-column-per-100 { width:100% !important; max-width: 100%; }
}</style><style type="text/css"></style></head><body style="background-color:#fafbfc;"><div style="background-color:#fafbfc;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px 20px;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:560px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%"><tr><td align="center" style="font-size:0px;padding:35px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;line-height:1;text-align:center;color:#333333;">{{ project_name }} - New Account</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><span>Welcome to your new account!</span></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Here are your account details:</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Username: {{ username }}</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Password: {{ password }}</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:15px 30px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#009688" role="presentation" style="border:none;border-radius:8px;cursor:auto;padding:10px 25px;background:#009688;" valign="middle"><a href="{{ link }}" style="background:#009688;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Go to Dashboard</a></td></tr></table></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:510px;" role="presentation" width="510px" ><tr><td style="height:0;line-height:0;"> &nbsp; }</style><style type="text/css"></style></head><body style="background-color:#fafbfc;"><div style="background-color:#fafbfc;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px 20px;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:560px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%"><tr><td align="center" style="font-size:0px;padding:35px;word-break:break-word;"><div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;line-height:1;text-align:center;color:#333333;">{{ project_name }} - New Account</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><span>Welcome to your new account!</span></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Here are your account details:</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Username: {{ username }}</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Password: {{ password }}</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:15px 30px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#009688" role="presentation" style="border:none;border-radius:8px;cursor:auto;padding:10px 25px;background:#009688;" valign="middle"><a href="{{ link }}" style="background:#009688;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Go to Dashboard</a></td></tr></table></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:510px;" role="presentation" width="510px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html> </td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>

2
backend/app/email-templates/build/reset_password.html

@ -22,4 +22,4 @@
<![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) { <![endif]--><!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"><style type="text/css">@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);</style><!--<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
.mj-column-per-100 { width:100% !important; max-width: 100%; } .mj-column-per-100 { width:100% !important; max-width: 100%; }
}</style><style type="text/css"></style></head><body style="background-color:#fafbfc;"><div style="background-color:#fafbfc;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px 20px;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:560px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%"><tr><td align="center" style="font-size:0px;padding:35px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:20px;line-height:1;text-align:center;color:#333333;">{{ project_name }} - Password Recovery</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><span>Hello {{ username }}</span></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">We've received a request to reset your password. You can do it by clicking the button below:</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:15px 30px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#009688" role="presentation" style="border:none;border-radius:8px;cursor:auto;padding:10px 25px;background:#009688;" valign="middle"><a href="{{ link }}" style="background:#009688;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Reset password</a></td></tr></table></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Or copy and paste the following link into your browser:</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><a href="{{ link }}">{{ link }}</a></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">This password will expire in {{ valid_hours }} hours.</div></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:510px;" role="presentation" width="510px" ><tr><td style="height:0;line-height:0;"> &nbsp; }</style><style type="text/css"></style></head><body style="background-color:#fafbfc;"><div style="background-color:#fafbfc;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px 20px;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:560px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%"><tr><td align="center" style="font-size:0px;padding:35px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:20px;line-height:1;text-align:center;color:#333333;">{{ project_name }} - Password Recovery</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><span>Hello {{ username }}</span></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">We've received a request to reset your password. You can do it by clicking the button below:</div></td></tr><tr><td align="center" vertical-align="middle" style="font-size:0px;padding:15px 30px;word-break:break-word;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"><tr><td align="center" bgcolor="#009688" role="presentation" style="border:none;border-radius:8px;cursor:auto;padding:10px 25px;background:#009688;" valign="middle"><a href="{{ link }}" style="background:#009688;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:18px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:none;" target="_blank">Reset password</a></td></tr></table></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">Or copy and paste the following link into your browser:</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><a href="{{ link }}">{{ link }}</a></div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;">This password will expire in {{ valid_hours }} hours.</div></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:510px;" role="presentation" width="510px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:14px;line-height:1;text-align:center;color:#555555;">If you didn't request a password recovery you can disregard this email.</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html> </td></tr></table><![endif]--></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:14px;line-height:1;text-align:center;color:#555555;">If you didn't request a password recovery you can disregard this email.</div></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>

2
backend/app/email-templates/build/test_email.html

@ -22,4 +22,4 @@
<![endif]--><style type="text/css">@media only screen and (min-width:480px) { <![endif]--><style type="text/css">@media only screen and (min-width:480px) {
.mj-column-per-100 { width:100% !important; max-width: 100%; } .mj-column-per-100 { width:100% !important; max-width: 100%; }
}</style><style type="text/css"></style></head><body style="background-color:#fafbfc;"><div style="background-color:#fafbfc;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px 20px;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:560px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%"><tr><td align="center" style="font-size:0px;padding:35px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:20px;line-height:1;text-align:center;color:#333333;">{{ project_name }}</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><span>Test email for: {{ email }}</span></div></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:510px;" role="presentation" width="510px" ><tr><td style="height:0;line-height:0;"> &nbsp; }</style><style type="text/css"></style></head><body style="background-color:#fafbfc;"><div style="background-color:#fafbfc;"><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--><div style="background:#ffffff;background-color:#ffffff;Margin:0px auto;max-width:600px;"><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;"><tbody><tr><td style="direction:ltr;font-size:0px;padding:40px 20px;text-align:center;vertical-align:top;"><!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:560px;" ><![endif]--><div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;"><table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%"><tr><td align="center" style="font-size:0px;padding:35px;word-break:break-word;"><div style="font-family:Arial, Helvetica, sans-serif;font-size:20px;line-height:1;text-align:center;color:#333333;">{{ project_name }}</div></td></tr><tr><td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;"><div style="font-family:, sans-serif;font-size:16px;line-height:1;text-align:center;color:#555555;"><span>Test email for: {{ email }}</span></div></td></tr><tr><td style="font-size:0px;padding:10px 25px;word-break:break-word;"><p style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:100%;"></p><!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 2px #cccccc;font-size:1;margin:0px auto;width:510px;" role="presentation" width="510px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html> </td></tr></table><![endif]--></td></tr></table></div><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table></div><!--[if mso | IE]></td></tr></table><![endif]--></div></body></html>

12
backend/app/tests/scripts/test_backend_pre_start.py

@ -24,10 +24,10 @@ def test_init_successful_connection() -> None:
except Exception: except Exception:
connection_successful = False connection_successful = False
assert ( assert connection_successful, (
connection_successful "The database connection should be successful and not raise an exception."
), "The database connection should be successful and not raise an exception." )
assert session_mock.exec.called_once_with( assert session_mock.exec.called_once_with(select(1)), (
select(1) "The session should execute a select statement once."
), "The session should execute a select statement once." )

12
backend/app/tests/scripts/test_test_pre_start.py

@ -24,10 +24,10 @@ def test_init_successful_connection() -> None:
except Exception: except Exception:
connection_successful = False connection_successful = False
assert ( assert connection_successful, (
connection_successful "The database connection should be successful and not raise an exception."
), "The database connection should be successful and not raise an exception." )
assert session_mock.exec.called_once_with( assert session_mock.exec.called_once_with(select(1)), (
select(1) "The session should execute a select statement once."
), "The session should execute a select statement once." )

2
development.md

@ -204,4 +204,4 @@ Adminer: http://localhost.tiangolo.com:8080
Traefik UI: http://localhost.tiangolo.com:8090 Traefik UI: http://localhost.tiangolo.com:8090
MailCatcher: http://localhost.tiangolo.com:1080 MailCatcher: http://localhost.tiangolo.com:1080

2
frontend/.gitignore

@ -27,4 +27,4 @@ openapi.json
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
/playwright/.auth/ /playwright/.auth/

2
frontend/src/components/Pending/PendingItems.tsx

@ -4,7 +4,7 @@ import { SkeletonText } from "../ui/skeleton"
const PendingItems = () => { const PendingItems = () => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Table.Root size={{ base: "sm", md: "md" }}> <Table.Root size={{ base: "sm", md: "md" }}>
<Table.Header> <Table.Header>

2
frontend/src/components/UserSettings/DeleteAccount.tsx

@ -5,7 +5,7 @@ import DeleteConfirmation from "./DeleteConfirmation"
const DeleteAccount = () => { const DeleteAccount = () => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Container maxW="full"> <Container maxW="full">
<Heading size="sm" py={4}> <Heading size="sm" py={4}>

4
frontend/src/components/ui/password-input.tsx

@ -97,7 +97,7 @@ export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
const VisibilityTrigger = forwardRef<HTMLButtonElement, ButtonProps>( const VisibilityTrigger = forwardRef<HTMLButtonElement, ButtonProps>(
function VisibilityTrigger(props, ref) { function VisibilityTrigger(props, ref) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<IconButton <IconButton
tabIndex={-1} tabIndex={-1}
@ -155,7 +155,7 @@ export const PasswordStrengthMeter = forwardRef<
function getColorPalette(percent: number) { function getColorPalette(percent: number) {
const { t } = useTranslation() const { t } = useTranslation()
switch (true) { switch (true) {
case percent < 33: case percent < 33:
return { label: t("auth.passwordStrengthLow"), colorPalette: "red" } return { label: t("auth.passwordStrengthLow"), colorPalette: "red" }

2
frontend/src/hooks/useCustomToast.ts

@ -5,7 +5,7 @@ import { toaster } from "@/components/ui/toaster"
const useCustomToast = () => { const useCustomToast = () => {
const { t } = useTranslation() const { t } = useTranslation()
const showSuccessToast = (description: string) => { const showSuccessToast = (description: string) => {
toaster.create({ toaster.create({
title: t("common.success"), title: t("common.success"),

6
frontend/src/i18n/index.ts

@ -21,15 +21,15 @@ i18n
resources, resources,
fallbackLng: 'en', fallbackLng: 'en',
debug: false, debug: false,
interpolation: { interpolation: {
escapeValue: false escapeValue: false
}, },
detection: { detection: {
order: ['localStorage', 'navigator', 'htmlTag'], order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage'] caches: ['localStorage']
} }
}) })
export default i18n export default i18n

2
frontend/src/routes/_layout/admin.tsx

@ -117,7 +117,7 @@ function UsersTable() {
function Admin() { function Admin() {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Container maxW="full"> <Container maxW="full">
<Heading size="lg" pt={12}> <Heading size="lg" pt={12}>

2
frontend/src/routes/_layout/items.tsx

@ -135,7 +135,7 @@ function ItemsTable() {
function Items() { function Items() {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Container maxW="full"> <Container maxW="full">
<Heading size="lg" pt={12}> <Heading size="lg" pt={12}>

4
frontend/src/routes/_layout/settings.tsx

@ -11,14 +11,14 @@ import useAuth from "@/hooks/useAuth"
function UserSettings() { function UserSettings() {
const { user: currentUser } = useAuth() const { user: currentUser } = useAuth()
const { t } = useTranslation() const { t } = useTranslation()
const tabsConfig = [ const tabsConfig = [
{ value: "my-profile", title: t("user.profile"), component: UserInformation }, { value: "my-profile", title: t("user.profile"), component: UserInformation },
{ value: "password", title: t("user.changePassword"), component: ChangePassword }, { value: "password", title: t("user.changePassword"), component: ChangePassword },
{ value: "appearance", title: t("user.appearance"), component: Appearance }, { value: "appearance", title: t("user.appearance"), component: Appearance },
{ value: "danger-zone", title: t("user.deleteAccount"), component: DeleteAccount }, { value: "danger-zone", title: t("user.deleteAccount"), component: DeleteAccount },
] ]
const finalTabs = currentUser?.is_superuser const finalTabs = currentUser?.is_superuser
? tabsConfig.slice(0, 3) ? tabsConfig.slice(0, 3)
: tabsConfig : tabsConfig

1
hooks/post_gen_project.py

@ -1,6 +1,5 @@
from pathlib import Path from pathlib import Path
path: Path path: Path
for path in Path(".").glob("**/*.sh"): for path in Path(".").glob("**/*.sh"):
data = path.read_bytes() data = path.read_bytes()

Loading…
Cancel
Save