Implement Auth provide user info

This commit is contained in:
Takeshi Kimata
2024-05-19 11:14:04 +09:00
parent 0a783bddfd
commit 3d141cf05b
16 changed files with 151 additions and 7 deletions

View File

@@ -0,0 +1,63 @@
import { UserType } from "@/types/user";
import Config from "@/config/config";
const apiServer = Config.apiServer;
async function signUp(newUser: UserType) {
const fetchOptions = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newUser),
};
const url = `${apiServer}/auth/signup`;
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const accessToken = await response.json();
storeToken(accessToken);
return accessToken;
} catch (error: any) {
console.error("Error sign up:", error);
throw error;
}
}
async function signIn(signInUser: UserType) {
const fetchOptions = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(signInUser),
};
const url = `${apiServer}/auth/signin`;
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const accessToken = await response.json();
storeToken(accessToken);
return accessToken;
} catch (error: any) {
console.error("Error sign in:", error);
throw error;
}
}
function storeToken(accessToken: Object) {
localStorage.setItem("testplat-auth-token", JSON.stringify(accessToken));
}
function signOut() {
localStorage.removeItem("testplat-auth-token");
}
export { signUp, signIn, signOut };

View File

@@ -0,0 +1,180 @@
"use client";
import React from "react";
import { useState, useContext } 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 { signUp, signIn } from "./authControl";
import { isValidEmail, isValidPassword } from "./validate";
import { TokenContext } from "../TokenProvider";
type Props = {
isSignup: Boolean;
messages: AuthMessages;
locale: string;
};
export default function AuthPage({ isSignup, messages, locale }: Props) {
const context = useContext(TokenContext);
const [user, setUser] = useState<UserType>({
id: null,
email: "",
password: "",
username: "",
role: roles.findIndex((entry) => entry.uid === "user"),
avatarPath: "",
});
const [confirmPassword, setConfirmPassword] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const togglePasswordVisibility = () =>
setIsPasswordVisible(!isPasswordVisible);
const validate = async () => {
if (!isValidEmail(user.email)) {
setErrorMessage(messages.invalidEmail);
return;
}
if (!isValidPassword(user.password)) {
setErrorMessage(messages.invalidPassword);
return;
}
if (isSignup) {
if (!user.username) {
setErrorMessage(messages.usernameEmpty);
return;
}
if (user.password !== confirmPassword) {
setErrorMessage(messages.passwordDoesNotMatch);
return;
}
}
await submit();
};
const submit = async () => {
if (isSignup) {
try {
const token = await signUp(user);
context.setToken(token);
// Move to signin page
} catch {
setErrorMessage(messages.signupError);
}
} else {
try {
const token = await signIn(user);
context.setToken(token);
// Move to signin page
} catch {
setErrorMessage(messages.signinError);
}
}
};
return (
<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} />}
>
{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

@@ -0,0 +1,27 @@
import { useTranslations } from "next-intl";
import { Input, Button, Card, CardHeader, CardBody } from "@nextui-org/react";
import { Link } from "@/src/navigation";
export default function Page(params: { locale: string }) {
const t = useTranslations("Auth");
return (
<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">{t('account')}</h4>
{/* <Link href={isSignup ? "/auth/signin" : "/auth/signup"} locale={locale}>
<Button
color="primary"
variant="light"
endContent={<ChevronRight size={16} />}
>
{messages.linkTitle}
</Button>
</Link> */}
</CardHeader>
<CardBody className="overflow-visible px-4 pt-0 pb-4">
afgdsfg
</CardBody>
</Card>
);
}

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("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 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 };