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

@@ -2,11 +2,13 @@ const express = require("express");
const router = express.Router(); const router = express.Router();
const defineUser = require("../../models/users"); const defineUser = require("../../models/users");
const { DataTypes } = require("sequelize"); const { DataTypes } = require("sequelize");
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const User = defineUser(sequelize, DataTypes); const User = defineUser(sequelize, DataTypes);
router.post("/auth/signin", async (req, res) => { router.post("/signin", async (req, res) => {
try { try {
const { email, password } = req.body; const { email, password } = req.body;
const user = await User.findOne({ const user = await User.findOne({

View File

@@ -3,11 +3,12 @@ const router = express.Router();
const defineUser = require("../../models/users"); const defineUser = require("../../models/users");
const { DataTypes } = require("sequelize"); const { DataTypes } = require("sequelize");
const roles = require("./roles"); const roles = require("./roles");
const bcrypt = require('bcrypt');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const User = defineUser(sequelize, DataTypes); const User = defineUser(sequelize, DataTypes);
router.post("/auth/signup", async (req, res) => { router.post("/signup", async (req, res) => {
try { try {
const { email, password, username } = req.body; const { email, password, username } = req.body;
const hashedPassword = await bcrypt.hash(password, 10); const hashedPassword = await bcrypt.hash(password, 10);

View File

@@ -26,7 +26,22 @@
}, },
"Auth": { "Auth": {
"signup": "Sign up", "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": { "Projects": {
"projectList": "Project List", "projectList": "Project List",

View File

@@ -25,8 +25,22 @@
"projects": "プロジェクト" "projects": "プロジェクト"
}, },
"Auth": { "Auth": {
"signup": "サインアップ", "signup": "新規登録",
"signin": "サインイン" "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": { "Projects": {
"projectList": "プロジェクト一覧", "projectList": "プロジェクト一覧",

View File

@@ -1,25 +1,20 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { useState, useEffect } from "react"; import { useState } from "react";
import { Input, Button } from "@nextui-org/react"; import { Input, Button, Card, CardHeader, CardBody } from "@nextui-org/react";
import { Eye, EyeOff } from "lucide-react"; import { Link } from "@/src/navigation";
import { ChevronRight, Eye, EyeOff } from "lucide-react";
import { UserType, AuthMessages } from "@/types/user"; import { UserType, AuthMessages } from "@/types/user";
import { roles } from "@/config/selection"; import { roles } from "@/config/selection";
import { usePathname } from "next/navigation";
import { signUp, signIn } from "./authControl"; import { signUp, signIn } from "./authControl";
import { isValidEmail, isValidPassword } from "./validate";
type Props = { type Props = {
isSignup: Boolean;
messages: AuthMessages; messages: AuthMessages;
locale: string; locale: string;
}; };
export default function AuthPage({ messages, locale }: Props) { export default function AuthPage({ isSignup, messages, locale }: Props) {
const [isSignup, setIsSignup] = useState(false);
const pathname = usePathname();
console.log(pathname);
if (pathname.includes("signup")) {
setIsSignup(true);
}
const [user, setUser] = useState<UserType>({ const [user, setUser] = useState<UserType>({
id: null, id: null,
email: "", email: "",
@@ -28,6 +23,7 @@ export default function AuthPage({ messages, locale }: Props) {
role: roles.findIndex((entry) => entry.uid === "user"), role: roles.findIndex((entry) => entry.uid === "user"),
avatarPath: "", avatarPath: "",
}); });
const [confirmPassword, setConfirmPassword] = useState("");
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const [isPasswordVisible, setIsPasswordVisible] = useState(false); const [isPasswordVisible, setIsPasswordVisible] = useState(false);
@@ -35,19 +31,26 @@ export default function AuthPage({ messages, locale }: Props) {
setIsPasswordVisible(!isPasswordVisible); setIsPasswordVisible(!isPasswordVisible);
const validate = async () => { const validate = async () => {
if (!user.email) { if (!isValidEmail(user.email)) {
setErrorMessage("email can't be empty"); setErrorMessage(messages.invalidEmail);
return; return;
} }
if (!user.password) { if (!isValidPassword(user.password)) {
setErrorMessage("password can't be empty"); setErrorMessage(messages.invalidPassword);
return; return;
} }
if (!user.username) { if (isSignup) {
setErrorMessage("username can't be empty"); if (!user.username) {
return; setErrorMessage(messages.usernameEmpty);
return;
}
if (user.password !== confirmPassword) {
setErrorMessage(messages.passwordDoesNotMatch);
return;
}
} }
await submit(); await submit();
@@ -59,108 +62,109 @@ export default function AuthPage({ messages, locale }: Props) {
console.log(signUpRet); console.log(signUpRet);
// if success, move to signin page // if success, move to signin page
} else { } else {
const signInRet = await signUp(user); const signInRet = await signIn(user);
console.log(signInRet); console.log(signInRet);
// if success, move to account page // if success, move to account page
} }
}; };
return ( return (
<div className="flex w-full flex-wrap md:flex-nowrap gap-4"> <Card className="border-none bg-background/60 dark:bg-default-100/50 w-[480px]">
<Input <CardHeader className="px-4 pt-4 pb-0 flex justify-between">
isRequired <h4 className="font-bold text-large">{messages.title}</h4>
type="email" <Link href={isSignup ? "/auth/signin" : "/auth/signup"} locale={locale}>
label="Email" <Button
onChange={(e) => { color="primary"
setUser({ variant="light"
...user, endContent={<ChevronRight size={16} />}
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}
> >
{isPasswordVisible ? <Eye size={24} /> : <EyeOff size={24} />} {messages.linkTitle}
</button> </Button>
} </Link>
type={isPasswordVisible ? "text" : "password"} </CardHeader>
onChange={(e) => { <CardBody className="overflow-visible px-4 pt-0 pb-4">
setUser({ <form>
...user, {errorMessage && (
password: e.target.value, <div className="my-3 text-danger">{errorMessage}</div>
}); )}
}} <Input
className="max-w-xs" isRequired
/> type="email"
<Button color="primary" onPress={validate}> label={messages.email}
{isSignup ? messages.signup : messages.signin} autoComplete="email"
</Button> className="mt-3"
</div> onChange={(e) => {
// <Modal setUser({
// isOpen={isOpen} ...user,
// onOpenChange={() => { email: e.target.value,
// onCancel(); });
// }} }}
// > />
// <ModalContent> {isSignup && (
// <ModalHeader className="flex flex-col gap-1"> <Input
// {messages.project} isRequired
// </ModalHeader> type="username"
// <ModalBody> label={messages.username}
// <Input autoComplete="username"
// type="text" className="mt-3"
// label={messages.projectName} onChange={(e) => {
// value={projectName.text} setUser({
// isInvalid={projectName.isValid} ...user,
// errorMessage={projectName.errorMessage} username: e.target.value,
// onChange={(e) => { });
// setProjectName({ }}
// ...projectName, />
// text: e.target.value, )}
// }); <Input
// }} label={messages.password}
// /> variant="bordered"
// <Textarea autoComplete={isSignup ? "new-password" : "current-password"}
// label={messages.projectDetail} className="mt-3"
// value={projectDetail.text} type={isPasswordVisible ? "text" : "password"}
// isInvalid={projectDetail.isValid} endContent={
// errorMessage={projectDetail.errorMessage} <button
// onChange={(e) => { className="focus:outline-none"
// setProjectDetail({ type="button"
// ...projectDetail, onClick={togglePasswordVisibility}
// text: e.target.value, >
// }); {isPasswordVisible ? <Eye size={20} /> : <EyeOff size={20} />}
// }} </button>
// /> }
// </ModalBody> onChange={(e) => {
// <ModalFooter> setUser({
// <Button variant="light" onPress={onCancel}> ...user,
// {messages.close} password: e.target.value,
// </Button> });
// <Button color="primary" onPress={validate}> }}
// {editingProject ? messages.update : messages.create} />
// </Button> {isSignup && (
// </ModalFooter> <Input
// </ModalContent> label={messages.confirmPassword}
// </Modal> 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 }) { export default function Page(params: { locale: string }) {
const t = useTranslations("Auth"); const t = useTranslations("Auth");
const messages = { const messages = {
signup: t("projectList"), title: t("signin"),
signin: t("project"), 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 ( 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 = { export type AuthMessages = {
signup: string; title: string;
signin: 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;
}; };