Implement Auth
This commit is contained in:
@@ -1,25 +1,20 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Input, Button } from "@nextui-org/react";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Input, Button, Card, CardHeader, CardBody } from "@nextui-org/react";
|
||||
import { Link } from "@/src/navigation";
|
||||
import { ChevronRight, Eye, EyeOff } from "lucide-react";
|
||||
import { UserType, AuthMessages } from "@/types/user";
|
||||
import { roles } from "@/config/selection";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { signUp, signIn } from "./authControl";
|
||||
import { isValidEmail, isValidPassword } from "./validate";
|
||||
type Props = {
|
||||
isSignup: Boolean;
|
||||
messages: AuthMessages;
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export default function AuthPage({ messages, locale }: Props) {
|
||||
const [isSignup, setIsSignup] = useState(false);
|
||||
const pathname = usePathname();
|
||||
console.log(pathname);
|
||||
if (pathname.includes("signup")) {
|
||||
setIsSignup(true);
|
||||
}
|
||||
|
||||
export default function AuthPage({ isSignup, messages, locale }: Props) {
|
||||
const [user, setUser] = useState<UserType>({
|
||||
id: null,
|
||||
email: "",
|
||||
@@ -28,6 +23,7 @@ export default function AuthPage({ messages, locale }: Props) {
|
||||
role: roles.findIndex((entry) => entry.uid === "user"),
|
||||
avatarPath: "",
|
||||
});
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
@@ -35,19 +31,26 @@ export default function AuthPage({ messages, locale }: Props) {
|
||||
setIsPasswordVisible(!isPasswordVisible);
|
||||
|
||||
const validate = async () => {
|
||||
if (!user.email) {
|
||||
setErrorMessage("email can't be empty");
|
||||
if (!isValidEmail(user.email)) {
|
||||
setErrorMessage(messages.invalidEmail);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.password) {
|
||||
setErrorMessage("password can't be empty");
|
||||
if (!isValidPassword(user.password)) {
|
||||
setErrorMessage(messages.invalidPassword);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.username) {
|
||||
setErrorMessage("username can't be empty");
|
||||
return;
|
||||
if (isSignup) {
|
||||
if (!user.username) {
|
||||
setErrorMessage(messages.usernameEmpty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.password !== confirmPassword) {
|
||||
setErrorMessage(messages.passwordDoesNotMatch);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await submit();
|
||||
@@ -59,108 +62,109 @@ export default function AuthPage({ messages, locale }: Props) {
|
||||
console.log(signUpRet);
|
||||
// if success, move to signin page
|
||||
} else {
|
||||
const signInRet = await signUp(user);
|
||||
const signInRet = await signIn(user);
|
||||
console.log(signInRet);
|
||||
// if success, move to account page
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
|
||||
<Input
|
||||
isRequired
|
||||
type="email"
|
||||
label="Email"
|
||||
onChange={(e) => {
|
||||
setUser({
|
||||
...user,
|
||||
email: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
isRequired
|
||||
type="username"
|
||||
label="User Name"
|
||||
onChange={(e) => {
|
||||
setUser({
|
||||
...user,
|
||||
username: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
variant="bordered"
|
||||
placeholder="Enter your password"
|
||||
endContent={
|
||||
<button
|
||||
className="focus:outline-none"
|
||||
type="button"
|
||||
onClick={togglePasswordVisibility}
|
||||
<Card className="border-none bg-background/60 dark:bg-default-100/50 w-[480px]">
|
||||
<CardHeader className="px-4 pt-4 pb-0 flex justify-between">
|
||||
<h4 className="font-bold text-large">{messages.title}</h4>
|
||||
<Link href={isSignup ? "/auth/signin" : "/auth/signup"} locale={locale}>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="light"
|
||||
endContent={<ChevronRight size={16} />}
|
||||
>
|
||||
{isPasswordVisible ? <Eye size={24} /> : <EyeOff size={24} />}
|
||||
</button>
|
||||
}
|
||||
type={isPasswordVisible ? "text" : "password"}
|
||||
onChange={(e) => {
|
||||
setUser({
|
||||
...user,
|
||||
password: e.target.value,
|
||||
});
|
||||
}}
|
||||
className="max-w-xs"
|
||||
/>
|
||||
<Button color="primary" onPress={validate}>
|
||||
{isSignup ? messages.signup : messages.signin}
|
||||
</Button>
|
||||
</div>
|
||||
// <Modal
|
||||
// isOpen={isOpen}
|
||||
// onOpenChange={() => {
|
||||
// onCancel();
|
||||
// }}
|
||||
// >
|
||||
// <ModalContent>
|
||||
// <ModalHeader className="flex flex-col gap-1">
|
||||
// {messages.project}
|
||||
// </ModalHeader>
|
||||
// <ModalBody>
|
||||
// <Input
|
||||
// type="text"
|
||||
// label={messages.projectName}
|
||||
// value={projectName.text}
|
||||
// isInvalid={projectName.isValid}
|
||||
// errorMessage={projectName.errorMessage}
|
||||
// onChange={(e) => {
|
||||
// setProjectName({
|
||||
// ...projectName,
|
||||
// text: e.target.value,
|
||||
// });
|
||||
// }}
|
||||
// />
|
||||
// <Textarea
|
||||
// label={messages.projectDetail}
|
||||
// value={projectDetail.text}
|
||||
// isInvalid={projectDetail.isValid}
|
||||
// errorMessage={projectDetail.errorMessage}
|
||||
// onChange={(e) => {
|
||||
// setProjectDetail({
|
||||
// ...projectDetail,
|
||||
// text: e.target.value,
|
||||
// });
|
||||
// }}
|
||||
// />
|
||||
// </ModalBody>
|
||||
// <ModalFooter>
|
||||
// <Button variant="light" onPress={onCancel}>
|
||||
// {messages.close}
|
||||
// </Button>
|
||||
// <Button color="primary" onPress={validate}>
|
||||
// {editingProject ? messages.update : messages.create}
|
||||
// </Button>
|
||||
// </ModalFooter>
|
||||
// </ModalContent>
|
||||
// </Modal>
|
||||
{messages.linkTitle}
|
||||
</Button>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardBody className="overflow-visible px-4 pt-0 pb-4">
|
||||
<form>
|
||||
{errorMessage && (
|
||||
<div className="my-3 text-danger">{errorMessage}</div>
|
||||
)}
|
||||
<Input
|
||||
isRequired
|
||||
type="email"
|
||||
label={messages.email}
|
||||
autoComplete="email"
|
||||
className="mt-3"
|
||||
onChange={(e) => {
|
||||
setUser({
|
||||
...user,
|
||||
email: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{isSignup && (
|
||||
<Input
|
||||
isRequired
|
||||
type="username"
|
||||
label={messages.username}
|
||||
autoComplete="username"
|
||||
className="mt-3"
|
||||
onChange={(e) => {
|
||||
setUser({
|
||||
...user,
|
||||
username: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
label={messages.password}
|
||||
variant="bordered"
|
||||
autoComplete={isSignup ? "new-password" : "current-password"}
|
||||
className="mt-3"
|
||||
type={isPasswordVisible ? "text" : "password"}
|
||||
endContent={
|
||||
<button
|
||||
className="focus:outline-none"
|
||||
type="button"
|
||||
onClick={togglePasswordVisibility}
|
||||
>
|
||||
{isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />}
|
||||
</button>
|
||||
}
|
||||
onChange={(e) => {
|
||||
setUser({
|
||||
...user,
|
||||
password: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{isSignup && (
|
||||
<Input
|
||||
label={messages.confirmPassword}
|
||||
variant="bordered"
|
||||
autoComplete="new-password"
|
||||
className="mt-3"
|
||||
type={isPasswordVisible ? "text" : "password"}
|
||||
endContent={
|
||||
<button
|
||||
className="focus:outline-none"
|
||||
type="button"
|
||||
onClick={togglePasswordVisibility}
|
||||
>
|
||||
{isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />}
|
||||
</button>
|
||||
}
|
||||
onChange={(e) => {
|
||||
setConfirmPassword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button color="primary" className="mt-3" onPress={validate}>
|
||||
{messages.submitTitle}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
11
frontend/src/app/[locale]/auth/layout.tsx
Normal file
11
frontend/src/app/[locale]/auth/layout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex items-center justify-center">{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,12 +4,25 @@ import { useTranslations } from "next-intl";
|
||||
export default function Page(params: { locale: string }) {
|
||||
const t = useTranslations("Auth");
|
||||
const messages = {
|
||||
signup: t("projectList"),
|
||||
signin: t("project"),
|
||||
title: t("signin"),
|
||||
linkTitle: t("or_signup"),
|
||||
submitTitle: t("signin"),
|
||||
email: t("email"),
|
||||
username: t("username"),
|
||||
password: t("password"),
|
||||
confirmPassword: t("confirm_password"),
|
||||
invalidEmail: t("invalid_email"),
|
||||
invalidPassword: t("invalid_password"),
|
||||
usernameEmpty: t("username_empty"),
|
||||
passwordDoesNotMatch: t("password_not_match"),
|
||||
EmailAlreadyExist: t("email_already_exist"),
|
||||
emailNotExist: t("email_not_exist"),
|
||||
signupError: t("signup_error"),
|
||||
signinError: t("signin_error"),
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<AuthPage messages={messages} locale={params.locale} />
|
||||
<AuthPage isSignup={false} messages={messages} locale={params.locale} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import AuthPage from "../authPage";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function Page(params: { locale: string }) {
|
||||
const t = useTranslations("Auth");
|
||||
const messages = {
|
||||
title: t("signup"),
|
||||
linkTitle: t("or_signin"),
|
||||
submitTitle: t("signup"),
|
||||
email: t("email"),
|
||||
username: t("username"),
|
||||
password: t("password"),
|
||||
confirmPassword: t("confirm_password"),
|
||||
invalidEmail: t("invalid_email"),
|
||||
invalidPassword: t("invalid_password"),
|
||||
usernameEmpty: t("username_empty"),
|
||||
passwordDoesNotMatch: t("password_not_match"),
|
||||
EmailAlreadyExist: t("email_already_exist"),
|
||||
emailNotExist: t("email_not_exist"),
|
||||
signupError: t("signup_error"),
|
||||
signinError: t("signin_error"),
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<AuthPage isSignup={true} messages={messages} locale={params.locale} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
24
frontend/src/app/[locale]/auth/validate.test.ts
Normal file
24
frontend/src/app/[locale]/auth/validate.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { isValidEmail, isValidPassword } from "./validate";
|
||||
|
||||
test("validate email", () => {
|
||||
const email1 = "aaa@gmail.com";
|
||||
expect(isValidEmail(email1)).toBe(true);
|
||||
|
||||
const email2 = "gmail.com";
|
||||
expect(isValidEmail(email2)).toBe(false);
|
||||
|
||||
const email23 = "";
|
||||
expect(isValidEmail(email23)).toBe(false);
|
||||
});
|
||||
|
||||
test("validate password", () => {
|
||||
const pass1 = "aaaaaaaa";
|
||||
expect(isValidPassword(pass1)).toBe(true);
|
||||
|
||||
const pass2 = "abcdefg";
|
||||
expect(isValidPassword(pass2)).toBe(false);
|
||||
|
||||
const pass3 = "";
|
||||
expect(isValidPassword(pass3)).toBe(false);
|
||||
});
|
||||
19
frontend/src/app/[locale]/auth/validate.ts
Normal file
19
frontend/src/app/[locale]/auth/validate.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
function isValidEmail(email: string) {
|
||||
const validRegex =
|
||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
if (email.match(validRegex)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidPassword(password: string) {
|
||||
if (password.length > 7) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { isValidEmail, isValidPassword };
|
||||
Reference in New Issue
Block a user