Implement Auth
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "プロジェクト一覧",
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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 }) {
|
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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user