diff --git a/backend/db/models/_model_base.py b/backend/db/models/_model_base.py index 8fc0eae..d89b830 100644 --- a/backend/db/models/_model_base.py +++ b/backend/db/models/_model_base.py @@ -1,16 +1,16 @@ from datetime import datetime from sqlalchemy import DateTime -from sqlalchemy.orm import declarative_base, Mapped, mapped_column +from sqlalchemy.orm import declarative_base, Mapped, mapped_column, QueryPropertyDescriptor from text_unidecode import unidecode from backend.db.db_setup import SessionLocal Model = declarative_base() -Model.query = SessionLocal.query_property() class SqlAlchemyBase(Model): __abstract__ = True + query: QueryPropertyDescriptor = SessionLocal.query_property() created_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.utcnow(), index=True) updated_at: Mapped[datetime | None] = mapped_column(DateTime, default=datetime.utcnow(), onupdate=datetime.utcnow()) diff --git a/backend/repos/repository_users.py b/backend/repos/repository_users.py index acaee17..d2d15b0 100644 --- a/backend/repos/repository_users.py +++ b/backend/repos/repository_users.py @@ -1,7 +1,7 @@ from backend.core.config import get_app_settings from backend.core.security.security import hash_password from backend.db.models import User -from backend.schemas import UserCreate +from backend.schemas import UserCreate, UserProfile from sqlalchemy.orm import Session from uuid import UUID @@ -20,7 +20,7 @@ class RepositoryUsers: return self.user.query.filter_by(username=username).first() def get_by_id(self, user_id: str): - return self.user.query.filter_by(id=UUID(user_id)).first() + return self.user.query.filter_by(id=UUID(str(user_id))).one() def create(self, db: Session, user: UserCreate | UserSeeds): try: @@ -34,3 +34,18 @@ class RepositoryUsers: db.refresh(db_user) return db_user + + def update(self, db: Session, user: UserProfile, user_id: str): + db_user = self.get_by_id(user_id) + if not db_user: + return None + try: + user.update_password() + self.user.query.where(User.id == user_id).update(user.dict(exclude_unset=True, exclude_none=True)) + db.commit() + except Exception: + db.rollback() + raise + + db.refresh(db_user) + return db_user diff --git a/backend/routes/auth/auth.py b/backend/routes/auth/auth.py index 6602d41..2b257d7 100644 --- a/backend/routes/auth/auth.py +++ b/backend/routes/auth/auth.py @@ -9,7 +9,7 @@ from backend.core.config import get_app_settings from backend.core.dependencies.dependencies import get_current_user from backend.core import MessageCode from backend.db.db_setup import generate_session -from backend.schemas import ReturnValue, UserRequest, LoginResponse, UserCreate, PrivateUser +from backend.schemas import ReturnValue, UserRequest, LoginResponse, PrivateUser from backend.services.user import UserService @@ -29,14 +29,6 @@ async def get_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], return {'access_token': token, 'token_type': 'bearer'} -@auth_router.put('/register') -def register_user(user: UserCreate, db: db_dependency) -> ReturnValue[Any]: - db_user = user_service.get_by_username(username=user.username) - if db_user: - raise HTTPException(status_code=400, detail=MessageCode.CREATED_USER) - user_service.create(db=db, user=user) - return ReturnValue(status=200, data="created") - @auth_router.post('/login', response_model=ReturnValue[LoginResponse]) def user_login(user: UserRequest, response: Response) -> ReturnValue[Any]: db_user = user_service.check_exist(user=user) diff --git a/backend/routes/user/user.py b/backend/routes/user/user.py index f171120..93ec7bf 100644 --- a/backend/routes/user/user.py +++ b/backend/routes/user/user.py @@ -1,11 +1,12 @@ from typing import Annotated, Any -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from backend.core.config import get_app_settings from backend.core.dependencies import is_logged_in +from backend.core.message_code import MessageCode from backend.db.db_setup import generate_session from backend.schemas.common import ReturnValue -from backend.schemas.user import ProfileResponse +from backend.schemas.user import ProfileResponse, UserCreate, UserProfile from backend.services.user import UserService @@ -16,6 +17,19 @@ settings = get_app_settings() db_dependency = Annotated[Session, Depends(generate_session)] current_user_token = Annotated[ProfileResponse, Depends(is_logged_in)] +@public_router.put('/register') +def register_user(user: UserCreate, db: db_dependency) -> ReturnValue[Any]: + db_user = user_service.get_by_username(username=user.username) + if db_user: + raise HTTPException(status_code=400, detail=MessageCode.CREATED_USER) + user_service.create(db=db, user=user) + return ReturnValue(status=200, data="created") + @public_router.get("/me", response_model=ReturnValue[ProfileResponse]) def get_user(current_user: current_user_token) -> ReturnValue[Any]: return ReturnValue(status=200, data=current_user) + +@public_router.put("/update-profile", response_model=ReturnValue[ProfileResponse]) +def update_user(user: UserProfile, current_user: current_user_token, db: db_dependency) -> ReturnValue[Any]: + db_user = user_service.update(db=db, user=user, user_id=current_user.id) + return ReturnValue(status=200, data=db_user) diff --git a/backend/schemas/user/user.py b/backend/schemas/user/user.py index 1e78f8f..5a606e7 100644 --- a/backend/schemas/user/user.py +++ b/backend/schemas/user/user.py @@ -3,6 +3,7 @@ from uuid import UUID from pydantic import ConfigDict from fastapi import Form +from backend.core.security.security import hash_password from backend.schemas.main_model import MainModel class UserBase(MainModel): @@ -14,6 +15,16 @@ class UserRequest(UserBase): class UserCreate(UserRequest): name: str +class UserProfile(MainModel): + name: str | None = None + password: str | None = None + is_admin: bool | None = None + is_lock: bool | None = None + model_config = ConfigDict(from_attributes=True) + + def update_password(self): + self.password = (None if self.password is None else hash_password(self.password)) + class UserSeeds(UserCreate): is_admin: bool is_lock: bool diff --git a/backend/services/user/user_service.py b/backend/services/user/user_service.py index c60f947..5a968b7 100644 --- a/backend/services/user/user_service.py +++ b/backend/services/user/user_service.py @@ -10,12 +10,20 @@ class UserService(BaseService): def __init__(self): self.repos = RepositoryUsers() - def get_all(self, skip: int = 0, limit: int = 100): - return self.repos.get_all(skip=skip, limit=limit) + def generate_token(self, user_id: str): + access_token = create_access_token(data={"sub": str(user_id)}) + refresh_token = create_refresh_token(data={"sub": str(user_id)}) + return access_token, refresh_token def get_by_username(self, username: str): return self.repos.get_by_username(username) + def get_access_token(self, user_id: str): + return create_access_token(data={"sub": str(user_id)}) + + def get_all(self, skip: int = 0, limit: int = 100): + return self.repos.get_all(skip=skip, limit=limit) + def get_by_id(self, user_id: str): return self.repos.get_by_id(user_id) @@ -23,6 +31,7 @@ class UserService(BaseService): return self.repos.create(db=db, user=user) def check_exist(self, user: UserRequest): + print(f"user: {user}") db_user = self.get_by_username(username=user.username) if not db_user: return False @@ -30,10 +39,5 @@ class UserService(BaseService): return False return db_user - def generate_token(self, user_id: str): - access_token = create_access_token(data={"sub": str(user_id)}) - refresh_token = create_refresh_token(data={"sub": str(user_id)}) - return access_token, refresh_token - - def get_access_token(self, user_id: str): - return create_access_token(data={"sub": str(user_id)}) + def update(self, db: Session, user: UserCreate, user_id: str): + return self.repos.update(db=db, user=user, user_id=user_id) diff --git a/frontend/src/api/url.js b/frontend/src/api/url.js index 7845825..d2402e0 100644 --- a/frontend/src/api/url.js +++ b/frontend/src/api/url.js @@ -2,3 +2,4 @@ export const POST_LOGIN = '/api/auth/login' export const POST_LOGOUT = '/api/auth/logout' export const POST_REFRESH = '/api/auth/refresh' export const GET_USER_PROFILE = '/api/user/me' +export const PUT_UPDATE_USER_PROFILE = '/api/user/update-profile' diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js index 02c97db..7b3dda6 100644 --- a/frontend/src/api/user.js +++ b/frontend/src/api/user.js @@ -1,6 +1,10 @@ import { protocol } from './index' -import { GET_USER_PROFILE } from './url' +import { GET_USER_PROFILE, PUT_UPDATE_USER_PROFILE } from './url' export const getProfile = () => { return protocol.get(GET_USER_PROFILE, {}) } + +export const putUpdateProfile = (payload) => { + return protocol.put(PUT_UPDATE_USER_PROFILE, payload) +} diff --git a/frontend/src/components/common/TextInput/TextInput.jsx b/frontend/src/components/common/TextInput/TextInput.jsx new file mode 100644 index 0000000..cba0c2b --- /dev/null +++ b/frontend/src/components/common/TextInput/TextInput.jsx @@ -0,0 +1,39 @@ +import { Field } from 'solid-form-handler' +import { Show, splitProps } from 'solid-js' +import { Dynamic } from 'solid-js/web' + +export default function Finput(props) { + const [local, rest] = splitProps(props, ['label', 'icon']) + + return ( + ( +
+ +
+ {local.label} +
+
+ + +
+ + {field.helpers.errorMessage} + +
+
+
+ )} + /> + ) +} diff --git a/frontend/src/components/common/TextInput/index.js b/frontend/src/components/common/TextInput/index.js new file mode 100644 index 0000000..30a59db --- /dev/null +++ b/frontend/src/components/common/TextInput/index.js @@ -0,0 +1 @@ +export { default } from './TextInput' diff --git a/frontend/src/hooks/useAuth.js b/frontend/src/hooks/useAuth.js index e0897f8..68bba11 100644 --- a/frontend/src/hooks/useAuth.js +++ b/frontend/src/hooks/useAuth.js @@ -7,12 +7,7 @@ export default function useAuth(setAuth) { const navigate = useNavigate() const clickLogIn = async (username, password, cbFormReset) => { - const loginData = { - username: username, - password: password, - } - - const resp = await postLogin(loginData) + const resp = await postLogin({ username, password }) if (resp.status === 200) { const token = resp.data || {} diff --git a/frontend/src/lang/vi.json b/frontend/src/lang/vi.json index be6542f..62a1140 100644 --- a/frontend/src/lang/vi.json +++ b/frontend/src/lang/vi.json @@ -6,7 +6,13 @@ "logout": "Đăng xuất", "dashboard": "Bảng điều khiển", "profile": "Hồ sơ", - "houses": "Kho" + "changeInfo": "Đổi thông tin", + "save": "Lưu", + "clear": "Xóa", + "houses": "Kho", + "displayName": "Display Name", + "newPassword": "New Password", + "confirmNewPassword": "Confirm New Password" }, "message": { "CREATED_USER": "Username already registered!", diff --git a/frontend/src/pages/Login/Login.jsx b/frontend/src/pages/Login/Login.jsx index 355a955..1fa57c8 100644 --- a/frontend/src/pages/Login/Login.jsx +++ b/frontend/src/pages/Login/Login.jsx @@ -1,13 +1,15 @@ import { useSiteContext } from '@context/SiteContext' import useLanguage from '@hooks/useLanguage' import { useNavigate } from '@solidjs/router' -import { Field, useFormHandler } from 'solid-form-handler' +import { IconKey, IconUser } from '@tabler/icons-solidjs' +import { useFormHandler } from 'solid-form-handler' import { yupSchema } from 'solid-form-handler/yup' -import { Show, onMount } from 'solid-js' +import { onMount } from 'solid-js' import * as yup from 'yup' import './login.scss' import Logo from '@assets/logo.svg' +import TextInput from '@components/common/TextInput' import useAuth from '@hooks/useAuth' import useToast from '@hooks/useToast' @@ -66,79 +68,18 @@ export default function Login() {

{language.ui.login}

- ( - - )} /> - ( - - )} />
+
+ +
+ + + ) +} diff --git a/frontend/src/pages/Profile/index.js b/frontend/src/pages/Profile/index.js new file mode 100644 index 0000000..aa67a44 --- /dev/null +++ b/frontend/src/pages/Profile/index.js @@ -0,0 +1 @@ +export { default } from './Profile' diff --git a/frontend/src/utils/helper.js b/frontend/src/utils/helper.js index 5f395d3..8f4c3bb 100644 --- a/frontend/src/utils/helper.js +++ b/frontend/src/utils/helper.js @@ -47,4 +47,13 @@ export class Helpers { ? JSON.parse(AES.decrypt(hash, SECRET_KEY).toString(enc.Utf8)) : defaultValue } + + static clearObject = (object) => { + for (var propName in object) { + if (object[propName] === null || object[propName] === undefined) { + delete object[propName] + } + } + return object + } }