Merge pull request 'feature/profile' (#3) from feature/profile into main
Reviewed-on: sam/fuware#3
This commit is contained in:
commit
59efe83d76
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
}
|
||||
|
39
frontend/src/components/common/TextInput/TextInput.jsx
Normal file
39
frontend/src/components/common/TextInput/TextInput.jsx
Normal file
@ -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 (
|
||||
<Field
|
||||
{...props}
|
||||
mode="input"
|
||||
render={(field) => (
|
||||
<div class="form-control w-full [&:not(:last-child)]:pb-3">
|
||||
<Show when={local.label}>
|
||||
<div class="label">
|
||||
<span class="label-text">{local.label}</span>
|
||||
</div>
|
||||
</Show>
|
||||
<label
|
||||
class="input input-bordered flex items-center gap-2 w-full"
|
||||
classList={{ 'input-error': field.helpers.error }}
|
||||
>
|
||||
<Show when={local.icon}>
|
||||
<Dynamic component={local.icon} size={18} />
|
||||
</Show>
|
||||
<input {...rest} class="grow w-full" {...field.props} />
|
||||
</label>
|
||||
<Show when={field.helpers.error}>
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-red-600">
|
||||
{field.helpers.errorMessage}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
1
frontend/src/components/common/TextInput/index.js
Normal file
1
frontend/src/components/common/TextInput/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './TextInput'
|
@ -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 || {}
|
||||
|
@ -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!",
|
||||
|
@ -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() {
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">{language.ui.login}</h1>
|
||||
<form autoComplete="off" onSubmit={submit}>
|
||||
<Field
|
||||
mode="input"
|
||||
<TextInput
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
icon={IconUser}
|
||||
formHandler={formHandler}
|
||||
render={(field) => (
|
||||
<label class="form-control w-full pb-5">
|
||||
<label
|
||||
class="input input-bordered flex items-center gap-2 w-full"
|
||||
classList={{ 'input-error': field.helpers.error }}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4 opacity-70"
|
||||
>
|
||||
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z" />
|
||||
</svg>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
class="grow w-full"
|
||||
placeholder="Username"
|
||||
{...field.props}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Show when={field.helpers.error}>
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-red-600">
|
||||
{field.helpers.errorMessage}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
<Field
|
||||
mode="input"
|
||||
<TextInput
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
icon={IconKey}
|
||||
formHandler={formHandler}
|
||||
render={(field) => (
|
||||
<label class="form-control w-full">
|
||||
<label
|
||||
class="input input-bordered flex items-center gap-2 w-full"
|
||||
classList={{ 'input-error': field.helpers.error }}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4 opacity-70"
|
||||
>
|
||||
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z" />
|
||||
</svg>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
class="grow w-full"
|
||||
placeholder="Password"
|
||||
{...field.props}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Show when={field.helpers.error}>
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-red-600">
|
||||
{field.helpers.errorMessage}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
</label>
|
||||
)}
|
||||
/>
|
||||
<div class="card-actions justify-end mt-5">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
|
@ -1,3 +0,0 @@
|
||||
export default function Profile() {
|
||||
return <>Profile</>
|
||||
}
|
115
frontend/src/pages/Profile/Profile.jsx
Normal file
115
frontend/src/pages/Profile/Profile.jsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { putUpdateProfile } from '@api/user'
|
||||
import TextInput from '@components/common/TextInput'
|
||||
import { useSiteContext } from '@context/SiteContext'
|
||||
import useLanguage from '@hooks/useLanguage'
|
||||
import useToast from '@hooks/useToast'
|
||||
import { IconKey, IconLetterN, IconUserCircle } from '@tabler/icons-solidjs'
|
||||
import { Helpers } from '@utils/helper'
|
||||
import { useFormHandler } from 'solid-form-handler'
|
||||
import { yupSchema } from 'solid-form-handler/yup'
|
||||
import { createEffect } from 'solid-js'
|
||||
import * as yup from 'yup'
|
||||
|
||||
const profileSchema = yup.object({
|
||||
name: yup.string().required('Name is required'),
|
||||
password: yup.string().nullable().optional(),
|
||||
'confirm-password': yup.string().when('password', {
|
||||
is: (val) => !!(val && val.length > 0),
|
||||
then: (schema) =>
|
||||
schema.oneOf([yup.ref('password'), null], 'Passwords must match'),
|
||||
}),
|
||||
})
|
||||
|
||||
const language = useLanguage()
|
||||
const notify = useToast()
|
||||
|
||||
export default function Profile() {
|
||||
const {
|
||||
store: { userInfo },
|
||||
setUser,
|
||||
} = useSiteContext()
|
||||
const formHandler = useFormHandler(yupSchema(profileSchema))
|
||||
const { formData } = formHandler
|
||||
|
||||
createEffect(() => {
|
||||
formHandler.fillForm({
|
||||
name: userInfo?.name,
|
||||
})
|
||||
})
|
||||
|
||||
const submit = async (event) => {
|
||||
event.preventDefault()
|
||||
await formHandler.validateForm()
|
||||
try {
|
||||
const { name, password } = formData()
|
||||
const clearObj = Helpers.clearObject({
|
||||
name: name || null,
|
||||
password: password || null,
|
||||
})
|
||||
const resp = await putUpdateProfile(clearObj)
|
||||
|
||||
if (resp.status === 200) {
|
||||
setUser(resp.data)
|
||||
formHandler.setFieldValue('password', '')
|
||||
formHandler.setFieldValue('confirm-password', '')
|
||||
notify.success({
|
||||
title: 'Update profile success!',
|
||||
description: 'Your profile has been updated!',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
notify.error({
|
||||
title: 'Update profile fail!',
|
||||
description: error?.data
|
||||
? language.message[error.data]
|
||||
: 'Your username or password input is wrong!',
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="profile">
|
||||
<div class="divider divider-accent text-xl">
|
||||
<span class="text-green-400">
|
||||
<IconUserCircle size={30} />
|
||||
</span>
|
||||
{language.ui.profile}
|
||||
</div>
|
||||
<div class="card w-full bg-base-100 shadow-lg border border-gray-200">
|
||||
<div class="card-body">
|
||||
<form autoComplete="off" onSubmit={submit}>
|
||||
<p class="card-title">{language.ui.changeInfo}</p>
|
||||
<div class="form-content py-5">
|
||||
<TextInput
|
||||
icon={IconLetterN}
|
||||
name="name"
|
||||
placeholder={language.ui.displayName}
|
||||
formHandler={formHandler}
|
||||
/>
|
||||
<TextInput
|
||||
icon={IconKey}
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder={language.ui.newPassword}
|
||||
formHandler={formHandler}
|
||||
/>
|
||||
<TextInput
|
||||
icon={IconKey}
|
||||
name="confirm-password"
|
||||
type="password"
|
||||
placeholder={language.ui.confirmNewPassword}
|
||||
formHandler={formHandler}
|
||||
/>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{language.ui.save}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
1
frontend/src/pages/Profile/index.js
Normal file
1
frontend/src/pages/Profile/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Profile'
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user