Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upload own data backend #282

Merged
merged 21 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4eee565
feat(api): create model class for userfile
cephaschapa Jan 11, 2024
100918c
feat(api): created userfile migration
cephaschapa Jan 11, 2024
3e74d8e
feat(api): added file upload route, modified migration and models
cephaschapa Jan 16, 2024
f6aa52c
feat(api): added delete and get file by id routes
cephaschapa Jan 16, 2024
e890df7
feat(api): added download userfile route
cephaschapa Jan 16, 2024
c3aa663
fix(api): show right error message
cephaschapa Jan 17, 2024
35c7612
fix(api): include file_type, sector to userFile model, updated routes…
cephaschapa Jan 17, 2024
042bc68
feat(test): added userFile unit tests
cephaschapa Jan 19, 2024
d58ee38
fix: removed unused packages
cephaschapa Jan 19, 2024
e7d1c65
fix(api): moved userFile validation to validation file
cephaschapa Jan 19, 2024
389c689
fix: pr review changes
cephaschapa Jan 22, 2024
768497a
fix: added correct formData names, removed csv file
cephaschapa Jan 22, 2024
929b4c5
Merge
cephaschapa Jan 22, 2024
724f7c8
Merge branch 'develop' into feature/upload-own-data-backend
cephaschapa Jan 22, 2024
be8aaf6
Merge branch 'feature/upload-own-data-backend' of https://github.com/…
cephaschapa Jan 22, 2024
5e28486
fix(tests): resolve unit test errors
cephaschapa Jan 22, 2024
3c0b395
fix: code clean up
cephaschapa Jan 22, 2024
6eefe3b
fix(api): file acces changes, renamed columns in migration
cephaschapa Jan 24, 2024
d2d3ec9
fix(api): refined api response, fixed assertions in unit tests
cephaschapa Jan 24, 2024
d673144
fix(api): merge conflicts
cephaschapa Jan 24, 2024
97f4807
fix(api): mime download content type
cephaschapa Jan 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions app/migrations/20240111123307-userfile.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.createTable('UserFile', {
id: {
type: Sequelize.UUID,
primaryKey: true,
},
userId: {
type: Sequelize.UUID,
allowNull: true,
references: {
model:'User',
key: 'user_id'
}
},
fileReference: {
type: Sequelize.STRING,
allowNull: true
},
data: {
type: Sequelize.BLOB,
allowNull: true,
},
fileType: {
type: Sequelize.STRING,
allowNull: true
},
sector: {
type: Sequelize.STRING,
allowNull: true
},
status:{
type: Sequelize.STRING,
allowNull: true
},
url: {
type: Sequelize.STRING,
allowNull: true
},
gpcRefNo: {
type: Sequelize.STRING,
allowNull: true
},

created: {
type: Sequelize.DATE,
allowNull: true,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
lastUpdated: {
type: Sequelize.DATE,
allowNull: true,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
onUpdate: Sequelize.literal('CURRENT_TIMESTAMP'),
}
});

},

async down (queryInterface, Sequelize) {
await queryInterface.dropTable('UserFile');
}
};
74 changes: 57 additions & 17 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"eslint": "8.52.0",
"eslint-config-next": "13.4.16",
"eslint-plugin-storybook": "^0.6.13",
"fetch-blob": "^4.0.0",
"fetch-mock": "^9.11.0",
"framer-motion": "^10.16.2",
"http-errors": "^2.0.0",
Expand Down
3 changes: 2 additions & 1 deletion app/src/app/[lng]/data/[step]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ import { logger } from "@/services/logger";

function getMailURI(locode?: string, sector?: string, year?: number): string {
const emails =
process.env.NEXT_PUBLIC_SUPPORT_EMAILS || "[email protected],[email protected]";
process.env.NEXT_PUBLIC_SUPPORT_EMAILS ||
"[email protected],[email protected]";
return `mailto://${emails}?subject=Missing third party data sources&body=City: ${locode}%0ASector: ${sector}%0AYear: ${year}`;
}

Expand Down
40 changes: 40 additions & 0 deletions app/src/app/api/v0/user/[user]/file/[file]/download-file/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { db } from "@/models";
import { apiHandler } from "@/util/api";
import createHttpError from "http-errors";
import { Session } from "next-auth";
import { NextResponse } from "next/server";

export const GET = apiHandler(
async (
_req: Request,
context: { session?: Session; params: Record<string, string> },
) => {
if (!context.session) {
throw new createHttpError.Unauthorized("Unauthorized");
}

const userFile = await db.models.UserFile.findOne({
where: {
id: context.params.file,
},
});

if (!userFile) {
throw new createHttpError.NotFound("User file not found");
}

if (!userFile.userId)
throw new createHttpError.NotFound("file does not belong to this user");

let body: Buffer | undefined;
let headers: Record<string, string> | null = null;

body = userFile.data;
headers = {
"Content-Type": `application/${userFile.fileType}`,
"Content-Disposition": `attachment; filename="${userFile.id}.${userFile.fileType}"`,
};

return new NextResponse(body, { headers });
},
);
57 changes: 57 additions & 0 deletions app/src/app/api/v0/user/[user]/file/[file]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { db } from "@/models";
import { apiHandler } from "@/util/api";
import createHttpError from "http-errors";
import { Session } from "next-auth";
import { NextResponse } from "next/server";

export const GET = apiHandler(
async (
_req: Request,
context: { session?: Session; params: Record<string, string> },
) => {
if (!context.session) {
throw new createHttpError.Unauthorized("Unauthorized");
}

const userFile = await db.models.UserFile.findOne({
where: {
id: context.params.file,
},
});

if (!userFile) {
throw new createHttpError.NotFound("User file not found");
}

if (!userFile.userId)
throw new createHttpError.NotFound("file does not belong to this user");

return NextResponse.json({ data: userFile });
},
);

export const DELETE = apiHandler(
async (
_req: Request,
context: { session?: Session; params: Record<string, string> },
) => {
if (!context.session) {
throw new createHttpError.Unauthorized("Unauthorized");
}

const userFile = await db.models.UserFile.findOne({
where: {
id: context.params.file,
},
});

if (!userFile) {
throw new createHttpError.NotFound("User file not found");
}
if (!userFile.userId)
throw new createHttpError.NotFound("file does not belong to this user");
await userFile.destroy();

return NextResponse.json({ data: userFile, deleted: true });
},
);
88 changes: 88 additions & 0 deletions app/src/app/api/v0/user/[user]/file/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { db } from "@/models";
import { apiHandler } from "@/util/api";
import { createUserFileRequset } from "@/util/validation";
import { randomUUID } from "crypto";
import createHttpError from "http-errors";
import { Session } from "next-auth";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

// TODO: use these variables to configure file size and format
const MAX_FILE_SIZE = 5000000;
const ACCEPTED_FILE_FORMATS = []; // file formats types to be parsed and refined later

export const GET = apiHandler(
async (
_req: Request,
context: { session?: Session; params: Record<string, string> },
) => {
const userId = context.params.user;
if (!context.session) {
throw new createHttpError.Unauthorized("Unauthorized");
}

const userFiles = await db.models.UserFile.findAll({
where: {
userId: userId,
},
});

if (!userFiles) {
throw new createHttpError.NotFound("User files not found");
}

return NextResponse.json({ data: userFiles });
},
);

export const POST = apiHandler(
async (
req: NextRequest,
context: { session?: Session; params: Record<string, string> },
) => {
const userId = context.params.user;

if (!context.session) {
throw new createHttpError.Unauthorized("Unauthorized");
}

const formData = await req.formData();
const file = formData?.get("data") as unknown as File;

if (!file)
throw new createHttpError.BadRequest("File not found, Please add a file");

const filename = file.name;

const fileType = file.name.slice(
((filename.lastIndexOf(".") - 1) >>> 0) + 2,
);

const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);

const fileData = {
userId: userId,
fileReference: formData.get("fileReference"),
url: formData.get("url"),
data: buffer,
fileType: fileType,
sector: formData.get("sector"),
status: formData.get("status"),
gpcRefNo: formData.get("gpcRefNo"),
};

const body = createUserFileRequset.parse(fileData);

const userFile = await db.models.UserFile.create({
id: randomUUID(),
...body,
});

if (!userFile) {
throw new createHttpError.NotFound("User files not found");
}

return NextResponse.json({ data: userFile });
},
);
Loading