This commit is contained in:
2026-04-18 14:56:24 +03:00
parent 6c871cd9eb
commit 0e7c60db6f
6 changed files with 193 additions and 8 deletions

View File

@@ -8,7 +8,7 @@ services:
ports:
- "${MYSQL_HOST_PORT}:3306"
volumes:
- ${MYSQL_VOLUME_NAME}:/var/lib/mysql
- volume:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
interval: ${MYSQL_HEALTHCHECK_INTERVAL}
@@ -54,4 +54,4 @@ services:
- "${SWAGGER_UI_HOST_PORT}:8080"
volumes:
${MYSQL_VOLUME_NAME}:
volume:

View File

@@ -2,11 +2,10 @@ FROM node:22-alpine AS builder
WORKDIR /app
ARG VITE_API_BASE_URL=http://localhost:8080/api/
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
COPY package.json package-lock.json ./
RUN npm ci
RUN npm i
COPY . .
RUN npm run build

View File

@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from "react-router"
import { isAuthenticated } from "./consts/auth"
import { LoginPage } from "./pages/LoginPage/LoginPage"
import { MainPage } from "./pages/MainPage/MainPage"
import { RegisterPage } from "./pages/RegisterPage/RegisterPage"
import { UserHistoryPage } from "./pages/UserHistoryPage/UserHistoryPage"
const ProtectedRoute = ({ children }: { children: ReactNode }) => {
@@ -33,6 +34,14 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/register"
element={
<ProtectedRoute>
<RegisterPage />
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
)

View File

@@ -1,7 +1,7 @@
import axios from "axios";
import { getValidToken } from "./auth";
const baseApiUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:8080/api/"
const baseApiUrl = import.meta.env.VITE_API_BASE_URL || "http://social-rating.nekiiinkognito.ru:8080/api/"
export const apiInstance = axios.create({ baseURL: baseApiUrl, transformResponse: (r) => JSON.parse(r) });

View File

@@ -64,9 +64,14 @@ export const MainPage = () => {
</Text>
</Flex>
<Button variant="soft" color="gray" radius="large" onClick={handleLogout}>
Logout
</Button>
<Flex gap="3" wrap="wrap">
<Button asChild variant="soft" color="pink" radius="large">
<RouterLink to="/register">Register user</RouterLink>
</Button>
<Button variant="soft" color="gray" radius="large" onClick={handleLogout}>
Logout
</Button>
</Flex>
</Flex>
</Card>

View File

@@ -0,0 +1,172 @@
import * as Form from "@radix-ui/react-form"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { Button, Callout, Card, Checkbox, Container, Flex, Heading, Link, Text, TextField } from "@radix-ui/themes"
import { useFormik } from "formik"
import { Link as RouterLink, useNavigate } from "react-router"
import * as yup from "yup"
import { apiInstance } from "../../consts/axios"
const validationSchema = yup.object({
email: yup.string().required().email(),
password: yup.string().required().min(8),
})
export const RegisterPage = () => {
const navigate = useNavigate()
const queryClient = useQueryClient()
const form = useFormik({
validationSchema,
initialValues: {
email: "",
password: "",
isAdmin: false,
},
onSubmit: () => {
registerMutation.mutate()
},
})
const registerMutation = useMutation({
mutationKey: ["register-user"],
mutationFn: async () => {
const response = await apiInstance.post("auth/register", {
email: form.values.email,
password: form.values.password,
isAdmin: form.values.isAdmin,
})
return response.data
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["users"] })
navigate("/", { replace: true })
},
})
const emailError = form.touched.email ? form.errors.email : undefined
const passwordError = form.touched.password ? form.errors.password : undefined
return (
<Container size="3" py="8">
<Card size="4">
<Flex direction="column" gap="5">
<Flex direction="column" gap="2">
<Link asChild color="gray" underline="hover">
<RouterLink to="/">Back to users</RouterLink>
</Link>
<Text size="2" weight="medium" color="pink">
Admin tools
</Text>
<Heading size="7">Register user</Heading>
<Text size="3" color="gray">
Create a new user account. This page uses the protected admin registration endpoint.
</Text>
</Flex>
<Form.Root onSubmit={form.handleSubmit}>
<Flex direction="column" gap="4">
<Form.Field name="email">
<Flex justify="between" align="center" mb="2">
<Form.Label asChild>
<Text size="2" weight="medium">
Email
</Text>
</Form.Label>
{emailError ? (
<Form.Message asChild match={() => !!emailError}>
<Text size="1" color="ruby">
{emailError}
</Text>
</Form.Message>
) : null}
</Flex>
<Form.Control asChild>
<TextField.Root
name="email"
type="email"
placeholder="user@example.com"
value={form.values.email}
onChange={form.handleChange}
onBlur={form.handleBlur}
autoComplete="email"
size="3"
variant="soft"
color={emailError ? "ruby" : undefined}
radius="large"
/>
</Form.Control>
</Form.Field>
<Form.Field name="password">
<Flex justify="between" align="center" mb="2">
<Form.Label asChild>
<Text size="2" weight="medium">
Password
</Text>
</Form.Label>
{passwordError ? (
<Form.Message asChild match={() => !!passwordError}>
<Text size="1" color="ruby">
{passwordError}
</Text>
</Form.Message>
) : null}
</Flex>
<Form.Control asChild>
<TextField.Root
name="password"
type="password"
placeholder="At least 8 characters"
value={form.values.password}
onChange={form.handleChange}
onBlur={form.handleBlur}
autoComplete="new-password"
size="3"
variant="soft"
color={passwordError ? "ruby" : undefined}
radius="large"
/>
</Form.Control>
</Form.Field>
<Flex align="center" gap="3">
<Checkbox
checked={form.values.isAdmin}
onCheckedChange={(checked) => form.setFieldValue("isAdmin", checked === true)}
/>
<Text size="2">Create as admin</Text>
</Flex>
{registerMutation.isError ? (
<Callout.Root color="ruby" variant="soft" size="2">
<Callout.Text>
Failed to register user. Make sure your account has admin access.
</Callout.Text>
</Callout.Root>
) : null}
<Flex gap="3" justify="end" wrap="wrap">
<Button asChild variant="soft" color="gray" radius="large">
<RouterLink to="/">Cancel</RouterLink>
</Button>
<Form.Submit asChild>
<Button
size="3"
radius="large"
loading={registerMutation.isPending}
disabled={!form.isValid || registerMutation.isPending}
>
Register user
</Button>
</Form.Submit>
</Flex>
</Flex>
</Form.Root>
</Flex>
</Card>
</Container>
)
}