qwe
This commit is contained in:
@@ -8,7 +8,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${MYSQL_HOST_PORT}:3306"
|
- "${MYSQL_HOST_PORT}:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- ${MYSQL_VOLUME_NAME}:/var/lib/mysql
|
- volume:/var/lib/mysql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
|
||||||
interval: ${MYSQL_HEALTHCHECK_INTERVAL}
|
interval: ${MYSQL_HEALTHCHECK_INTERVAL}
|
||||||
@@ -54,4 +54,4 @@ services:
|
|||||||
- "${SWAGGER_UI_HOST_PORT}:8080"
|
- "${SWAGGER_UI_HOST_PORT}:8080"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
${MYSQL_VOLUME_NAME}:
|
volume:
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ FROM node:22-alpine AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG VITE_API_BASE_URL=http://localhost:8080/api/
|
|
||||||
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
|
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci
|
RUN npm i
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from "react-router"
|
|||||||
import { isAuthenticated } from "./consts/auth"
|
import { isAuthenticated } from "./consts/auth"
|
||||||
import { LoginPage } from "./pages/LoginPage/LoginPage"
|
import { LoginPage } from "./pages/LoginPage/LoginPage"
|
||||||
import { MainPage } from "./pages/MainPage/MainPage"
|
import { MainPage } from "./pages/MainPage/MainPage"
|
||||||
|
import { RegisterPage } from "./pages/RegisterPage/RegisterPage"
|
||||||
import { UserHistoryPage } from "./pages/UserHistoryPage/UserHistoryPage"
|
import { UserHistoryPage } from "./pages/UserHistoryPage/UserHistoryPage"
|
||||||
|
|
||||||
const ProtectedRoute = ({ children }: { children: ReactNode }) => {
|
const ProtectedRoute = ({ children }: { children: ReactNode }) => {
|
||||||
@@ -33,6 +34,14 @@ function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/register"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<RegisterPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { getValidToken } from "./auth";
|
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) });
|
export const apiInstance = axios.create({ baseURL: baseApiUrl, transformResponse: (r) => JSON.parse(r) });
|
||||||
|
|
||||||
|
|||||||
@@ -64,10 +64,15 @@ export const MainPage = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</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}>
|
<Button variant="soft" color="gray" radius="large" onClick={handleLogout}>
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card size="4">
|
<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