Implement Auth account dropdown

This commit is contained in:
Takeshi Kimata
2024-05-19 13:33:08 +09:00
parent 3d141cf05b
commit a5b218f55e
11 changed files with 172 additions and 66 deletions

View File

@@ -24,10 +24,10 @@ module.exports = function (sequelize) {
if (!passwordMatch) { if (!passwordMatch) {
return res.status(401).json({ error: "Authentication failed" }); return res.status(401).json({ error: "Authentication failed" });
} }
const token = jwt.sign({ userId: user.id }, "your-secret-key", { const accessToken = jwt.sign({ userId: user.id }, "your-secret-key", {
expiresIn: "1h", expiresIn: "1h",
}); });
res.status(200).json({ token }); res.status(200).json({ access_token: accessToken, user });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).send("Sign up failed"); res.status(500).send("Sign up failed");

View File

@@ -32,7 +32,7 @@ module.exports = function (sequelize) {
expiresIn: "1h", expiresIn: "1h",
}); });
user.password = undefined;; user.password = undefined;
res.status(200).json({ access_token: accessToken, user }); res.status(200).json({ access_token: accessToken, user });
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@@ -22,7 +22,11 @@
"title": "Open Source Test Case Management Tool", "title": "Open Source Test Case Management Tool",
"description": "Integrate and manage all your software testing", "description": "Integrate and manage all your software testing",
"docs": "Docs", "docs": "Docs",
"projects": "Projects" "projects": "Projects",
"account": "Account",
"signup": "Sign up",
"signin": "Sign in",
"signout": "Sign out"
}, },
"Auth": { "Auth": {
"account": "Account", "account": "Account",

View File

@@ -22,7 +22,11 @@
"title": "オープンソーステストケース管理ツール", "title": "オープンソーステストケース管理ツール",
"description": "ソフトウェア開発にかかわるすべてのテストを統合管理", "description": "ソフトウェア開発にかかわるすべてのテストを統合管理",
"docs": "ドキュメント", "docs": "ドキュメント",
"projects": "プロジェクト" "projects": "プロジェクト",
"account": "アカウント",
"signup": "新規登録",
"signin": "サインイン",
"signout": "サインアウト"
}, },
"Auth": { "Auth": {
"account": "アカウント", "account": "アカウント",

View File

@@ -6,56 +6,103 @@ import {
DropdownMenu, DropdownMenu,
DropdownItem, DropdownItem,
} from "@nextui-org/react"; } from "@nextui-org/react";
import { User, ChevronDown } from "lucide-react"; import {
User,
ChevronDown,
PenTool,
ArrowRightFromLine,
ArrowRightToLine,
} from "lucide-react";
import { useContext } from "react"; import { useContext } from "react";
import { TokenContext } from "./TokenProvider"; import { TokenContext } from "./TokenProvider";
import { Link } from "@/src/navigation"; import { useRouter } from "@/src/navigation";
import { AccountDropDownMessages } from "@/types/user";
type Props = { type Props = {
messages: AccountDropDownMessages;
locale: string; locale: string;
}; };
const signinItems = [ export default function DropdownAccount({ messages, locale }: Props) {
{ uid: "account", title: "Account", href: "/account" }, const router = useRouter();
{ uid: "signout", title: "Sign out", href: "/account/signout" },
];
const signoutItems = [
{ uid: "signin", title: "Sign in", href: "/account/signin" },
];
export default function LangSwitch({ locale }: Props) {
const context = useContext(TokenContext); const context = useContext(TokenContext);
const signOut = () => {
context.setToken(null);
context.removeTokenFromLocalStorage();
router.push(`/`, { locale: locale });
};
const signinItems = [
{
uid: "account",
title: messages.account,
icon: <User size={16} />,
onPress: () => {
router.push("/account", { locale: locale });
},
},
{
uid: "signout",
title: messages.signOut,
icon: <ArrowRightFromLine size={16} />,
onPress: signOut,
},
];
const signoutItems = [
{
uid: "signin",
title: messages.signIn,
icon: <ArrowRightToLine size={16} />,
onPress: () => {
router.push("/account/signin", { locale: locale });
},
},
{
uid: "signup",
title: messages.signUp,
icon: <PenTool size={16} />,
onPress: () => {
router.push("/account/signup", { locale: locale });
},
},
];
return ( return (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button <Button
size="sm" size="sm"
variant="faded" variant="light"
startContent={<User size={16} />} startContent={<User size={16} />}
endContent={<ChevronDown size={16} />} endContent={<ChevronDown size={16} />}
> >
{context.token ? context.token.user.username : "SignIn"} {context.token && context.token.user
? context.token.user.username
: messages.signIn}
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
{context.token ? ( {context.token && context.token.user ? (
<DropdownMenu aria-label="account actions when sign in"> <DropdownMenu aria-label="account actions when sign in">
{signinItems.map((entry) => ( {signinItems.map((entry) => (
<DropdownItem key={entry.uid}> <DropdownItem
<Link href={entry.href} locale={locale}> key={entry.uid}
{entry.title} title={entry.title}
</Link> startContent={entry.icon}
</DropdownItem> onPress={entry.onPress}
/>
))} ))}
</DropdownMenu> </DropdownMenu>
) : ( ) : (
<DropdownMenu aria-label="account actions when sign out"> <DropdownMenu aria-label="account actions when sign out">
{signoutItems.map((entry) => ( {signoutItems.map((entry) => (
<DropdownItem key={entry.uid}> <DropdownItem
<Link href={entry.href} locale={locale}> key={entry.uid}
{entry.title} title={entry.title}
</Link> startContent={entry.icon}
</DropdownItem> onPress={entry.onPress}
/>
))} ))}
</DropdownMenu> </DropdownMenu>
)} )}

View File

@@ -15,7 +15,7 @@ const locales = [
{ code: "ja", name: "日本語" }, { code: "ja", name: "日本語" },
]; ];
export default function LangSwitch(params: { locale: string }) { export default function DropdownLanguage(params: { locale: string }) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
@@ -36,7 +36,7 @@ export default function LangSwitch(params: { locale: string }) {
<DropdownTrigger> <DropdownTrigger>
<Button <Button
size="sm" size="sm"
variant="faded" variant="light"
startContent={<Globe size={16} />} startContent={<Globe size={16} />}
endContent={<ChevronDown size={16} />} endContent={<ChevronDown size={16} />}
> >

View File

@@ -12,14 +12,20 @@ import {
import { MoveUpRight } from "lucide-react"; import { MoveUpRight } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Link } from "@/src/navigation"; import { Link } from "@/src/navigation";
import LangSwitch from "./LangSwitch";
import { ThemeSwitch } from "@/components/theme-switch"; import { ThemeSwitch } from "@/components/theme-switch";
import { GithubIcon } from "@/components/icons"; import { GithubIcon } from "@/components/icons";
import Image from "next/image"; import Image from "next/image";
import DropdownAccount from "./DropdownAccount"; import DropdownAccount from "./DropdownAccount";
import DropdownLanguage from "./DropdownLanguage";
export default function Header(params: { locale: string }) { export default function Header(params: { locale: string }) {
const t = useTranslations("Header"); const t = useTranslations("Header");
const messages = {
account: t("account"),
signUp: t("signup"),
signIn: t("signin"),
signOut: t("signout"),
};
// Links shown Header or slider // Links shown Header or slider
const commonLinks = [ const commonLinks = [
@@ -102,13 +108,21 @@ export default function Header(params: { locale: string }) {
<GithubIcon className="text-default-500" /> <GithubIcon className="text-default-500" />
</NextUiLink> </NextUiLink>
<ThemeSwitch /> <ThemeSwitch />
<LangSwitch locale={params.locale} /> <div className="hidden md:block">
<DropdownAccount locale={params.locale} /> <DropdownAccount messages={messages} locale={params.locale} />
<DropdownLanguage locale={params.locale} />
</div>
<NavbarMenuToggle className="md:hidden" /> <NavbarMenuToggle className="md:hidden" />
</NavbarContent> </NavbarContent>
<NavbarMenu> <NavbarMenu>
<div className="mx-4 mt-2 flex flex-col gap-2"> <div className="mx-4 mt-2 flex flex-col gap-2">
<NavbarMenuItem>
<DropdownAccount messages={messages} locale={params.locale} />
</NavbarMenuItem>
<NavbarMenuItem>
<DropdownLanguage locale={params.locale} />
</NavbarMenuItem>
{commonLinks.map((link) => {commonLinks.map((link) =>
link.isExternal ? ( link.isExternal ? (
<NavbarMenuItem key={link.uid}> <NavbarMenuItem key={link.uid}>

View File

@@ -1,23 +1,47 @@
"use client"; "use client";
import { createContext, useState } from "react"; import { createContext, useState, useEffect } from "react";
import { TokenType, UserType } from "@/types/user"; import { TokenType } from "@/types/user";
import { TokenProps } from "@/types/user"; import { TokenProps } from "@/types/user";
const defaultTokenContext = { const LOCAL_STORAGE_KEY = "testplat-auth-token";
token: null,
setUser: (token: TokenType) => {}, function storeTokenToLocalStorage(token: TokenType) {
}; localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(token));
}
function removeTokenFromLocalStorage() {
localStorage.removeItem(LOCAL_STORAGE_KEY);
}
const defaultTokenContext = {};
const TokenContext = createContext(defaultTokenContext); const TokenContext = createContext(defaultTokenContext);
const TokenProvider = ({ children }: TokenProps) => { const TokenProvider = ({ children }: TokenProps) => {
const [token, setToken] = useState<TokenType>(null); const [token, setToken] = useState<TokenType>({
access_token: "",
user: null,
});
const tokenContext = { const tokenContext = {
token, token,
setToken, setToken,
storeTokenToLocalStorage,
removeTokenFromLocalStorage,
}; };
useEffect(() => {
const restoreTokenFromLocalStorage = () => {
const tokenString = localStorage.getItem(LOCAL_STORAGE_KEY);
if (tokenString) {
setToken(JSON.parse(tokenString));
}
};
restoreTokenFromLocalStorage();
}, []);
return ( return (
<TokenContext.Provider value={tokenContext}>{children}</TokenContext.Provider> <TokenContext.Provider value={tokenContext}>
{children}
</TokenContext.Provider>
); );
}; };

View File

@@ -18,9 +18,8 @@ async function signUp(newUser: UserType) {
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`); throw new Error(`HTTP error! Status: ${response.status}`);
} }
const accessToken = await response.json(); const token = await response.json();
storeToken(accessToken); return token;
return accessToken;
} catch (error: any) { } catch (error: any) {
console.error("Error sign up:", error); console.error("Error sign up:", error);
throw error; throw error;
@@ -43,21 +42,12 @@ async function signIn(signInUser: UserType) {
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`); throw new Error(`HTTP error! Status: ${response.status}`);
} }
const accessToken = await response.json(); const token = await response.json();
storeToken(accessToken); return token;
return accessToken;
} catch (error: any) { } catch (error: any) {
console.error("Error sign in:", error); console.error("Error sign in:", error);
throw error; throw error;
} }
} }
function storeToken(accessToken: Object) { export { signUp, signIn };
localStorage.setItem("testplat-auth-token", JSON.stringify(accessToken));
}
function signOut() {
localStorage.removeItem("testplat-auth-token");
}
export { signUp, signIn, signOut };

View File

@@ -9,6 +9,8 @@ import { roles } from "@/config/selection";
import { signUp, signIn } from "./authControl"; import { signUp, signIn } 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";
type Props = { type Props = {
isSignup: Boolean; isSignup: Boolean;
messages: AuthMessages; messages: AuthMessages;
@@ -16,6 +18,7 @@ type Props = {
}; };
export default function AuthPage({ isSignup, messages, locale }: Props) { export default function AuthPage({ isSignup, messages, locale }: Props) {
const router = useRouter();
const context = useContext(TokenContext); const context = useContext(TokenContext);
const [user, setUser] = useState<UserType>({ const [user, setUser] = useState<UserType>({
id: null, id: null,
@@ -59,30 +62,36 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
}; };
const submit = async () => { const submit = async () => {
let token;
if (isSignup) { if (isSignup) {
try { try {
const token = await signUp(user); token = await signUp(user);
context.setToken(token);
// Move to signin page
} catch { } catch {
setErrorMessage(messages.signupError); setErrorMessage(messages.signupError);
return;
} }
} else { } else {
try { try {
const token = await signIn(user); token = await signIn(user);
context.setToken(token);
// Move to signin page
} catch { } catch {
setErrorMessage(messages.signinError); setErrorMessage(messages.signinError);
return;
} }
} }
context.setToken(token);
context.storeTokenToLocalStorage(token);
router.push("/account", { locale: locale });
}; };
return ( return (
<Card className="border-none bg-background/60 dark:bg-default-100/50 w-[480px]"> <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"> <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 href={isSignup ? "/auth/signin" : "/auth/signup"} locale={locale}> <Link
href={isSignup ? "/account/signin" : "/account/signup"}
locale={locale}
>
<Button <Button
color="primary" color="primary"
variant="light" variant="light"

View File

@@ -12,11 +12,18 @@ export type TokenProps = {
}; };
export type TokenType = { export type TokenType = {
access_token: string;
user: UserType;
};
export type TokenContextType = {
token: { token: {
access_token: string; access_token: string;
user: UserType; user: UserType;
}; };
setToken: () => {}; setToken: (token: TokenType) => {};
storeTokenToLocalStorage: (token: TokenType) => {};
removeTokenFromLocalStorage: () => {};
}; };
export type AuthMessages = { export type AuthMessages = {
@@ -36,3 +43,10 @@ export type AuthMessages = {
signupError: string; signupError: string;
signinError: string; signinError: string;
}; };
export type AccountDropDownMessages = {
account: string;
signUp: string;
signIn: string;
signOut: string;
};