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
flexDirection={vertical ? "column" : "row"}
alignItems={vertical ? "stretch" : "flex-start"}
gap={!vertical ? "$3" : undefined}
{...props}
>
<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 FormField from "@/components/ui/form";
import { InputField } from "@/components/ui/input";
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 ThemeSwitcher from "@/components/containers/theme-switcher";
import { useMutation } from "@tanstack/react-query";
import { z } from "zod";
import { ErrorAlert } from "@/components/ui/alert";
import { loginResultSchema, loginSchema } from "./schema";
import api from "@/lib/api";
import { loginSchema } from "./schema";
import Icons from "@/components/ui/icons";
import authStore from "@/stores/auth";
import tamaguiConfig from "@/tamagui.config";
import { useLoginMutation } from "./hooks";
export default function LoginPage() {
const form = useZForm(loginSchema);
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 login = useLoginMutation();
const onSubmit = form.handleSubmit((values) => {
login.mutate(values);
@ -63,7 +48,9 @@ export default function LoginPage() {
}}
>
<Card bordered p="$4" gap="$4">
<Text fontSize="$8">Login</Text>
<Text fontSize="$9" mt="$4">
Login
</Text>
<ErrorAlert error={login.error} />
@ -86,14 +73,37 @@ export default function LoginPage() {
<Separator />
<Button
icon={<Icons name="lock" size={16} />}
icon={<Icons name="login" size={16} />}
onPress={onSubmit}
isLoading={login.isPending}
>
Connect
Login
</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
</Button>
</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(),
});
export type LoginSchema = z.infer<typeof loginSchema>;
export const loginResultSchema = z.object({
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()}
}
func (r *Auth) FindUser(username string) (*models.User, error) {
func (r *Auth) FindUser(username string, email string) (*models.User, error) {
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
}
@ -48,3 +51,10 @@ func (r *Auth) RemoveUserSession(sessionId string, force bool) error {
res := db.Delete(&models.UserSession{ID: sessionId})
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"
"rul.sh/vaulterm/lib"
"rul.sh/vaulterm/middleware"
"rul.sh/vaulterm/models"
"rul.sh/vaulterm/utils"
)
@ -12,6 +13,7 @@ func Router(app *fiber.App) {
router.Post("/login", login)
router.Get("/user", middleware.Protected(), getUser)
router.Post("/register", register)
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 {
return &fiber.Error{
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 {
force := c.Query("force")
sessionId := c.Locals("sessionId").(string)

View File

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