From d6e7c980252a79b78165b7967651bdcd48a93d82 Mon Sep 17 00:00:00 2001 From: Takeshi Kimata <117462761+kimatata@users.noreply.github.com> Date: Sat, 25 May 2024 12:35:10 +0900 Subject: [PATCH] Check user auth in api request --- backend/middleware/auth.js | 21 ++++++++++++++++ backend/routes/auth/authSettings.js | 4 +++ backend/routes/auth/roles.js | 2 -- backend/routes/auth/signin.js | 4 ++- backend/routes/auth/signup.js | 5 ++-- backend/routes/projects/index.js | 3 ++- backend/routes/projects/show.js | 8 +++++- frontend/messages/en.json | 1 + frontend/messages/ja.json | 1 + frontend/src/app/[locale]/DropdownAccount.tsx | 25 +++++++++---------- frontend/src/app/[locale]/TokenProvider.tsx | 21 +++++++++++++--- .../src/app/[locale]/account/AccountPage.tsx | 2 +- .../app/[locale]/projects/ProjectsPage.tsx | 9 ++++--- .../app/[locale]/projects/ProjectsTable.tsx | 24 ++++++++++-------- .../projects/[projectId]/runs/RunsTable.tsx | 20 +++++++-------- frontend/src/app/[locale]/projects/page.tsx | 1 + .../app/[locale]/projects/projectsControl.ts | 3 ++- frontend/types/project.ts | 1 + frontend/types/user.ts | 1 + 19 files changed, 106 insertions(+), 50 deletions(-) create mode 100644 backend/middleware/auth.js create mode 100644 backend/routes/auth/authSettings.js delete mode 100644 backend/routes/auth/roles.js diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 0000000..6719dab --- /dev/null +++ b/backend/middleware/auth.js @@ -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 }; diff --git a/backend/routes/auth/authSettings.js b/backend/routes/auth/authSettings.js new file mode 100644 index 0000000..ab36cd7 --- /dev/null +++ b/backend/routes/auth/authSettings.js @@ -0,0 +1,4 @@ +const roles = [{ uid: 'admin' }, { uid: 'moderator' }, { uid: 'user' }]; +const defaultDangerKey = 'test-plat-default-key'; + +module.exports = { roles, defaultDangerKey }; diff --git a/backend/routes/auth/roles.js b/backend/routes/auth/roles.js deleted file mode 100644 index 947988e..0000000 --- a/backend/routes/auth/roles.js +++ /dev/null @@ -1,2 +0,0 @@ -const roles = [{ uid: 'admin' }, { uid: 'moderator' }, { uid: 'user' }]; -module.exports = roles; diff --git a/backend/routes/auth/signin.js b/backend/routes/auth/signin.js index 5f7d4a9..d1eda9f 100644 --- a/backend/routes/auth/signin.js +++ b/backend/routes/auth/signin.js @@ -4,9 +4,11 @@ const defineUser = require('../../models/users'); const { DataTypes } = require('sequelize'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); +const { defaultDangerKey } = require('./authSettings'); module.exports = function (sequelize) { const User = defineUser(sequelize, DataTypes); + const secretKey = process.env.SECRET_KEY || defaultDangerKey; router.post('/signin', async (req, res) => { try { @@ -24,7 +26,7 @@ module.exports = function (sequelize) { if (!passwordMatch) { 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', }); res.status(200).json({ access_token: accessToken, user }); diff --git a/backend/routes/auth/signup.js b/backend/routes/auth/signup.js index 8fed0d9..b87f98f 100644 --- a/backend/routes/auth/signup.js +++ b/backend/routes/auth/signup.js @@ -2,12 +2,13 @@ const express = require('express'); const router = express.Router(); const defineUser = require('../../models/users'); const { DataTypes } = require('sequelize'); -const roles = require('./roles'); +const { roles, defaultDangerKey } = require('./authSettings'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); module.exports = function (sequelize) { const User = defineUser(sequelize, DataTypes); + const secretKey = process.env.SECRET_KEY || defaultDangerKey; router.post('/signup', async (req, res) => { try { @@ -27,7 +28,7 @@ module.exports = function (sequelize) { role: initialRole, }); - const accessToken = jwt.sign({ userId: user.id }, 'your-secret-key', { + const accessToken = jwt.sign({ userId: user.id }, secretKey, { expiresIn: '1h', }); diff --git a/backend/routes/projects/index.js b/backend/routes/projects/index.js index 5295f6a..be3494b 100644 --- a/backend/routes/projects/index.js +++ b/backend/routes/projects/index.js @@ -2,11 +2,12 @@ const express = require('express'); const router = express.Router(); const defineProject = require('../../models/projects'); const { DataTypes } = require('sequelize'); +const { verifySinedIn } = require('../../middleware/auth'); module.exports = function (sequelize) { const Project = defineProject(sequelize, DataTypes); - router.get('/', async (req, res) => { + router.get('/', verifySinedIn, async (req, res) => { try { const projects = await Project.findAll(); res.json(projects); diff --git a/backend/routes/projects/show.js b/backend/routes/projects/show.js index f2f3bf4..f5e8218 100644 --- a/backend/routes/projects/show.js +++ b/backend/routes/projects/show.js @@ -3,19 +3,25 @@ const router = express.Router(); const defineProject = require('../../models/projects'); const defineFolder = require('../../models/folders'); const { DataTypes } = require('sequelize'); +const { verifySinedIn } = require('../../middleware/auth'); module.exports = function (sequelize) { const Project = defineProject(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes); Project.hasMany(Folder, { foreignKey: 'projectId' }); - router.get('/:projectId', async (req, res) => { + router.get('/:projectId', verifySinedIn, async (req, res) => { const projectId = req.params.projectId; if (!projectId) { 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 { const project = await Project.findByPk(projectId, { include: [ diff --git a/frontend/messages/en.json b/frontend/messages/en.json index ead6d6d..9c5ca3b 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -56,6 +56,7 @@ "edit_project": "Edit Project", "delete_project": "Delete Project", "id": "ID", + "publicity": "Publicity", "name": "Name", "detail": "Detail", "last_update": "Last update", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index 5d3473a..adf0474 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -55,6 +55,7 @@ "edit_project": "プロジェクトの編集", "delete_project": "プロジェクトの削除", "id": "ID", + "publicity": "公開", "name": "名前", "detail": "詳細", "last_update": "最終更新", diff --git a/frontend/src/app/[locale]/DropdownAccount.tsx b/frontend/src/app/[locale]/DropdownAccount.tsx index 4a9dd15..0655024 100644 --- a/frontend/src/app/[locale]/DropdownAccount.tsx +++ b/frontend/src/app/[locale]/DropdownAccount.tsx @@ -23,17 +23,16 @@ export default function DropdownAccount({ messages, locale, onItemPress }: Props router.push(`/`, { locale: locale }); }; - let userAvatar = - context.token && context.token.user ? ( - - ) : ( - - ); + let userAvatar = context.isSignedIn() ? ( + + ) : ( + + ); const signinItems = [ { @@ -81,10 +80,10 @@ export default function DropdownAccount({ messages, locale, onItemPress }: Props - {context.token && context.token.user ? ( + {context.isSignedIn() ? ( {signinItems.map((entry) => ( diff --git a/frontend/src/app/[locale]/TokenProvider.tsx b/frontend/src/app/[locale]/TokenProvider.tsx index 7ae36ce..25ce3c9 100644 --- a/frontend/src/app/[locale]/TokenProvider.tsx +++ b/frontend/src/app/[locale]/TokenProvider.tsx @@ -1,6 +1,6 @@ 'use client'; import { createContext, useState, useEffect } from 'react'; -import { TokenType } from '@/types/user'; +import { TokenContextType, TokenType } from '@/types/user'; import { TokenProps } from '@/types/user'; import { useRouter, usePathname } from '@/src/navigation'; @@ -15,8 +15,17 @@ function removeTokenFromLocalStorage() { localStorage.removeItem(LOCAL_STORAGE_KEY); } -const defaultTokenContext = {}; -const TokenContext = createContext(defaultTokenContext); +const defaultContext = { + token: { + access_token: '', + user: null, + }, + isSignedIn: () => false, + setToken: (token: TokenType) => {}, + storeTokenToLocalStorage, + removeTokenFromLocalStorage, +}; +const TokenContext = createContext(defaultContext); const TokenProvider = ({ locale, children }: TokenProps) => { const router = useRouter(); @@ -26,8 +35,14 @@ const TokenProvider = ({ locale, children }: TokenProps) => { access_token: '', user: null, }); + + const isSignedIn = () => { + return token && token.user && token.user.username ? true : false; + }; + const tokenContext = { token, + isSignedIn, setToken, storeTokenToLocalStorage, removeTokenFromLocalStorage, diff --git a/frontend/src/app/[locale]/account/AccountPage.tsx b/frontend/src/app/[locale]/account/AccountPage.tsx index aa64daa..29b0f7a 100644 --- a/frontend/src/app/[locale]/account/AccountPage.tsx +++ b/frontend/src/app/[locale]/account/AccountPage.tsx @@ -18,7 +18,7 @@ export default function AccountPage({ messages, locale }: Props) { return ( <> - {context.token && context.token.user && ( + {context.isSignedIn() && ( { async function fetchDataEffect() { + if (!context.isSignedIn()) { + return; + } try { - const data = await fetchProjects(); + const data = await fetchProjects(context.token.access_token); setProjects(data); } catch (error: any) { console.error('Error in effect:', error.message); @@ -29,14 +32,14 @@ export default function ProjectsPage({ messages, locale }: Props) { } fetchDataEffect(); - }, []); + }, [context]); // dialog const [isNeedSignedInDialogOpen, setIsNeedSignedInDialogOpen] = useState(false); const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false); const [editingProject, setEditingProject] = useState(null); const openDialogForCreate = () => { - if (!context.token || !context.token.user) { + if (!context.isSignedIn()) { setIsNeedSignedInDialogOpen(true); return; } diff --git a/frontend/src/app/[locale]/projects/ProjectsTable.tsx b/frontend/src/app/[locale]/projects/ProjectsTable.tsx index 3c8174d..8283c63 100644 --- a/frontend/src/app/[locale]/projects/ProjectsTable.tsx +++ b/frontend/src/app/[locale]/projects/ProjectsTable.tsx @@ -7,6 +7,7 @@ import { TableRow, TableCell, Button, + Chip, DropdownTrigger, Dropdown, DropdownMenu, @@ -29,8 +30,8 @@ type Props = { export default function ProjectsTable({ projects, onEditProject, onDeleteProject, messages, locale }: Props) { const headerColumns = [ { name: messages.id, uid: 'id', sortable: true }, + { name: messages.publicity, uid: 'isPublic', 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.actions, uid: 'actions' }, ]; @@ -60,18 +61,19 @@ export default function ProjectsTable({ projects, onEditProject, onDeleteProject switch (columnKey) { case 'id': return {cellValue}; + case 'isPublic': + return cellValue ? {messages.public} : <>; case 'name': + const maxLength = 30; + const truncatedDetail = truncateText(project.detail, maxLength); return ( - - {cellValue} - - ); - case 'detail': - const maxLength = 20; - const truncatedValue = truncateText(cellValue, maxLength); - return ( -
-
{truncatedValue}
+
+ + {cellValue} + +
+
{truncatedDetail}
+
); case 'updatedAt': diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/RunsTable.tsx b/frontend/src/app/[locale]/projects/[projectId]/runs/RunsTable.tsx index 7fc9cff..6104329 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/RunsTable.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/RunsTable.tsx @@ -30,7 +30,6 @@ export default function RunsTable({ projectId, runs, onDeleteRun, messages, loca const headerColumns = [ { name: messages.id, uid: 'id', 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.actions, uid: 'actions' }, ]; @@ -61,17 +60,16 @@ export default function RunsTable({ projectId, runs, onDeleteRun, messages, loca case 'id': return {cellValue}; case 'name': + const maxLength = 30; + const truncatedDescription = truncateText(run.description, maxLength); return ( - - {cellValue} - - ); - case 'detail': - const maxLength = 20; - const truncatedValue = truncateText(cellValue, maxLength); - return ( -
-
{truncatedValue}
+
+ + {cellValue} + +
+
{truncatedDescription}
+
); case 'updatedAt': diff --git a/frontend/src/app/[locale]/projects/page.tsx b/frontend/src/app/[locale]/projects/page.tsx index 88bbc21..6ede5e7 100644 --- a/frontend/src/app/[locale]/projects/page.tsx +++ b/frontend/src/app/[locale]/projects/page.tsx @@ -10,6 +10,7 @@ export default function Page(params: { locale: string }) { editProject: t('edit_project'), deleteProject: t('delete_project'), id: t('id'), + publicity: t('publicity'), name: t('name'), detail: t('detail'), lastUpdate: t('last_update'), diff --git a/frontend/src/app/[locale]/projects/projectsControl.ts b/frontend/src/app/[locale]/projects/projectsControl.ts index 9c724d2..321d9cd 100644 --- a/frontend/src/app/[locale]/projects/projectsControl.ts +++ b/frontend/src/app/[locale]/projects/projectsControl.ts @@ -4,7 +4,7 @@ const apiServer = Config.apiServer; /** * fetch project records */ -async function fetchProjects() { +async function fetchProjects(jwt: string) { const url = `${apiServer}/projects`; try { @@ -12,6 +12,7 @@ async function fetchProjects() { method: 'GET', headers: { 'Content-Type': 'application/json', + Authorization: jwt, }, }); diff --git a/frontend/types/project.ts b/frontend/types/project.ts index 3533ee8..4dc4177 100644 --- a/frontend/types/project.ts +++ b/frontend/types/project.ts @@ -19,6 +19,7 @@ export type ProjectsMessages = { editProject: string; deleteProject: string; id: string; + publicity: string; name: string; detail: string; lastUpdate: string; diff --git a/frontend/types/user.ts b/frontend/types/user.ts index f8b4c1c..067fd16 100644 --- a/frontend/types/user.ts +++ b/frontend/types/user.ts @@ -22,6 +22,7 @@ export type TokenContextType = { access_token: string; user: UserType; }; + isSignedIn: () => Boolean; setToken: (token: TokenType) => {}; storeTokenToLocalStorage: (token: TokenType) => {}; removeTokenFromLocalStorage: () => {};