Implement Auth account dropdown
This commit is contained in:
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -22,7 +22,11 @@
|
|||||||
"title": "オープンソーステストケース管理ツール",
|
"title": "オープンソーステストケース管理ツール",
|
||||||
"description": "ソフトウェア開発にかかわるすべてのテストを統合管理",
|
"description": "ソフトウェア開発にかかわるすべてのテストを統合管理",
|
||||||
"docs": "ドキュメント",
|
"docs": "ドキュメント",
|
||||||
"projects": "プロジェクト"
|
"projects": "プロジェクト",
|
||||||
|
"account": "アカウント",
|
||||||
|
"signup": "新規登録",
|
||||||
|
"signin": "サインイン",
|
||||||
|
"signout": "サインアウト"
|
||||||
},
|
},
|
||||||
"Auth": {
|
"Auth": {
|
||||||
"account": "アカウント",
|
"account": "アカウント",
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default function DropdownAccount({ messages, locale }: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
const context = useContext(TokenContext);
|
||||||
|
|
||||||
|
const signOut = () => {
|
||||||
|
context.setToken(null);
|
||||||
|
context.removeTokenFromLocalStorage();
|
||||||
|
router.push(`/`, { locale: locale });
|
||||||
|
};
|
||||||
|
|
||||||
const signinItems = [
|
const signinItems = [
|
||||||
{ uid: "account", title: "Account", href: "/account" },
|
{
|
||||||
{ uid: "signout", title: "Sign out", href: "/account/signout" },
|
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 = [
|
const signoutItems = [
|
||||||
{ uid: "signin", title: "Sign in", href: "/account/signin" },
|
{
|
||||||
|
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 });
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function LangSwitch({ locale }: Props) {
|
|
||||||
const context = useContext(TokenContext);
|
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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} />}
|
||||||
>
|
>
|
||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user