Implement Auth account page
@@ -11,7 +11,7 @@ module.exports = function (sequelize) {
|
|||||||
|
|
||||||
router.post("/signup", async (req, res) => {
|
router.post("/signup", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { email, password, username } = req.body;
|
const { email, password, username, avatarPath } = req.body;
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
const userCount = await User.count();
|
const userCount = await User.count();
|
||||||
@@ -25,7 +25,7 @@ module.exports = function (sequelize) {
|
|||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
username: username,
|
username: username,
|
||||||
role: initialRole,
|
role: initialRole,
|
||||||
avatarPath: null,
|
avatarPath: avatarPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const accessToken = jwt.sign({ userId: user.id }, "your-secret-key", {
|
const accessToken = jwt.sign({ userId: user.id }, "your-secret-key", {
|
||||||
|
|||||||
@@ -64,6 +64,22 @@ const testRunCaseStatus = [
|
|||||||
{ uid: "skipped", color: "primary", chartColor: "#805aab" },
|
{ uid: "skipped", color: "primary", chartColor: "#805aab" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const avatars = [
|
||||||
|
"/avatar/bear.png",
|
||||||
|
"/avatar/cat.png",
|
||||||
|
"/avatar/cow.png",
|
||||||
|
"/avatar/dog.png",
|
||||||
|
"/avatar/giraffe.png",
|
||||||
|
"/avatar/koala.png",
|
||||||
|
"/avatar/lion.png",
|
||||||
|
"/avatar/owl.png",
|
||||||
|
"/avatar/panda.png",
|
||||||
|
"/avatar/penguin.png",
|
||||||
|
"/avatar/rhinoceros.png",
|
||||||
|
"/avatar/shark.png",
|
||||||
|
"/avatar/sloth.png",
|
||||||
|
];
|
||||||
|
|
||||||
export {
|
export {
|
||||||
roles,
|
roles,
|
||||||
priorities,
|
priorities,
|
||||||
@@ -72,4 +88,5 @@ export {
|
|||||||
templates,
|
templates,
|
||||||
testRunStatus,
|
testRunStatus,
|
||||||
testRunCaseStatus,
|
testRunCaseStatus,
|
||||||
|
avatars,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,7 +46,8 @@
|
|||||||
"email_already_exist": "Already registered email",
|
"email_already_exist": "Already registered email",
|
||||||
"email_not_exist": "Email address not found",
|
"email_not_exist": "Email address not found",
|
||||||
"signup_error": "Sign up failed",
|
"signup_error": "Sign up failed",
|
||||||
"signin_error": "Sign in failed"
|
"signin_error": "Sign in failed",
|
||||||
|
"your_projects": "Your Projects"
|
||||||
},
|
},
|
||||||
"Projects": {
|
"Projects": {
|
||||||
"projectList": "Project List",
|
"projectList": "Project List",
|
||||||
|
|||||||
@@ -45,7 +45,8 @@
|
|||||||
"email_already_exist": "登録済みのメールアドレスです",
|
"email_already_exist": "登録済みのメールアドレスです",
|
||||||
"email_not_exist": "メールアドレスが見つかりません",
|
"email_not_exist": "メールアドレスが見つかりません",
|
||||||
"signup_error": "新規登録に失敗しました",
|
"signup_error": "新規登録に失敗しました",
|
||||||
"signin_error": "サインインに失敗しました"
|
"signin_error": "サインインに失敗しました",
|
||||||
|
"your_projects": "保有プロジェクト"
|
||||||
},
|
},
|
||||||
"Projects": {
|
"Projects": {
|
||||||
"projectList": "プロジェクト一覧",
|
"projectList": "プロジェクト一覧",
|
||||||
|
|||||||
BIN
frontend/public/avatar/bear.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
frontend/public/avatar/cat.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
frontend/public/avatar/cow.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
frontend/public/avatar/dog.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
frontend/public/avatar/giraffe.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
frontend/public/avatar/koala.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
frontend/public/avatar/lion.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
frontend/public/avatar/owl.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
frontend/public/avatar/panda.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
frontend/public/avatar/penguin.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
frontend/public/avatar/rhinoceros.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
frontend/public/avatar/shark.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
frontend/public/avatar/sloth.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
@@ -2,8 +2,10 @@
|
|||||||
import { createContext, useState, useEffect } from "react";
|
import { createContext, useState, useEffect } from "react";
|
||||||
import { TokenType } from "@/types/user";
|
import { TokenType } from "@/types/user";
|
||||||
import { TokenProps } from "@/types/user";
|
import { TokenProps } from "@/types/user";
|
||||||
|
import { useRouter, usePathname } from "@/src/navigation";
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY = "testplat-auth-token";
|
const LOCAL_STORAGE_KEY = "testplat-auth-token";
|
||||||
|
const privatePaths = ["/account"];
|
||||||
|
|
||||||
function storeTokenToLocalStorage(token: TokenType) {
|
function storeTokenToLocalStorage(token: TokenType) {
|
||||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(token));
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(token));
|
||||||
@@ -16,7 +18,10 @@ function removeTokenFromLocalStorage() {
|
|||||||
const defaultTokenContext = {};
|
const defaultTokenContext = {};
|
||||||
const TokenContext = createContext(defaultTokenContext);
|
const TokenContext = createContext(defaultTokenContext);
|
||||||
|
|
||||||
const TokenProvider = ({ children }: TokenProps) => {
|
const TokenProvider = ({ locale, children }: TokenProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const [token, setToken] = useState<TokenType>({
|
const [token, setToken] = useState<TokenType>({
|
||||||
access_token: "",
|
access_token: "",
|
||||||
user: null,
|
user: null,
|
||||||
@@ -28,13 +33,27 @@ const TokenProvider = ({ children }: TokenProps) => {
|
|||||||
removeTokenFromLocalStorage,
|
removeTokenFromLocalStorage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const restoreTokenFromLocalStorage = () => {
|
||||||
|
const tokenString = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||||
|
if (tokenString) {
|
||||||
|
const restoredToken = JSON.parse(tokenString);
|
||||||
|
setToken(restoredToken);
|
||||||
|
} else {
|
||||||
|
checkSignInPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkSignInPage = () => {
|
||||||
|
if (
|
||||||
|
privatePaths.some((path) => {
|
||||||
|
return path === pathname;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
router.push(`/account/signin`, { locale: locale });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const restoreTokenFromLocalStorage = () => {
|
|
||||||
const tokenString = localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
||||||
if (tokenString) {
|
|
||||||
setToken(JSON.parse(tokenString));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
restoreTokenFromLocalStorage();
|
restoreTokenFromLocalStorage();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
46
frontend/src/app/[locale]/account/AccountPage.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { Avatar, Card, CardHeader, CardBody, Divider } from "@nextui-org/react";
|
||||||
|
import { TokenContext } from "../TokenProvider";
|
||||||
|
|
||||||
|
type AccountPageMessages = {
|
||||||
|
yourProjects: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
messages: AccountPageMessages;
|
||||||
|
locale: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AccountPage({ messages, locale }: Props) {
|
||||||
|
const context = useContext(TokenContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{context.token && context.token.user && (
|
||||||
|
<Card className="w-[600px] mt-16 mx-3">
|
||||||
|
<CardHeader className="flex gap-6">
|
||||||
|
<Avatar
|
||||||
|
isBordered
|
||||||
|
radius="full"
|
||||||
|
className="w-16 h-16 text-large"
|
||||||
|
src={context.token.user.avatarPath}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="text-2xl font-bold">
|
||||||
|
{context.token.user.username}
|
||||||
|
</p>
|
||||||
|
<p className="text-lg text-default-500">
|
||||||
|
{context.token.user.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<Divider />
|
||||||
|
<CardBody className="overflow-visible px-4 pb-4">
|
||||||
|
<p className="text-default-500">{messages.yourProjects}</p>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { UserType } from "@/types/user";
|
import { UserType } from "@/types/user";
|
||||||
|
import { avatars } from "@/config/selection";
|
||||||
import Config from "@/config/config";
|
import Config from "@/config/config";
|
||||||
const apiServer = Config.apiServer;
|
const apiServer = Config.apiServer;
|
||||||
|
|
||||||
@@ -50,4 +51,9 @@ async function signIn(signInUser: UserType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { signUp, signIn };
|
function getRandomAvatarPath() {
|
||||||
|
const randomIndex = Math.floor(Math.random() * avatars.length);
|
||||||
|
return avatars[randomIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
export { signUp, signIn, getRandomAvatarPath };
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Link } from "@/src/navigation";
|
|||||||
import { ChevronRight, Eye, EyeOff } from "lucide-react";
|
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 { signUp, signIn } from "./authControl";
|
import { signUp, signIn, getRandomAvatarPath } from "./authControl";
|
||||||
import { isValidEmail, isValidPassword } from "./validate";
|
import { isValidEmail, isValidPassword } from "./validate";
|
||||||
import { TokenContext } from "../TokenProvider";
|
import { TokenContext } from "../TokenProvider";
|
||||||
import { useRouter } from "@/src/navigation";
|
import { useRouter } from "@/src/navigation";
|
||||||
@@ -58,6 +58,8 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialavatarPath = getRandomAvatarPath();
|
||||||
|
user.avatarPath = initialavatarPath;
|
||||||
await submit();
|
await submit();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-none bg-background/60 dark:bg-default-100/50 w-[480px]">
|
<Card className="w-[480px] mt-16">
|
||||||
<CardHeader className="px-4 pt-4 pb-0 flex justify-between">
|
<CardHeader className="px-4 pt-4 pb-0 flex justify-between">
|
||||||
<h4 className="font-bold text-large">{messages.title}</h4>
|
<h4 className="font-bold text-large">{messages.title}</h4>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -1,27 +1,10 @@
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Input, Button, Card, CardHeader, CardBody } from "@nextui-org/react";
|
import AccountPage from "./AccountPage";
|
||||||
import { Link } from "@/src/navigation";
|
|
||||||
|
|
||||||
export default function Page(params: { locale: string }) {
|
export default function Page(params: { locale: string }) {
|
||||||
const t = useTranslations("Auth");
|
const t = useTranslations("Auth");
|
||||||
|
const messages = {
|
||||||
|
yourProjects: t("your_projects"),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return <AccountPage messages={messages} locale={params.locale} />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export default function RootLayout({
|
|||||||
fontSans.variable
|
fontSans.variable
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Providers themeProps={{ attribute: "class", defaultTheme: "light" }}>
|
<Providers
|
||||||
|
themeProps={{ attribute: "class", defaultTheme: "light" }}
|
||||||
|
tokenProps={{ locale: locale }}
|
||||||
|
>
|
||||||
<div className="relative flex flex-col min-h-screen light:bg-neutral-50 dark:bg-neutral-800">
|
<div className="relative flex flex-col min-h-screen light:bg-neutral-50 dark:bg-neutral-800">
|
||||||
<Header locale={locale} />
|
<Header locale={locale} />
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
|
|||||||
@@ -12,16 +12,16 @@ import { TokenProps } from "@/types/user";
|
|||||||
export interface ProvidersProps {
|
export interface ProvidersProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
themeProps?: ThemeProviderProps;
|
themeProps?: ThemeProviderProps;
|
||||||
userProps?: TokenProps;
|
tokenProps?: TokenProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Providers({ children, themeProps, userProps }: ProvidersProps) {
|
export function Providers({ children, themeProps, tokenProps }: ProvidersProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextUIProvider navigate={router.push}>
|
<NextUIProvider navigate={router.push}>
|
||||||
<NextThemesProvider {...themeProps}>
|
<NextThemesProvider {...themeProps}>
|
||||||
<TokenProvider {...userProps}>{children}</TokenProvider>
|
<TokenProvider {...tokenProps}>{children}</TokenProvider>
|
||||||
</NextThemesProvider>
|
</NextThemesProvider>
|
||||||
</NextUIProvider>
|
</NextUIProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type UserType = {
|
|||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
export type TokenProps = {
|
export type TokenProps = {
|
||||||
|
locale: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||