Implement Auth

This commit is contained in:
Takeshi Kimata
2024-05-18 13:42:58 +09:00
parent 8103297dae
commit 92a4c30ce2
11 changed files with 269 additions and 125 deletions

View File

@@ -26,7 +26,22 @@
},
"Auth": {
"signup": "Sign up",
"signin": "Sign in"
"signin": "Sign in",
"or_signup": "You need sign up?",
"or_signin": "You need sign in?",
"email": "Email",
"username": "User name",
"password": "Password",
"confirm_password": "Password (confirm)",
"invalid_email": "Invalid email",
"invalid_password": "Password must be at least 8 characters",
"username_empty": "username is empty",
"password_empty": "password is empty",
"password_not_match": "Pasword does not match",
"email_already_exist": "Already registered email",
"email_not_exist": "Email address not found",
"signup_error": "Sign up failed",
"signin_error": "Sign in failed"
},
"Projects": {
"projectList": "Project List",

View File

@@ -25,8 +25,22 @@
"projects": "プロジェクト"
},
"Auth": {
"signup": "サインアップ",
"signin": "サインイン"
"signup": "新規登録",
"signin": "サインイン",
"or_signup": "新規登録ページ",
"or_signin": "サインインページ",
"email": "メールアドレス",
"username": "ユーザー名",
"password": "パスワード",
"confirm_password": "パスワード(確認)",
"invalid_email": "無効なメールアドレスです",
"invalid_password": "パスワードは8文字以上である必要があります",
"username_empty": "ユーザー名が入力されていません",
"password_not_match": "パスワードが一致しません",
"email_already_exist": "登録済みのメールアドレスです",
"email_not_exist": "メールアドレスが見つかりません",
"signup_error": "新規登録できません",
"signin_error": "サインインできません"
},
"Projects": {
"projectList": "プロジェクト一覧",

View File

@@ -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>
);
}

View 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>
</>
);
}

View File

@@ -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} />
</>
);
}

View File

@@ -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} />
</>
);
}

View 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);
});

View 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 };

View File

@@ -8,6 +8,19 @@ export type UserType = {
};
export type AuthMessages = {
signup: string;
signin: string;
title: string;
linkTitle: string;
submitTitle: string;
email: string;
username: string;
password: string;
confirmPassword: string;
invalidEmail: string;
invalidPassword: string;
usernameEmpty: string;
passwordDoesNotMatch: string;
EmailAlreadyExist: string;
emailNotExist: string;
signupError: string;
signinError: string;
};