qwe
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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) });
|
||||
|
||||
|
||||
@@ -64,10 +64,15 @@ export const MainPage = () => {
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<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>
|
||||
|
||||
<Card size="4">
|
||||
|
||||
172
frontend/social-raiting/src/pages/RegisterPage/RegisterPage.tsx
Normal file
172
frontend/social-raiting/src/pages/RegisterPage/RegisterPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user