Check user auth in api request
This commit is contained in:
21
backend/middleware/auth.js
Normal file
21
backend/middleware/auth.js
Normal 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 };
|
||||||
4
backend/routes/auth/authSettings.js
Normal file
4
backend/routes/auth/authSettings.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const roles = [{ uid: 'admin' }, { uid: 'moderator' }, { uid: 'user' }];
|
||||||
|
const defaultDangerKey = 'test-plat-default-key';
|
||||||
|
|
||||||
|
module.exports = { roles, defaultDangerKey };
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
const roles = [{ uid: 'admin' }, { uid: 'moderator' }, { uid: 'user' }];
|
|
||||||
module.exports = roles;
|
|
||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "最終更新",
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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: () => {};
|
||||||
|
|||||||
Reference in New Issue
Block a user