ci: 重构
Deploy to Staging / deploy (push) Waiting to run
Lint Backend / lint-backend (push) Waiting to run
Playwright Tests / changes (push) Waiting to run
Playwright Tests / test-playwright (1, 4) (push) Blocked by required conditions
Playwright Tests / test-playwright (2, 4) (push) Blocked by required conditions
Playwright Tests / test-playwright (3, 4) (push) Blocked by required conditions
Playwright Tests / test-playwright (4, 4) (push) Blocked by required conditions
Playwright Tests / merge-playwright-reports (push) Blocked by required conditions
Playwright Tests / alls-green-playwright (push) Blocked by required conditions
Test Backend / test-backend (push) Waiting to run
Test Docker Compose / test-docker-compose (push) Waiting to run
Deploy to Staging / deploy (push) Waiting to run
Lint Backend / lint-backend (push) Waiting to run
Playwright Tests / changes (push) Waiting to run
Playwright Tests / test-playwright (1, 4) (push) Blocked by required conditions
Playwright Tests / test-playwright (2, 4) (push) Blocked by required conditions
Playwright Tests / test-playwright (3, 4) (push) Blocked by required conditions
Playwright Tests / test-playwright (4, 4) (push) Blocked by required conditions
Playwright Tests / merge-playwright-reports (push) Blocked by required conditions
Playwright Tests / alls-green-playwright (push) Blocked by required conditions
Test Backend / test-backend (push) Waiting to run
Test Docker Compose / test-docker-compose (push) Waiting to run
This commit is contained in:
@@ -11,7 +11,7 @@ from app.core import security
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.security import get_password_hash
|
from app.core.security import get_password_hash
|
||||||
from app.models import Message, NewPassword, Token, UserPublic
|
from app.models import Message, NewPassword, Token, UserPublic
|
||||||
from app.utils import (
|
from app.utils.common import (
|
||||||
generate_password_reset_token,
|
generate_password_reset_token,
|
||||||
generate_reset_password_email,
|
generate_reset_password_email,
|
||||||
send_email,
|
send_email,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from app.models import (
|
|||||||
UserUpdate,
|
UserUpdate,
|
||||||
UserUpdateMe,
|
UserUpdateMe,
|
||||||
)
|
)
|
||||||
from app.utils import generate_new_account_email, send_email
|
from app.utils.common import generate_new_account_email, send_email
|
||||||
|
|
||||||
router = APIRouter(prefix="/users", tags=["users"])
|
router = APIRouter(prefix="/users", tags=["users"])
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from pydantic.networks import EmailStr
|
|||||||
|
|
||||||
from app.api.deps import get_current_active_superuser
|
from app.api.deps import get_current_active_superuser
|
||||||
from app.models import Message
|
from app.models import Message
|
||||||
from app.utils import generate_test_email, send_email
|
from app.utils.common import generate_test_email, send_email
|
||||||
|
|
||||||
router = APIRouter(prefix="/utils", tags=["utils"])
|
router = APIRouter(prefix="/utils", tags=["utils"])
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
from fastapi import APIRouter
|
|
||||||
|
|
||||||
from app.api.v1.endpoints import auth, users, utils, todos
|
|
||||||
|
|
||||||
api_router = APIRouter()
|
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
|
||||||
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
|
||||||
api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
|
|
||||||
api_router.include_router(todos.router, prefix="/todos", tags=["todos"])
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app.api import deps
|
|
||||||
from app.schemas.todo import Todo, TodoCreate, TodoUpdate
|
|
||||||
from app.models.todo import Todo as TodoModel
|
|
||||||
from app.crud.todo import todo
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[Todo])
|
|
||||||
def read_todos(
|
|
||||||
db: Session = Depends(deps.get_db),
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 100,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Retrieve todos.
|
|
||||||
"""
|
|
||||||
return todo.get_multi(db, skip=skip, limit=limit)
|
|
||||||
|
|
||||||
@router.post("/", response_model=Todo)
|
|
||||||
def create_todo(
|
|
||||||
*,
|
|
||||||
db: Session = Depends(deps.get_db),
|
|
||||||
todo_in: TodoCreate,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Create new todo.
|
|
||||||
"""
|
|
||||||
return todo.create(db=db, obj_in=todo_in)
|
|
||||||
|
|
||||||
@router.put("/{id}", response_model=Todo)
|
|
||||||
def update_todo(
|
|
||||||
*,
|
|
||||||
db: Session = Depends(deps.get_db),
|
|
||||||
id: int,
|
|
||||||
todo_in: TodoUpdate,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Update a todo.
|
|
||||||
"""
|
|
||||||
todo_current = todo.get(db=db, id=id)
|
|
||||||
if not todo_current:
|
|
||||||
raise HTTPException(status_code=404, detail="Todo not found")
|
|
||||||
return todo.update(db=db, db_obj=todo_current, obj_in=todo_in)
|
|
||||||
|
|
||||||
@router.get("/{id}", response_model=Todo)
|
|
||||||
def read_todo(
|
|
||||||
*,
|
|
||||||
db: Session = Depends(deps.get_db),
|
|
||||||
id: int,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Get todo by ID.
|
|
||||||
"""
|
|
||||||
todo_current = todo.get(db=db, id=id)
|
|
||||||
if not todo_current:
|
|
||||||
raise HTTPException(status_code=404, detail="Todo not found")
|
|
||||||
return todo_current
|
|
||||||
|
|
||||||
@router.delete("/{id}", response_model=Todo)
|
|
||||||
def delete_todo(
|
|
||||||
*,
|
|
||||||
db: Session = Depends(deps.get_db),
|
|
||||||
id: int,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Delete todo.
|
|
||||||
"""
|
|
||||||
todo_current = todo.get(db=db, id=id)
|
|
||||||
if not todo_current:
|
|
||||||
raise HTTPException(status_code=404, detail="Todo not found")
|
|
||||||
return todo.remove(db=db, id=id)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app.crud.base import CRUDBase
|
|
||||||
from app.models.todo import Todo
|
|
||||||
from app.schemas.todo import TodoCreate, TodoUpdate
|
|
||||||
|
|
||||||
class CRUDTodo(CRUDBase[Todo, TodoCreate, TodoUpdate]):
|
|
||||||
def create(self, db: Session, *, obj_in: TodoCreate) -> Todo:
|
|
||||||
obj_in_data = jsonable_encoder(obj_in)
|
|
||||||
db_obj = self.model(**obj_in_data)
|
|
||||||
db.add(db_obj)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_obj)
|
|
||||||
return db_obj
|
|
||||||
|
|
||||||
def get_multi(
|
|
||||||
self, db: Session, *, skip: int = 0, limit: int = 100
|
|
||||||
) -> List[Todo]:
|
|
||||||
return db.query(self.model).offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
todo = CRUDTodo(Todo)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!-- --><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style type="text/css">#outlook a { padding:0; }
|
|
||||||
.ReadMsgBody { width:100%; }
|
|
||||||
.ExternalClass { width:100%; }
|
|
||||||
.ExternalClass * { line-height:100%; }
|
|
||||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
|
||||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
|
||||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
|
||||||
p { display:block;margin:13px 0; }</style><!--[if !mso]><!--><style type="text/css">@media only screen and (max-width:480px) {
|
|
||||||
@-ms-viewport { width:320px; }
|
|
||||||
@viewport { width:320px; }
|
|
||||||
}</style><!--<![endif]--><!--[if mso]>
|
|
||||||
<xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:AllowPNG/>
|
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml>
|
|
||||||
<![endif]--><!--[if lte mso 11]>
|
|
||||||
<style type="text/css">
|
|
||||||
.outlook-group-fix { width:100% !important; }
|
|
||||||
</style>
|
|
||||||
<![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%; }
|
|
||||||
}</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;">
|
|
||||||
</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>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!-- --><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style type="text/css">#outlook a { padding:0; }
|
|
||||||
.ReadMsgBody { width:100%; }
|
|
||||||
.ExternalClass { width:100%; }
|
|
||||||
.ExternalClass * { line-height:100%; }
|
|
||||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
|
||||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
|
||||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
|
||||||
p { display:block;margin:13px 0; }</style><!--[if !mso]><!--><style type="text/css">@media only screen and (max-width:480px) {
|
|
||||||
@-ms-viewport { width:320px; }
|
|
||||||
@viewport { width:320px; }
|
|
||||||
}</style><!--<![endif]--><!--[if mso]>
|
|
||||||
<xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:AllowPNG/>
|
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml>
|
|
||||||
<![endif]--><!--[if lte mso 11]>
|
|
||||||
<style type="text/css">
|
|
||||||
.outlook-group-fix { width:100% !important; }
|
|
||||||
</style>
|
|
||||||
<![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%; }
|
|
||||||
}</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;">
|
|
||||||
</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>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><title></title><!--[if !mso]><!-- --><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style type="text/css">#outlook a { padding:0; }
|
|
||||||
.ReadMsgBody { width:100%; }
|
|
||||||
.ExternalClass { width:100%; }
|
|
||||||
.ExternalClass * { line-height:100%; }
|
|
||||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
|
||||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
|
||||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
|
||||||
p { display:block;margin:13px 0; }</style><!--[if !mso]><!--><style type="text/css">@media only screen and (max-width:480px) {
|
|
||||||
@-ms-viewport { width:320px; }
|
|
||||||
@viewport { width:320px; }
|
|
||||||
}</style><!--<![endif]--><!--[if mso]>
|
|
||||||
<xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:AllowPNG/>
|
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml>
|
|
||||||
<![endif]--><!--[if lte mso 11]>
|
|
||||||
<style type="text/css">
|
|
||||||
.outlook-group-fix { width:100% !important; }
|
|
||||||
</style>
|
|
||||||
<![endif]--><style type="text/css">@media only screen and (min-width:480px) {
|
|
||||||
.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;">
|
|
||||||
</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>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<mjml>
|
|
||||||
<mj-body background-color="#fafbfc">
|
|
||||||
<mj-section background-color="#fff" padding="40px 20px">
|
|
||||||
<mj-column vertical-align="middle" width="100%">
|
|
||||||
<mj-text align="center" padding="35px" font-size="20px" color="#333">{{ project_name }} - New Account</mj-text>
|
|
||||||
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555"><span>Welcome to your new account!</span></mj-text>
|
|
||||||
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">Here are your account details:</mj-text>
|
|
||||||
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">Username: {{ username }}</mj-text>
|
|
||||||
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">Password: {{ password }}</mj-text>
|
|
||||||
<mj-button align="center" font-size="18px" background-color="#009688" border-radius="8px" color="#fff" href="{{ link }}" padding="15px 30px">Go to Dashboard</mj-button>
|
|
||||||
<mj-divider border-color="#ccc" border-width="2px"></mj-divider>
|
|
||||||
</mj-column>
|
|
||||||
</mj-section>
|
|
||||||
</mj-body>
|
|
||||||
</mjml>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<mjml>
|
|
||||||
<mj-body background-color="#fafbfc">
|
|
||||||
<mj-section background-color="#fff" padding="40px 20px">
|
|
||||||
<mj-column vertical-align="middle" width="100%">
|
|
||||||
<mj-text align="center" padding="35px" font-size="20px" font-family="Arial, Helvetica, sans-serif" color="#333">{{ project_name }} - Password Recovery</mj-text>
|
|
||||||
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555"><span>Hello {{ username }}</span></mj-text>
|
|
||||||
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">We've received a request to reset your password. You can do it by clicking the button below:</mj-text>
|
|
||||||
<mj-button align="center" font-size="18px" background-color="#009688" border-radius="8px" color="#fff" href="{{ link }}" padding="15px 30px">Reset password</mj-button>
|
|
||||||
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">Or copy and paste the following link into your browser:</mj-text>
|
|
||||||
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555"><a href="{{ link }}">{{ link }}</a></mj-text>
|
|
||||||
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">This password will expire in {{ valid_hours }} hours.</mj-text>
|
|
||||||
<mj-divider border-color="#ccc" border-width="2px"></mj-divider>
|
|
||||||
<mj-text align="center" font-size="14px" padding-left="25px" padding-right="25px" font-family="Arial, Helvetica, sans-serif" color="#555">If you didn't request a password recovery you can disregard this email.</mj-text>
|
|
||||||
</mj-column>
|
|
||||||
</mj-section>
|
|
||||||
</mj-body>
|
|
||||||
</mjml>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<mjml>
|
|
||||||
<mj-body background-color="#fafbfc">
|
|
||||||
<mj-section background-color="#fff" padding="40px 20px">
|
|
||||||
<mj-column vertical-align="middle" width="100%">
|
|
||||||
<mj-text align="center" padding="35px" font-size="20px" font-family="Arial, Helvetica, sans-serif" color="#333">{{ project_name }}</mj-text>
|
|
||||||
<mj-text align="center" font-size="16px" padding-left="25px" padding-right="25px" font-family=", sans-serif" color="#555"><span>Test email for: {{ email }}</span></mj-text>
|
|
||||||
<mj-divider border-color="#ccc" border-width="2px"></mj-divider>
|
|
||||||
</mj-column>
|
|
||||||
</mj-section>
|
|
||||||
</mj-body>
|
|
||||||
</mjml>
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from sqlmodel import SQLModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
# JSON payload containing access token
|
||||||
|
class Token(SQLModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
|
||||||
|
|
||||||
|
# Contents of JWT token
|
||||||
|
class TokenPayload(SQLModel):
|
||||||
|
sub: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class NewPassword(SQLModel):
|
||||||
|
token: str
|
||||||
|
new_password: str = Field(min_length=8, max_length=40)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import uuid
|
||||||
|
from sqlmodel import Field, Relationship, SQLModel
|
||||||
|
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
# Shared properties
|
||||||
|
class ItemBase(SQLModel):
|
||||||
|
title: str = Field(min_length=1, max_length=255)
|
||||||
|
description: str | None = Field(default=None, max_length=255)
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to receive on item creation
|
||||||
|
class ItemCreate(ItemBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to receive on item update
|
||||||
|
class ItemUpdate(ItemBase):
|
||||||
|
title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# Database model, database table inferred from class name
|
||||||
|
class Item(ItemBase, table=True):
|
||||||
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||||
|
owner_id: uuid.UUID = Field(
|
||||||
|
foreign_key="user.id", nullable=False, ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
owner: User | None = Relationship(back_populates="items")
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to return via API, id is always required
|
||||||
|
class ItemPublic(ItemBase):
|
||||||
|
id: uuid.UUID
|
||||||
|
owner_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
|
class ItemsPublic(SQLModel):
|
||||||
|
data: list[ItemPublic]
|
||||||
|
count: int
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from app.db.base_class import Base
|
|
||||||
|
|
||||||
class Todo(Base):
|
|
||||||
__tablename__ = "todos"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
title = Column(String, index=True)
|
|
||||||
description = Column(String, nullable=True)
|
|
||||||
completed = Column(Boolean, default=False)
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from pydantic import EmailStr
|
from pydantic import EmailStr
|
||||||
from sqlmodel import Field, Relationship, SQLModel
|
from sqlmodel import Field, Relationship, SQLModel
|
||||||
|
|
||||||
|
from app.models.item import Item
|
||||||
|
|
||||||
|
|
||||||
# Shared properties
|
# Shared properties
|
||||||
class UserBase(SQLModel):
|
class UserBase(SQLModel):
|
||||||
@@ -43,7 +44,7 @@ class UpdatePassword(SQLModel):
|
|||||||
class User(UserBase, table=True):
|
class User(UserBase, table=True):
|
||||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||||
hashed_password: str
|
hashed_password: str
|
||||||
items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True)
|
items: list[Item] = Relationship(back_populates="owner", cascade_delete=True)
|
||||||
|
|
||||||
|
|
||||||
# Properties to return via API, id is always required
|
# Properties to return via API, id is always required
|
||||||
@@ -54,60 +55,3 @@ class UserPublic(UserBase):
|
|||||||
class UsersPublic(SQLModel):
|
class UsersPublic(SQLModel):
|
||||||
data: list[UserPublic]
|
data: list[UserPublic]
|
||||||
count: int
|
count: int
|
||||||
|
|
||||||
|
|
||||||
# Shared properties
|
|
||||||
class ItemBase(SQLModel):
|
|
||||||
title: str = Field(min_length=1, max_length=255)
|
|
||||||
description: str | None = Field(default=None, max_length=255)
|
|
||||||
|
|
||||||
|
|
||||||
# Properties to receive on item creation
|
|
||||||
class ItemCreate(ItemBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# Properties to receive on item update
|
|
||||||
class ItemUpdate(ItemBase):
|
|
||||||
title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
# Database model, database table inferred from class name
|
|
||||||
class Item(ItemBase, table=True):
|
|
||||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
|
||||||
owner_id: uuid.UUID = Field(
|
|
||||||
foreign_key="user.id", nullable=False, ondelete="CASCADE"
|
|
||||||
)
|
|
||||||
owner: User | None = Relationship(back_populates="items")
|
|
||||||
|
|
||||||
|
|
||||||
# Properties to return via API, id is always required
|
|
||||||
class ItemPublic(ItemBase):
|
|
||||||
id: uuid.UUID
|
|
||||||
owner_id: uuid.UUID
|
|
||||||
|
|
||||||
|
|
||||||
class ItemsPublic(SQLModel):
|
|
||||||
data: list[ItemPublic]
|
|
||||||
count: int
|
|
||||||
|
|
||||||
|
|
||||||
# Generic message
|
|
||||||
class Message(SQLModel):
|
|
||||||
message: str
|
|
||||||
|
|
||||||
|
|
||||||
# JSON payload containing access token
|
|
||||||
class Token(SQLModel):
|
|
||||||
access_token: str
|
|
||||||
token_type: str = "bearer"
|
|
||||||
|
|
||||||
|
|
||||||
# Contents of JWT token
|
|
||||||
class TokenPayload(SQLModel):
|
|
||||||
sub: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class NewPassword(SQLModel):
|
|
||||||
token: str
|
|
||||||
new_password: str = Field(min_length=8, max_length=40)
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import uuid
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from app.models import Item, ItemCreate
|
||||||
|
|
||||||
|
|
||||||
|
def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -> Item:
|
||||||
|
db_item = Item.model_validate(item_in, update={"owner_id": owner_id})
|
||||||
|
session.add(db_item)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_item)
|
||||||
|
return db_item
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import uuid
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
from app.core.security import get_password_hash, verify_password
|
from app.core.security import get_password_hash
|
||||||
from app.models import Item, ItemCreate, User, UserCreate, UserUpdate
|
from app.models import User, UserCreate, UserUpdate
|
||||||
|
|
||||||
|
|
||||||
def create_user(*, session: Session, user_create: UserCreate) -> User:
|
def create_user(*, session: Session, user_create: UserCreate) -> User:
|
||||||
@@ -35,20 +34,3 @@ def get_user_by_email(*, session: Session, email: str) -> User | None:
|
|||||||
statement = select(User).where(User.email == email)
|
statement = select(User).where(User.email == email)
|
||||||
session_user = session.exec(statement).first()
|
session_user = session.exec(statement).first()
|
||||||
return session_user
|
return session_user
|
||||||
|
|
||||||
|
|
||||||
def authenticate(*, session: Session, email: str, password: str) -> User | None:
|
|
||||||
db_user = get_user_by_email(session=session, email=email)
|
|
||||||
if not db_user:
|
|
||||||
return None
|
|
||||||
if not verify_password(password, db_user.hashed_password):
|
|
||||||
return None
|
|
||||||
return db_user
|
|
||||||
|
|
||||||
|
|
||||||
def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -> Item:
|
|
||||||
db_item = Item.model_validate(item_in, update={"owner_id": owner_id})
|
|
||||||
session.add(db_item)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(db_item)
|
|
||||||
return db_item
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
from pydantic import BaseModel
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
class TodoBase(BaseModel):
|
|
||||||
title: str
|
|
||||||
description: Optional[str] = None
|
|
||||||
completed: bool = False
|
|
||||||
|
|
||||||
class TodoCreate(TodoBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class TodoUpdate(TodoBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Todo(TodoBase):
|
|
||||||
id: int
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: Optional[datetime] = None
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from app.core.security import verify_password
|
||||||
|
from app.repositories.user import get_user_by_email
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate(*, session: Session, email: str, password: str) -> User | None:
|
||||||
|
db_user = get_user_by_email(session=session, email=email)
|
||||||
|
if not db_user:
|
||||||
|
return None
|
||||||
|
if not verify_password(password, db_user.hashed_password):
|
||||||
|
return None
|
||||||
|
return db_user
|
||||||
@@ -9,7 +9,7 @@ from app.crud import create_user
|
|||||||
from app.models import UserCreate
|
from app.models import UserCreate
|
||||||
from app.tests.utils.user import user_authentication_headers
|
from app.tests.utils.user import user_authentication_headers
|
||||||
from app.tests.utils.utils import random_email, random_lower_string
|
from app.tests.utils.utils import random_email, random_lower_string
|
||||||
from app.utils import generate_password_reset_token
|
from app.utils.common import generate_password_reset_token
|
||||||
|
|
||||||
|
|
||||||
def test_get_access_token(client: TestClient) -> None:
|
def test_get_access_token(client: TestClient) -> None:
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
pydantic>=2.8.2
|
||||||
|
sqlmodel>=0.0.14
|
||||||
|
python-jose[cryptography]>=3.3.0
|
||||||
|
passlib[bcrypt]>=1.7.4
|
||||||
|
python-multipart>=0.0.6
|
||||||
|
email-validator>=2.1.0.post1
|
||||||
Reference in New Issue
Block a user