backend for queues & minor front tweaks

This commit is contained in:
2024-04-11 16:39:08 +03:00
parent 8b8124d58d
commit 57965fc147
21 changed files with 210 additions and 35 deletions

View File

@ -14,3 +14,21 @@ class User(Base):
username = Column(String(length=24), unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
owns_queues = relationship("Queue", backref="owner", lazy="dynamic")
class AnonymousUser(Base):
__tablename__ = "anonymoususers"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, index=True)
class Queue(Base):
__tablename__ = "queues"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, index=True)
description = Column(String, index=True)
owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id"))

View File

@ -6,10 +6,12 @@ from .db.database import SessionLocal, engine
from .dependencies import get_db
from .views.auth.api import router as auth_router
from .views.queue.api import router as queue_router
app = FastAPI(dependencies=[Depends(get_db)])
models.Base.metadata.create_all(bind=engine)
app.include_router(queue_router)
app.include_router(auth_router)

View File

@ -68,10 +68,3 @@ async def read_users_me(
current_user: Annotated[schemas.User, Depends(services.get_current_active_user)],
):
return current_user
# @app.get("/users/me/items/")
# async def read_own_items(
# current_user: Annotated[User, Depends(get_current_active_user)],
# ):
# return [{"item_id": "Foo", "owner": current_user.username}]

View File

@ -1,5 +1,6 @@
from typing import Union
from pydantic import BaseModel
from uuid import UUID
class User(BaseModel):
@ -8,7 +9,7 @@ class User(BaseModel):
class UserInDB(User):
hashed_password: str
id: UUID
class Config:
from_attributes = True

View File

@ -20,15 +20,15 @@ def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
def get_password_hash(password) -> str:
return pwd_context.hash(password)
def get_user_by_id(db: Session, user_id: uuid.uuid4):
def get_user_by_id(db: Session, user_id: uuid.uuid4) -> models.User:
return db.query(models.User).filter(models.User.id == user_id).first()
def get_user_by_username(db: Session, username: int):
def get_user_by_username(db: Session, username: int) -> models.User:
return db.query(models.User).filter(models.User.username == username).first()
@ -62,15 +62,13 @@ def create_user(db: Session, user_data: schemas.UserRegister) -> schemas.UserInD
)
db.add(user)
db.commit()
return schemas.UserInDB(
username=user.username, name=user.name, hashed_password=user.hashed_password
)
return schemas.UserInDB.model_validate(user)
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: Annotated[Session, Depends(get_db)],
) -> schemas.User:
) -> schemas.UserInDB:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",

View File

View File

@ -0,0 +1,41 @@
from datetime import datetime, timedelta, timezone
from typing import Annotated, Union
from sqlalchemy.orm import Session
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
from ...config import jwt_config
from ...dependencies import get_db
from . import schemas
from . import services
from ..auth import services as auth_services
from ..auth import schemas as auth_schemas
router = APIRouter(
prefix="/queue",
tags=["queue"],
dependencies=[Depends(get_db)],
responses={404: {"description": "Not found"}},
)
@router.get("/")
async def user_queues_list(
queues: Annotated[schemas.Queue, Depends(services.get_user_queues)],
) -> list[schemas.QueueInDb]:
return queues
@router.post("/")
async def create_queue(
new_queue: schemas.Queue,
current_user: Annotated[auth_schemas.User, Depends(auth_services.get_current_user)],
db: Annotated[Session, Depends(get_db)],
) -> schemas.QueueInDb:
return services.create_queue(new_queue=new_queue, current_user=current_user, db=db)

View File

@ -0,0 +1,27 @@
from typing import Union
from pydantic import BaseModel
from uuid import UUID
class ParticipantInfo(BaseModel):
total: int
remaining: int
class Queue(BaseModel):
name: str
description: Union[str, None] = None
class QueueInList(Queue):
participants: ParticipantInfo
class Config:
from_attributes = True
class QueueInDb(Queue):
id: UUID
class Config:
from_attributes = True

View File

@ -0,0 +1,30 @@
from fastapi import Depends
from typing import Annotated
from sqlalchemy.orm import Session
from ...dependencies import get_db
from ...db import models
from ..auth import services as auth_services
from ..auth import schemas as auth_schemas
from . import schemas
def get_user_queues(
current_user: Annotated[auth_schemas.User, Depends(auth_services.get_current_user)]
) -> list[schemas.QueueInDb]:
return [schemas.QueueInDb.model_validate(q) for q in current_user.owns_queues]
def create_queue(
new_queue: schemas.Queue,
current_user: auth_schemas.UserInDB,
db: Session,
) -> schemas.QueueInDb:
q = models.Queue(
name=new_queue.name, description=new_queue.description, owner_id=current_user.id
)
db.add(q)
db.commit()
return schemas.QueueInDb.model_validate(q)

View File

@ -11,6 +11,7 @@
"@ant-design/icons": "^5.3.5",
"@reduxjs/toolkit": "^2.2.2",
"antd": "^5.15.4",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
@ -3051,7 +3052,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -3336,7 +3336,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@ -3346,8 +3345,7 @@
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/punycode": {
"version": "2.3.1",

View File

@ -11,6 +11,7 @@
"@ant-design/icons": "^5.3.5",
"@reduxjs/toolkit": "^2.2.2",
"antd": "^5.15.4",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",

View File

@ -1,10 +1,15 @@
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
export default defineConfig({
plugins: [pluginReact()],
server: {
host: '0.0.0.0',
host: "0.0.0.0",
port: 3000,
},
html: {
favicon: "./static/logo-square.png",
title: "queueful!",
template: "./static/index.html",
},
});

View File

@ -21,8 +21,9 @@ const App = () => {
<MessageContext.Provider value={messageApi}>
<div className="content">
{contextHolder}
<HeaderComponent />
<AppRoutes />
<AppRoutes>
<HeaderComponent />
</AppRoutes>
</div>
</MessageContext.Provider>
</ConfigProvider>

View File

@ -1,11 +1,17 @@
import { LogoutOutlined, UserOutlined } from "@ant-design/icons";
import { Button, Layout, Menu, MenuProps, Popover, Tooltip } from "antd";
import React, { useEffect, useState } from "react";
import {
DesktopOutlined,
LogoutOutlined,
SettingOutlined,
UserOutlined,
} from "@ant-design/icons";
import { Layout, Menu, MenuProps, Popover } from "antd";
import React, { useState } from "react";
import AuthModal from "./AuthModal";
import "./styles.css";
import { StorePrototype, logOut, store } from "../config/store";
import { useSelector } from "react-redux";
import tr from "../config/translation";
import { Link } from "react-router-dom";
const { Header } = Layout;
@ -20,15 +26,27 @@ const HeaderComponent = () => {
);
const userMenuItems: MenuProps["items"] = [
{
label: <Link to="/settings">{tr("Settings")}</Link>,
key: "settings",
icon: <SettingOutlined />,
},
{
label: tr("Log out"),
key: "logout",
icon: <LogoutOutlined />,
danger: true,
onClick: () => store.dispatch(logOut()),
},
];
const items: MenuProps["items"] = [
{
label: <Link to={user ? "/dashboard" : "#"}>{tr("Dashboard")}</Link>,
key: "dashboard",
icon: <DesktopOutlined />,
disabled: !user,
},
{
label: user ? (
<Popover

View File

@ -1 +1 @@
export const baseUrl = "http://localhost/api";
export const baseUrl = `${window.location.protocol}//${window.location.host}/api`;

View File

@ -1,19 +1,25 @@
import React from "react";
import React, { ReactNode } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import MainPage from "./MainPage";
import { getLocalToken, loadLanguage, store } from "../config/store";
import DashboardPage from "./DashboardPage";
import PropTypes from "prop-types";
const AppRoutes = () => {
const AppRoutes = ({ children }: { children: ReactNode }) => {
store.dispatch(getLocalToken());
store.dispatch(loadLanguage());
return (
<BrowserRouter>
{children}
<Routes>
<Route path="/" Component={MainPage} />
<Route path="/" element={<MainPage />} />
<Route path="/dashboard" element={<DashboardPage />} />
</Routes>
</BrowserRouter>
);
};
AppRoutes.propTypes = {
children: PropTypes.node,
};
export default AppRoutes;

View File

@ -0,0 +1,22 @@
import React from "react";
import "./styles.css";
import logoSquare from "../../static/logo-square.png";
import { Button } from "antd";
import tr from "../config/translation";
const DashboardPage = () => {
return (
<div className="card main">
<img src={logoSquare} alt="logo" className="image" />
<p style={{ fontSize: "2rem" }}>
{tr("Queuing has never been so simple")}
</p>
<div className="button-box">
<Button>{tr("Join a queue")}</Button>
<Button>{tr("Take a tour")}</Button>
</div>
</div>
);
};
export default DashboardPage;

View File

@ -1,13 +1,13 @@
import React from "react";
import "./styles.css";
import logoSquare from "../../static/logo-square.png";
import logo from "../../static/logo-full.png";
import { Button } from "antd";
import tr from "../config/translation";
const MainPage = () => {
return (
<div className="card main">
<img src={logoSquare} alt="logo" className="image" />
<img src={logo} alt="logo" className="image" />
<p style={{ fontSize: "2rem" }}>
{tr("Queuing has never been so simple")}
</p>

View File

@ -14,7 +14,7 @@
}
.image {
height: 10vw;
height: 100px;
width: auto;
}

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Comfortaa:wght@300..700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="<%= mountId %>"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB