feat: add register page

This commit is contained in:
Khairul Hidayat 2024-11-14 18:50:58 +07:00
parent 836c85351a
commit b574f83e74
11 changed files with 277 additions and 27 deletions

View File

@ -0,0 +1,3 @@
import RegisterPage from "@/pages/auth/register";
export default RegisterPage;

View File

@ -0,0 +1,3 @@
import ResetPasswordPage from "@/pages/auth/reset-password";
export default ResetPasswordPage;

View File

@ -17,6 +17,7 @@ const FormField = ({
<XStack <XStack
flexDirection={vertical ? "column" : "row"} flexDirection={vertical ? "column" : "row"}
alignItems={vertical ? "stretch" : "flex-start"} alignItems={vertical ? "stretch" : "flex-start"}
gap={!vertical ? "$3" : undefined}
{...props} {...props}
> >
<Label htmlFor={htmlFor} w={120} $xs={{ w: 100 }}> <Label htmlFor={htmlFor} w={120} $xs={{ w: 100 }}>

View File

@ -0,0 +1,44 @@
import { useMutation } from "@tanstack/react-query";
import {
loginResultSchema,
LoginSchema,
loginSchema,
RegisterSchema,
} from "./schema";
import authStore from "@/stores/auth";
import { router } from "expo-router";
import api from "@/lib/api";
export const useLoginMutation = () => {
return useMutation({
mutationFn: async (body: LoginSchema) => {
const res = await api("/auth/login", { method: "POST", body });
const { data } = loginResultSchema.safeParse(res);
if (!data) {
throw new Error("Invalid response!");
}
return data;
},
onSuccess(data) {
authStore.setState({ token: data.sessionId });
router.replace("/");
},
});
};
export const useRegisterMutation = () => {
return useMutation({
mutationFn: async (body: RegisterSchema) => {
const res = await api("/auth/register", { method: "POST", body });
const { data } = loginResultSchema.safeParse(res);
if (!data) {
throw new Error("Invalid response!");
}
return data;
},
onSuccess(data) {
authStore.setState({ token: data.sessionId });
router.replace("/");
},
});
};

View File

@ -1,37 +1,22 @@
import { Text, ScrollView, Card, Separator } from "tamagui"; import { Text, ScrollView, Card, Separator, XStack } from "tamagui";
import React from "react"; import React from "react";
import FormField from "@/components/ui/form"; import FormField from "@/components/ui/form";
import { InputField } from "@/components/ui/input"; import { InputField } from "@/components/ui/input";
import { useZForm } from "@/hooks/useZForm"; import { useZForm } from "@/hooks/useZForm";
import { router, Stack } from "expo-router"; import { Link, router, Stack } from "expo-router";
import Button from "@/components/ui/button"; import Button from "@/components/ui/button";
import ThemeSwitcher from "@/components/containers/theme-switcher"; import ThemeSwitcher from "@/components/containers/theme-switcher";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { z } from "zod"; import { z } from "zod";
import { ErrorAlert } from "@/components/ui/alert"; import { ErrorAlert } from "@/components/ui/alert";
import { loginResultSchema, loginSchema } from "./schema"; import { loginSchema } from "./schema";
import api from "@/lib/api";
import Icons from "@/components/ui/icons"; import Icons from "@/components/ui/icons";
import authStore from "@/stores/auth";
import tamaguiConfig from "@/tamagui.config"; import tamaguiConfig from "@/tamagui.config";
import { useLoginMutation } from "./hooks";
export default function LoginPage() { export default function LoginPage() {
const form = useZForm(loginSchema); const form = useZForm(loginSchema);
const login = useLoginMutation();
const login = useMutation({
mutationFn: async (body: z.infer<typeof loginSchema>) => {
const res = await api("/auth/login", { method: "POST", body });
const { data } = loginResultSchema.safeParse(res);
if (!data) {
throw new Error("Invalid response!");
}
return data;
},
onSuccess(data) {
authStore.setState({ token: data.sessionId });
router.replace("/");
},
});
const onSubmit = form.handleSubmit((values) => { const onSubmit = form.handleSubmit((values) => {
login.mutate(values); login.mutate(values);
@ -63,7 +48,9 @@ export default function LoginPage() {
}} }}
> >
<Card bordered p="$4" gap="$4"> <Card bordered p="$4" gap="$4">
<Text fontSize="$8">Login</Text> <Text fontSize="$9" mt="$4">
Login
</Text>
<ErrorAlert error={login.error} /> <ErrorAlert error={login.error} />
@ -86,14 +73,37 @@ export default function LoginPage() {
<Separator /> <Separator />
<Button <Button
icon={<Icons name="lock" size={16} />} icon={<Icons name="login" size={16} />}
onPress={onSubmit} onPress={onSubmit}
isLoading={login.isPending} isLoading={login.isPending}
> >
Connect Login
</Button> </Button>
<Button onPress={() => router.push("/server")} bg="$colorTransparent"> <XStack justifyContent="space-between">
<Text textAlign="center" fontSize="$4">
Not registered yet?{" "}
<Link href="/auth/register" asChild>
<Text cursor="pointer" fontWeight="600">
Register Now.
</Text>
</Link>
</Text>
<Link href="/auth/reset-password" asChild>
<Text cursor="pointer" fontWeight="600" fontSize="$4">
Reset Password
</Text>
</Link>
</XStack>
<Separator w="100%" />
<Button
onPress={() => router.push("/server")}
bg="$colorTransparent"
icon={<Icons name="desktop-classic" size={16} />}
>
Change Server Change Server
</Button> </Button>
</Card> </Card>

View File

@ -0,0 +1,98 @@
import { Text, Separator, Card, ScrollView } from "tamagui";
import React from "react";
import { Link, Stack } from "expo-router";
import Button from "@/components/ui/button";
import Icons from "@/components/ui/icons";
import FormField from "@/components/ui/form";
import { InputField } from "@/components/ui/input";
import { ErrorAlert } from "@/components/ui/alert";
import tamaguiConfig from "@/tamagui.config";
import { useRegisterMutation } from "./hooks";
import { useZForm } from "@/hooks/useZForm";
import { registerSchema } from "./schema";
const RegisterPage = () => {
const form = useZForm(registerSchema);
const register = useRegisterMutation();
const onSubmit = form.handleSubmit((values) => {
register.mutate(values);
});
return (
<>
<Stack.Screen
options={{
title: "Register",
contentStyle: {
width: "100%",
maxWidth: tamaguiConfig.media.xs.maxWidth,
marginHorizontal: "auto",
},
}}
/>
<ScrollView
contentContainerStyle={{
padding: "$4",
pb: "$12",
justifyContent: "center",
flexGrow: 1,
}}
>
<Card bordered p="$4" gap="$4">
<Text fontSize="$9" mt="$4">
Register new account.
</Text>
<ErrorAlert error={register.error} />
<FormField label="Full Name">
<InputField form={form} name="name" />
</FormField>
<FormField label="Username">
<InputField form={form} name="username" />
</FormField>
<FormField label="Email Address">
<InputField form={form} name="email" />
</FormField>
<FormField label="Password">
<InputField form={form} name="password" secureTextEntry />
</FormField>
<FormField label="Confirm Password">
<InputField
form={form}
name="confirmPassword"
secureTextEntry
onSubmitEditing={onSubmit}
/>
</FormField>
<Separator />
<Button
icon={<Icons name="account-plus" size={16} />}
onPress={onSubmit}
isLoading={register.isPending}
>
Register
</Button>
<Text textAlign="center" fontSize="$4">
Already registered?{" "}
<Link href="/auth/login" replace asChild>
<Text cursor="pointer" fontWeight="600">
Login Now.
</Text>
</Link>
</Text>
</Card>
</ScrollView>
</>
);
};
export default RegisterPage;

View File

@ -0,0 +1,12 @@
import { View, Text } from "react-native";
import React from "react";
const ResetPasswordPage = () => {
return (
<View>
<Text>ResetPasswordPage</Text>
</View>
);
};
export default ResetPasswordPage;

View File

@ -5,6 +5,23 @@ export const loginSchema = z.object({
password: z.string(), password: z.string(),
}); });
export type LoginSchema = z.infer<typeof loginSchema>;
export const loginResultSchema = z.object({ export const loginResultSchema = z.object({
sessionId: z.string().min(40), sessionId: z.string().min(40),
}); });
export const registerSchema = z
.object({
name: z.string().min(3),
username: z.string().min(3),
email: z.string().email(),
password: z.string().min(3),
confirmPassword: z.string().min(3),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
export type RegisterSchema = z.infer<typeof registerSchema>;

View File

@ -13,9 +13,12 @@ func NewRepository() *Auth {
return &Auth{db: db.Get()} return &Auth{db: db.Get()}
} }
func (r *Auth) FindUser(username string) (*models.User, error) { func (r *Auth) FindUser(username string, email string) (*models.User, error) {
var user models.User var user models.User
ret := r.db.Where("username = ? OR email = ?", username, username).First(&user) if email == "" {
email = username
}
ret := r.db.Where("username = ? OR email = ?", username, email).First(&user)
return &user, ret.Error return &user, ret.Error
} }
@ -48,3 +51,10 @@ func (r *Auth) RemoveUserSession(sessionId string, force bool) error {
res := db.Delete(&models.UserSession{ID: sessionId}) res := db.Delete(&models.UserSession{ID: sessionId})
return res.Error return res.Error
} }
func (r *Auth) CreateUser(user *models.User) (string, error) {
if err := r.db.Create(user).Error; err != nil {
return "", err
}
return r.CreateUserSession(user)
}

View File

@ -4,6 +4,7 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"rul.sh/vaulterm/lib" "rul.sh/vaulterm/lib"
"rul.sh/vaulterm/middleware" "rul.sh/vaulterm/middleware"
"rul.sh/vaulterm/models"
"rul.sh/vaulterm/utils" "rul.sh/vaulterm/utils"
) )
@ -12,6 +13,7 @@ func Router(app *fiber.App) {
router.Post("/login", login) router.Post("/login", login)
router.Get("/user", middleware.Protected(), getUser) router.Get("/user", middleware.Protected(), getUser)
router.Post("/register", register)
router.Post("/logout", middleware.Protected(), logout) router.Post("/logout", middleware.Protected(), logout)
} }
@ -26,7 +28,7 @@ func login(c *fiber.Ctx) error {
} }
} }
user, err := repo.FindUser(body.Username) user, err := repo.FindUser(body.Username, "")
if err != nil { if err != nil {
return &fiber.Error{ return &fiber.Error{
Code: fiber.StatusUnauthorized, Code: fiber.StatusUnauthorized,
@ -71,6 +73,49 @@ func getUser(c *fiber.Ctx) error {
}) })
} }
func register(c *fiber.Ctx) error {
repo := NewRepository()
var body RegisterSchema
if err := c.BodyParser(&body); err != nil {
return &fiber.Error{
Code: fiber.StatusBadRequest,
Message: err.Error(),
}
}
exist, _ := repo.FindUser(body.Username, body.Email)
if exist.ID != "" {
return &fiber.Error{
Code: fiber.StatusBadRequest,
Message: "Username or email already exists",
}
}
password, err := lib.HashPassword(body.Password)
if err != nil {
return utils.ResponseError(c, err, 500)
}
user := &models.User{
Name: body.Name,
Username: body.Username,
Email: body.Email,
Password: password,
Role: models.UserRoleUser,
}
sessionId, err := repo.CreateUser(user)
if err != nil {
return utils.ResponseError(c, err, 500)
}
return c.JSON(fiber.Map{
"user": user,
"sessionId": sessionId,
})
}
func logout(c *fiber.Ctx) error { func logout(c *fiber.Ctx) error {
force := c.Query("force") force := c.Query("force")
sessionId := c.Locals("sessionId").(string) sessionId := c.Locals("sessionId").(string)

View File

@ -18,3 +18,10 @@ type GetUserResult struct {
middleware.AuthUser middleware.AuthUser
Teams []TeamWithRole `json:"teams"` Teams []TeamWithRole `json:"teams"`
} }
type RegisterSchema struct {
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}