Check user auth in api request

This commit is contained in:
Takeshi Kimata
2024-05-25 12:35:10 +09:00
parent 7a0bb63e7b
commit d6e7c98025
19 changed files with 106 additions and 50 deletions

View File

@@ -0,0 +1,21 @@
const jwt = require('jsonwebtoken');
const { defaultDangerKey } = require('../routes/auth/authSettings');
function verifySinedIn(req, res, next) {
const token = req.header('Authorization');
const secretKey = process.env.SECRET_KEY || defaultDangerKey;
if (!token) {
return res.status(401).json({ error: 'Access denied' });
}
try {
const decoded = jwt.verify(token, secretKey);
req.userId = decoded.userId;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
module.exports = { verifySinedIn };

View File

@@ -0,0 +1,4 @@
const roles = [{ uid: 'admin' }, { uid: 'moderator' }, { uid: 'user' }];
const defaultDangerKey = 'test-plat-default-key';
module.exports = { roles, defaultDangerKey };

View File

@@ -1,2 +0,0 @@
const roles = [{ uid: 'admin' }, { uid: 'moderator' }, { uid: 'user' }];
module.exports = roles;

View File

@@ -4,9 +4,11 @@ const defineUser = require('../../models/users');
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { defaultDangerKey } = require('./authSettings');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const User = defineUser(sequelize, DataTypes); const User = defineUser(sequelize, DataTypes);
const secretKey = process.env.SECRET_KEY || defaultDangerKey;
router.post('/signin', async (req, res) => { router.post('/signin', async (req, res) => {
try { try {
@@ -24,7 +26,7 @@ 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 accessToken = jwt.sign({ userId: user.id }, 'your-secret-key', { const accessToken = jwt.sign({ userId: user.id }, secretKey, {
expiresIn: '1h', expiresIn: '1h',
}); });
res.status(200).json({ access_token: accessToken, user }); res.status(200).json({ access_token: accessToken, user });

View File

@@ -2,12 +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 roles = require('./roles'); const { roles, defaultDangerKey } = require('./authSettings');
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const User = defineUser(sequelize, DataTypes); const User = defineUser(sequelize, DataTypes);
const secretKey = process.env.SECRET_KEY || defaultDangerKey;
router.post('/signup', async (req, res) => { router.post('/signup', async (req, res) => {
try { try {
@@ -27,7 +28,7 @@ module.exports = function (sequelize) {
role: initialRole, role: initialRole,
}); });
const accessToken = jwt.sign({ userId: user.id }, 'your-secret-key', { const accessToken = jwt.sign({ userId: user.id }, secretKey, {
expiresIn: '1h', expiresIn: '1h',
}); });

View File

@@ -2,11 +2,12 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const defineProject = require('../../models/projects'); const defineProject = require('../../models/projects');
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { verifySinedIn } = require('../../middleware/auth');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
router.get('/', async (req, res) => { router.get('/', verifySinedIn, async (req, res) => {
try { try {
const projects = await Project.findAll(); const projects = await Project.findAll();
res.json(projects); res.json(projects);

View File

@@ -3,19 +3,25 @@ const router = express.Router();
const defineProject = require('../../models/projects'); const defineProject = require('../../models/projects');
const defineFolder = require('../../models/folders'); const defineFolder = require('../../models/folders');
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { verifySinedIn } = require('../../middleware/auth');
module.exports = function (sequelize) { module.exports = function (sequelize) {
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
const Folder = defineFolder(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes);
Project.hasMany(Folder, { foreignKey: 'projectId' }); Project.hasMany(Folder, { foreignKey: 'projectId' });
router.get('/:projectId', async (req, res) => { router.get('/:projectId', verifySinedIn, async (req, res) => {
const projectId = req.params.projectId; const projectId = req.params.projectId;
if (!projectId) { if (!projectId) {
return res.status(400).json({ error: 'projectId is required' }); return res.status(400).json({ error: 'projectId is required' });
} }
// if project is private, only project owner can access
if (!project.isPublic && project.userId !== req.userId) {
return res.status(403).json({ error: 'Forbidden' });
}
try { try {
const project = await Project.findByPk(projectId, { const project = await Project.findByPk(projectId, {
include: [ include: [

View File

@@ -56,6 +56,7 @@
"edit_project": "Edit Project", "edit_project": "Edit Project",
"delete_project": "Delete Project", "delete_project": "Delete Project",
"id": "ID", "id": "ID",
"publicity": "Publicity",
"name": "Name", "name": "Name",
"detail": "Detail", "detail": "Detail",
"last_update": "Last update", "last_update": "Last update",

View File

@@ -55,6 +55,7 @@
"edit_project": "プロジェクトの編集", "edit_project": "プロジェクトの編集",
"delete_project": "プロジェクトの削除", "delete_project": "プロジェクトの削除",
"id": "ID", "id": "ID",
"publicity": "公開",
"name": "名前", "name": "名前",
"detail": "詳細", "detail": "詳細",
"last_update": "最終更新", "last_update": "最終更新",

View File

@@ -23,17 +23,16 @@ export default function DropdownAccount({ messages, locale, onItemPress }: Props
router.push(`/`, { locale: locale }); router.push(`/`, { locale: locale });
}; };
let userAvatar = let userAvatar = context.isSignedIn() ? (
context.token && context.token.user ? ( <Avatar
<Avatar size={16}
size={16} name={context.token.user.username}
name={context.token.user.username} variant="beam"
variant="beam" colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']}
colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']} />
/> ) : (
) : ( <User size={16} />
<User size={16} /> );
);
const signinItems = [ const signinItems = [
{ {
@@ -81,10 +80,10 @@ export default function DropdownAccount({ messages, locale, onItemPress }: Props
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button size="sm" variant="light" startContent={userAvatar} endContent={<ChevronDown size={16} />}> <Button size="sm" variant="light" startContent={userAvatar} endContent={<ChevronDown size={16} />}>
{context.token && context.token.user ? context.token.user.username : messages.signIn} {context.isSignedIn() ? context.token.user.username : messages.signIn}
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
{context.token && context.token.user ? ( {context.isSignedIn() ? (
<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} title={entry.title} startContent={entry.icon} onPress={entry.onPress} /> <DropdownItem key={entry.uid} title={entry.title} startContent={entry.icon} onPress={entry.onPress} />

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { createContext, useState, useEffect } from 'react'; import { createContext, useState, useEffect } from 'react';
import { TokenType } from '@/types/user'; import { TokenContextType, TokenType } from '@/types/user';
import { TokenProps } from '@/types/user'; import { TokenProps } from '@/types/user';
import { useRouter, usePathname } from '@/src/navigation'; import { useRouter, usePathname } from '@/src/navigation';
@@ -15,8 +15,17 @@ function removeTokenFromLocalStorage() {
localStorage.removeItem(LOCAL_STORAGE_KEY); localStorage.removeItem(LOCAL_STORAGE_KEY);
} }
const defaultTokenContext = {}; const defaultContext = {
const TokenContext = createContext(defaultTokenContext); token: {
access_token: '',
user: null,
},
isSignedIn: () => false,
setToken: (token: TokenType) => {},
storeTokenToLocalStorage,
removeTokenFromLocalStorage,
};
const TokenContext = createContext<TokenContextType>(defaultContext);
const TokenProvider = ({ locale, children }: TokenProps) => { const TokenProvider = ({ locale, children }: TokenProps) => {
const router = useRouter(); const router = useRouter();
@@ -26,8 +35,14 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
access_token: '', access_token: '',
user: null, user: null,
}); });
const isSignedIn = () => {
return token && token.user && token.user.username ? true : false;
};
const tokenContext = { const tokenContext = {
token, token,
isSignedIn,
setToken, setToken,
storeTokenToLocalStorage, storeTokenToLocalStorage,
removeTokenFromLocalStorage, removeTokenFromLocalStorage,

View File

@@ -18,7 +18,7 @@ export default function AccountPage({ messages, locale }: Props) {
return ( return (
<> <>
{context.token && context.token.user && ( {context.isSignedIn() && (
<Card className="w-[600px] mt-16 mx-3"> <Card className="w-[600px] mt-16 mx-3">
<CardHeader className="flex gap-6"> <CardHeader className="flex gap-6">
<Avatar <Avatar

View File

@@ -20,8 +20,11 @@ export default function ProjectsPage({ messages, locale }: Props) {
useEffect(() => { useEffect(() => {
async function fetchDataEffect() { async function fetchDataEffect() {
if (!context.isSignedIn()) {
return;
}
try { try {
const data = await fetchProjects(); const data = await fetchProjects(context.token.access_token);
setProjects(data); setProjects(data);
} catch (error: any) { } catch (error: any) {
console.error('Error in effect:', error.message); console.error('Error in effect:', error.message);
@@ -29,14 +32,14 @@ export default function ProjectsPage({ messages, locale }: Props) {
} }
fetchDataEffect(); fetchDataEffect();
}, []); }, [context]);
// dialog // dialog
const [isNeedSignedInDialogOpen, setIsNeedSignedInDialogOpen] = useState(false); const [isNeedSignedInDialogOpen, setIsNeedSignedInDialogOpen] = useState(false);
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false); const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false);
const [editingProject, setEditingProject] = useState<ProjectType | null>(null); const [editingProject, setEditingProject] = useState<ProjectType | null>(null);
const openDialogForCreate = () => { const openDialogForCreate = () => {
if (!context.token || !context.token.user) { if (!context.isSignedIn()) {
setIsNeedSignedInDialogOpen(true); setIsNeedSignedInDialogOpen(true);
return; return;
} }

View File

@@ -7,6 +7,7 @@ import {
TableRow, TableRow,
TableCell, TableCell,
Button, Button,
Chip,
DropdownTrigger, DropdownTrigger,
Dropdown, Dropdown,
DropdownMenu, DropdownMenu,
@@ -29,8 +30,8 @@ type Props = {
export default function ProjectsTable({ projects, onEditProject, onDeleteProject, messages, locale }: Props) { export default function ProjectsTable({ projects, onEditProject, onDeleteProject, messages, locale }: Props) {
const headerColumns = [ const headerColumns = [
{ name: messages.id, uid: 'id', sortable: true }, { name: messages.id, uid: 'id', sortable: true },
{ name: messages.publicity, uid: 'isPublic', sortable: true },
{ name: messages.name, uid: 'name', sortable: true }, { name: messages.name, uid: 'name', sortable: true },
{ name: messages.detail, uid: 'detail', sortable: true },
{ name: messages.lastUpdate, uid: 'updatedAt', sortable: true }, { name: messages.lastUpdate, uid: 'updatedAt', sortable: true },
{ name: messages.actions, uid: 'actions' }, { name: messages.actions, uid: 'actions' },
]; ];
@@ -60,18 +61,19 @@ export default function ProjectsTable({ projects, onEditProject, onDeleteProject
switch (columnKey) { switch (columnKey) {
case 'id': case 'id':
return <span>{cellValue}</span>; return <span>{cellValue}</span>;
case 'isPublic':
return cellValue ? <Chip size="sm">{messages.public}</Chip> : <></>;
case 'name': case 'name':
const maxLength = 30;
const truncatedDetail = truncateText(project.detail, maxLength);
return ( return (
<Link href={`/projects/${project.id}/home`} locale={locale} className={NextUiLinkClasses}> <div>
{cellValue} <Link href={`/projects/${project.id}/home`} locale={locale} className={NextUiLinkClasses}>
</Link> {cellValue}
); </Link>
case 'detail': <div className="text-xs text-default-500">
const maxLength = 20; <div>{truncatedDetail}</div>
const truncatedValue = truncateText(cellValue, maxLength); </div>
return (
<div className="flex items-center space-x-2">
<div>{truncatedValue}</div>
</div> </div>
); );
case 'updatedAt': case 'updatedAt':

View File

@@ -30,7 +30,6 @@ export default function RunsTable({ projectId, runs, onDeleteRun, messages, loca
const headerColumns = [ const headerColumns = [
{ name: messages.id, uid: 'id', sortable: true }, { name: messages.id, uid: 'id', sortable: true },
{ name: messages.name, uid: 'name', sortable: true }, { name: messages.name, uid: 'name', sortable: true },
{ name: messages.description, uid: 'description', sortable: true },
{ name: messages.lastUpdate, uid: 'updatedAt', sortable: true }, { name: messages.lastUpdate, uid: 'updatedAt', sortable: true },
{ name: messages.actions, uid: 'actions' }, { name: messages.actions, uid: 'actions' },
]; ];
@@ -61,17 +60,16 @@ export default function RunsTable({ projectId, runs, onDeleteRun, messages, loca
case 'id': case 'id':
return <span>{cellValue}</span>; return <span>{cellValue}</span>;
case 'name': case 'name':
const maxLength = 30;
const truncatedDescription = truncateText(run.description, maxLength);
return ( return (
<Link href={`/projects/${projectId}/runs/${run.id}`} locale={locale} className={NextUiLinkClasses}> <div>
{cellValue} <Link href={`/projects/${projectId}/runs/${run.id}`} locale={locale} className={NextUiLinkClasses}>
</Link> {cellValue}
); </Link>
case 'detail': <div className="text-xs text-default-500">
const maxLength = 20; <div>{truncatedDescription}</div>
const truncatedValue = truncateText(cellValue, maxLength); </div>
return (
<div className="flex items-center space-x-2">
<div>{truncatedValue}</div>
</div> </div>
); );
case 'updatedAt': case 'updatedAt':

View File

@@ -10,6 +10,7 @@ export default function Page(params: { locale: string }) {
editProject: t('edit_project'), editProject: t('edit_project'),
deleteProject: t('delete_project'), deleteProject: t('delete_project'),
id: t('id'), id: t('id'),
publicity: t('publicity'),
name: t('name'), name: t('name'),
detail: t('detail'), detail: t('detail'),
lastUpdate: t('last_update'), lastUpdate: t('last_update'),

View File

@@ -4,7 +4,7 @@ const apiServer = Config.apiServer;
/** /**
* fetch project records * fetch project records
*/ */
async function fetchProjects() { async function fetchProjects(jwt: string) {
const url = `${apiServer}/projects`; const url = `${apiServer}/projects`;
try { try {
@@ -12,6 +12,7 @@ async function fetchProjects() {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: jwt,
}, },
}); });

View File

@@ -19,6 +19,7 @@ export type ProjectsMessages = {
editProject: string; editProject: string;
deleteProject: string; deleteProject: string;
id: string; id: string;
publicity: string;
name: string; name: string;
detail: string; detail: string;
lastUpdate: string; lastUpdate: string;

View File

@@ -22,6 +22,7 @@ export type TokenContextType = {
access_token: string; access_token: string;
user: UserType; user: UserType;
}; };
isSignedIn: () => Boolean;
setToken: (token: TokenType) => {}; setToken: (token: TokenType) => {};
storeTokenToLocalStorage: (token: TokenType) => {}; storeTokenToLocalStorage: (token: TokenType) => {};
removeTokenFromLocalStorage: () => {}; removeTokenFromLocalStorage: () => {};