Skip to content
Snippets Groups Projects
Commit 31c0e951 authored by Bernard Roland (Student Com20)'s avatar Bernard Roland (Student Com20)
Browse files

Multiple changes to the database and api

- Added an icon to the tasks
- Added a color to the porjects
- Added an image to the users
- Removed unnecessary fields
- Added a API route to update tasks
- Fixed the API route to get tasks
parent e1609e53
No related branches found
No related tags found
No related merge requests found
......@@ -9,6 +9,7 @@ export async function up(database: Knex): Promise<void> {
table.string('passwd_hash', 60).notNullable();
table.text('email');
table.text('real_name');
table.binary('image');
})
.createTable('teams', table => {
table.uuid('id').notNullable().primary();
......@@ -28,6 +29,7 @@ export async function up(database: Knex): Promise<void> {
.createTable('projects', table => {
table.uuid('id').notNullable().primary();
table.text('name').notNullable();
table.string('color').notNullable();
})
.createTable('team_projects', table => {
table.uuid('project_id').notNullable().references('projects.id');
......@@ -39,6 +41,7 @@ export async function up(database: Knex): Promise<void> {
table.uuid('project_id').notNullable().references('projects.id');
table.text('name').notNullable();
table.text('text').notNullable();
table.string('icon').notNullable();
table.enum('status', [ 'open', 'closed', 'suspended' ]).notNullable();
table.enum('priority', [ 'low', 'medium', 'high', 'urgent' ]).notNullable();
table.timestamp('created').notNullable();
......@@ -59,8 +62,6 @@ export async function up(database: Knex): Promise<void> {
table.uuid('user_id').notNullable().references('users.id');
table.uuid('task_id').notNullable().references('tasks.id');
table.primary(['user_id', 'task_id']);
table.boolean('assigned').notNullable();
table.boolean('working').notNullable();
})
.createTable('comments', table => {
table.uuid('id').notNullable().primary();
......
......@@ -11,8 +11,10 @@
"bcrypt": "^5.0.1",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"express-fileupload": "^1.2.1",
"jsonwebtoken": "^8.5.1",
"knex": "^0.95.4",
"sharp": "^0.28.1",
"sqlite3": "^5.0.2",
"uuid": "^8.3.2"
},
......@@ -25,9 +27,11 @@
"@types/bcrypt": "^3.0.1",
"@types/body-parser": "^1.19.0",
"@types/express": "^4.17.11",
"@types/express-fileupload": "^1.1.6",
"@types/jest": "^26.0.22",
"@types/jsonwebtoken": "^8.5.1",
"@types/node": "^14.14.37",
"@types/sharp": "^0.28.0",
"@types/uuid": "^8.3.0",
"jest": "^26.6.3",
"nodemon": "^2.0.7",
......
import express, { Request, Response, NextFunction } from 'express';
import { json as bodyJson } from 'body-parser';
import fileupload from 'express-fileupload';
import { port } from './config';
import { addDefaultHeaders } from './headers';
......@@ -10,6 +11,7 @@ const app = express();
app.use(addDefaultHeaders);
app.use(bodyJson());
app.use(fileupload());
app.use('/v1', v1);
......
......@@ -85,11 +85,12 @@ project.get('/:uuid', async (req, res) => {
interface AddProjectBody {
teams: Array<string>;
name: string;
color: string;
token: Token;
}
project.post('/', async (req, res) => {
if (isOfType<AddProjectBody>(req.body, [['teams', 'object'], ['name', 'string']])) {
if (isOfType<AddProjectBody>(req.body, [['teams', 'object'], ['name', 'string'], ['color', 'string']])) {
try {
const team_ids = req.body.teams;
for (const team_id of team_ids) {
......@@ -101,36 +102,44 @@ project.post('/', async (req, res) => {
return;
}
}
const project_id = uuid();
const team = await database('users')
.innerJoin('team_members', 'users.id', 'team_members.user_id')
.select({ id: 'team_members.team_id' })
.where({
'users.id': req.body.token.id,
})
.whereIn('team_members.team_id', team_ids);
if (team.length === team_ids.length) {
await database.transaction(async transaction => {
await transaction('projects').insert({
id: project_id,
name: req.body.name,
});
await transaction('team_projects').insert(
team_ids.map(team_id => ({
project_id: project_id,
team_id: team_id,
}))
);
});
res.status(200).json({
status: 'success',
id: project_id,
});
} else {
res.status(404).json({
const color = req.body.color;
if (!color.match(/^#[0-9a-fA-F]{6}$/)) {
res.status(400).json({
status: 'error',
message: 'team not found',
message: 'malformed color',
});
} else {
const project_id = uuid();
const team = await database('users')
.innerJoin('team_members', 'users.id', 'team_members.user_id')
.select({ id: 'team_members.team_id' })
.where({
'users.id': req.body.token.id,
})
.whereIn('team_members.team_id', team_ids);
if (team.length === team_ids.length) {
await database.transaction(async transaction => {
await transaction('projects').insert({
id: project_id,
name: req.body.name,
});
await transaction('team_projects').insert(
team_ids.map(team_id => ({
project_id: project_id,
team_id: team_id,
}))
);
});
res.status(200).json({
status: 'success',
id: project_id,
});
} else {
res.status(404).json({
status: 'error',
message: 'team not found',
});
}
}
} catch (e) {
res.status(400).json({
......@@ -147,9 +156,10 @@ project.post('/', async (req, res) => {
});
interface UpdateProjectBody {
add_teams?: Array<string>;
remove_teams?: Array<string>;
add_teams?: Array<string>;
name?: string;
color?: string;
token: Token;
}
......@@ -184,13 +194,20 @@ project.put('/:uuid', async (req, res) => {
});
if (projects.length >= 1) {
await database.transaction(async transaction => {
if (typeof req.body.name === 'string') {
await transaction('projects')
.update({
name: req.body.name,
}).where({
id: id,
});
await transaction('projects')
.update({
name: req.body.name,
color: req.body.color,
}).where({
id: id,
});
if (remove_team_ids.length !== 0) {
await transaction('team_projects')
.delete()
.where({
'project_id': id,
})
.whereIn('team_id', remove_team_ids);
}
if (add_team_ids.length !== 0) {
await transaction('team_projects').insert(
......@@ -200,14 +217,6 @@ project.put('/:uuid', async (req, res) => {
}))
);
}
if (remove_team_ids.length !== 0) {
await transaction('team_projects')
.delete()
.where({
'project_id': id,
})
.whereIn('team_id', remove_team_ids);
}
});
res.status(200).json({
status: 'success',
......
......@@ -17,7 +17,7 @@ task.get('/:uuid', async (req, res) => {
const task = await database('users')
.innerJoin('team_members', 'users.id', 'team_members.user_id')
.innerJoin('team_projects', 'team_members.team_id', 'team_projects.team_id')
.innerJoin('tasks', 'team_projects.project_id', 'task.team_id')
.innerJoin('tasks', 'team_projects.project_id', 'tasks.project_id')
.select({
id: 'tasks.id',
project: 'tasks.project_id',
......@@ -32,10 +32,40 @@ task.get('/:uuid', async (req, res) => {
'users.id': req.body.token.id,
'tasks.id': id,
});
if (task.length === 1) {
if (task.length >= 1) {
const assigned = await database('task_assignees')
.select({
id: 'task_assignees.user_id',
})
.where({
'task_assignees.task_id': id,
});
const dependentcies = await database('task_dependencies')
.select({
id: 'task_dependencies.requires_id',
})
.where({
'task_dependencies.task_id': id,
});
const requirements = await database('task_requirements')
.select({
role: 'task_requirements.role_id',
time: 'task_requirements.time'
})
.where({
'task_requirements.task_id': id,
});
res.status(200).json({
status: 'success',
task: task[0],
task: {
...task[0],
assigned: assigned.map(row => row.id),
dependentcies: dependentcies.map(row => row.id),
requirements: requirements.map(row => ({
role: row.role,
time: row.time,
})),
},
});
} else {
res.status(404).json({
......@@ -66,23 +96,26 @@ interface AddTaskBody {
project: string;
name: string;
text: string;
icon: string;
priority: string;
dependentcies: Array<string>;
requirements: Array<TaskRequirement>;
assigned: Array<string>;
token: Token;
}
task.post('/', async (req, res) => {
if (isOfType<AddTaskBody>(req.body, [
['project', 'string'], ['name', 'string'], ['text', 'string'],
['project', 'string'], ['name', 'string'], ['text', 'string'], ['icon', 'string'],
['priority', 'string'], ['dependentcies', 'object'], ['requirements', 'object'],
])) {
try {
const project_id = req.body.project;
const dependentcy_ids = req.body.dependentcies;
const assigned_ids = req.body.assigned;
const requirements = req.body.requirements;
const requirement_ids = requirements.map(req => req.role);
for (const team_id of dependentcy_ids.concat(requirement_ids).concat([ project_id ])) {
for (const team_id of [ ...dependentcy_ids, ...requirement_ids, ...assigned_ids, project_id ]) {
if (!validate(team_id)) {
res.status(400).json({
status: 'error',
......@@ -100,8 +133,7 @@ task.post('/', async (req, res) => {
.where({
'users.id': req.body.token.id,
'projects.id': project_id,
})
.groupBy('projects.id');
});
if (project.length >= 1) {
await database.transaction(async transaction => {
await transaction('tasks').insert({
......@@ -109,6 +141,7 @@ task.post('/', async (req, res) => {
project_id: project_id,
name: req.body.name,
text: req.body.text,
icon: req.body.icon,
priority: req.body.priority,
status: 'open',
created: new Date(),
......@@ -131,6 +164,14 @@ task.post('/', async (req, res) => {
}))
);
}
if (assigned_ids.length !== 0) {
await transaction('task_assignees').insert(
assigned_ids.map(user_id => ({
task_id: task_id,
user_id: user_id,
}))
);
}
});
res.status(200).json({
status: 'success',
......@@ -143,7 +184,6 @@ task.post('/', async (req, res) => {
});
}
} catch (e) {
console.error(e);
res.status(400).json({
status: 'error',
message: 'failed to create task',
......@@ -157,5 +197,145 @@ task.post('/', async (req, res) => {
}
});
interface UpdateTaskBody {
name?: string;
text?: string;
icon?: string;
priority?: string;
status?: string;
remove_dependentcies?: Array<string>;
remove_requirements?: Array<string>;
remove_assigned?: Array<string>;
add_dependentcies?: Array<string>;
add_requirements?: Array<TaskRequirement>;
add_assigned?: Array<string>;
token: Token;
}
task.put('/:uuid', async (req, res) => {
if (isOfType<UpdateTaskBody>(req.body, [])) {
try {
const task_id = req.params.uuid;
const remove_dependentcy_ids = req.body.remove_dependentcies ?? [];
const remove_assigned_ids = req.body.remove_assigned ?? [];
const remove_requirement_ids = req.body.remove_requirements ?? [];
const add_dependentcy_ids = req.body.add_dependentcies ?? [];
const add_assigned_ids = req.body.add_assigned ?? [];
const add_requirements = req.body.add_requirements ?? [];
const add_requirement_ids = add_requirements.map(req => req.role);
const all_ids = [
...remove_requirement_ids,
...remove_assigned_ids,
...remove_dependentcy_ids,
...add_requirement_ids,
...add_assigned_ids,
...add_dependentcy_ids,
task_id,
];
for (const team_id of all_ids) {
if (!validate(team_id)) {
res.status(400).json({
status: 'error',
message: 'malformed uuid',
});
return;
}
}
const task = await database('users')
.innerJoin('team_members', 'users.id', 'team_members.user_id')
.innerJoin('team_projects', 'team_members.team_id', 'team_projects.team_id')
.innerJoin('tasks', 'team_projects.project_id', 'tasks.project_id')
.select({ id: 'tasks.id' })
.where({
'users.id': req.body.token.id,
'tasks.id': task_id,
});
if (task.length >= 1) {
await database.transaction(async transaction => {
await transaction('tasks')
.update({
name: req.body.name,
text: req.body.text,
icon: req.body.icon,
priority: req.body.priority,
status: req.body.status,
edited: new Date(),
})
.where({
'tasks.id': task_id,
});
if (remove_requirement_ids.length !== 0) {
await transaction('task_requirements')
.delete()
.where({
'task_id': task_id,
})
.whereIn('role_id', remove_requirement_ids);
}
if (remove_dependentcy_ids.length !== 0) {
await transaction('task_dependencies')
.delete()
.where({
'task_id': task_id,
})
.whereIn('requires_id', remove_dependentcy_ids);
}
if (remove_assigned_ids.length !== 0) {
await transaction('task_assignees')
.delete()
.where({
'task_id': task_id,
})
.whereIn('user_id', remove_assigned_ids);
}
if (add_requirements.length !== 0) {
await transaction('task_requirements').insert(
add_requirements.map(requirement => ({
task_id: task_id,
role_id: requirement.role,
time: requirement.time,
}))
);
}
if (add_dependentcy_ids.length !== 0) {
await transaction('task_dependencies').insert(
add_dependentcy_ids.map(dependentcy_id => ({
task_id: task_id,
requires_id: dependentcy_id,
}))
);
}
if (add_assigned_ids.length !== 0) {
await transaction('task_assignees').insert(
add_assigned_ids.map(user_id => ({
task_id: task_id,
user_id: user_id,
}))
);
}
});
res.status(200).json({
status: 'success',
});
} else {
res.status(404).json({
status: 'error',
message: 'task not found',
});
}
} catch (e) {
res.status(400).json({
status: 'error',
message: 'failed to update task',
});
}
} else {
res.status(400).json({
status: 'error',
message: 'missing request fields',
});
}
});
export default task;
import express from 'express';
import express, { Request, Response } from 'express';
import { validate } from 'uuid';
import sharp from 'sharp';
import { UploadedFile } from 'express-fileupload';
import database from '../database';
import { isOfType } from '../util';
......@@ -17,7 +19,7 @@ user.get('/name/:username', async (req, res) => {
username: 'user_name',
})
.where({ username: name });
if (user.length === 1) {
if (user.length >= 1) {
res.status(200).json({
status: 'success',
user: user[0],
......@@ -48,7 +50,7 @@ user.get('/', async (req, res) => {
realname: 'real_name',
})
.where({ id: req.body.token.id });
if (user.length === 1) {
if (user.length >= 1) {
res.status(200).json({
status: 'success',
user: user[0],
......@@ -85,7 +87,7 @@ user.put('/', async (req, res) => {
real_name: req.body.realname,
})
.where({ id: req.body.token.id });
if (updated === 1) {
if (updated >= 1) {
res.status(200).json({
status: 'success',
});
......@@ -109,6 +111,43 @@ user.put('/', async (req, res) => {
}
});
user.put('/image', async (req, res) => {
if (req.files?.image && isOfType<UploadedFile>(req.files.image, [['data', 'object']])) {
try {
const image = await sharp(req.files.image.data).resize(128, 128, {
fit: "contain",
}).png({
compressionLevel: 9,
}).toBuffer();
const updated = await database('users')
.update({
image: image,
})
.where({ id: req.body.token.id });
if (updated >= 1) {
res.status(200).json({
status: 'success',
});
} else {
res.status(404).json({
status: 'error',
message: 'user not found',
});
}
} catch (e) {
res.status(400).json({
status: 'error',
message: 'failed to update user',
});
}
} else {
res.status(400).json({
status: 'error',
message: 'missing image file',
});
}
});
user.get('/:uuid', async (req, res) => {
try {
const id = req.params.uuid;
......@@ -121,7 +160,7 @@ user.get('/:uuid', async (req, res) => {
realname: 'real_name',
})
.where({ id: id });
if (user.length === 1) {
if (user.length >= 1) {
res.status(200).json({
status: 'success',
user: user[0],
......@@ -146,4 +185,49 @@ user.get('/:uuid', async (req, res) => {
}
});
function sendRangedData(req: Request, res: Response, data: Buffer) {
res.setHeader('Accept-Ranges', 'bytes');
const range = req.range(data.length);
if (typeof range === 'object' && range.type === 'bytes' && range.length === 1) {
res.status(206);
res.setHeader('Content-Range', `${range[0].start}\-${range[0].end}/${data.length}`);
res.send(data.slice(range[0].start, range[0].end+1));
} else {
res.status(200);
res.send(data);
}
}
user.get('/:uuid/image', async (req, res) => {
try {
const id = req.params.uuid;
if (validate(id)) {
const user = await database('users')
.select({
image: 'users.image'
})
.where({ id: id });
if (user.length >= 1) {
res.setHeader('Content-Type', 'image/png');
sendRangedData(req, res, user[0].image);
} else {
res.status(404).json({
status: 'error',
message: 'user not found',
});
}
} else {
res.status(400).json({
status: 'error',
message: 'malformed uuid',
});
}
} catch (e) {
res.status(400).json({
status: 'error',
message: 'failed get user',
});
}
});
export default user;
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment