From bcef9a4a9dbe083f98c441111bf40a5bcdd5fc25 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Tue, 21 Jun 2022 18:28:13 +0300 Subject: [PATCH] chore: wip replicating folders api --- api/src/controllers/folder.ts | 49 ++++++++++++++- api/src/middlewares/authenticateToken.ts | 14 +++++ api/src/middlewares/header.ts | 29 +++++++++ api/src/middlewares/index.ts | 2 + api/src/middlewares/mock.ts | 24 ++++++++ api/src/model/Folder.ts | 76 ++++++++++++++++++++++++ api/src/routes/api/auth.ts | 6 +- api/src/routes/api/folders.ts | 67 +++++++++++++++++++++ api/src/routes/api/index.ts | 2 + api/src/utils/index.ts | 1 + api/src/utils/mock/header.ts | 38 ++++++++++++ api/src/utils/mock/index.ts | 2 + api/src/utils/mock/query.ts | 18 ++++++ 13 files changed, 322 insertions(+), 6 deletions(-) create mode 100644 api/src/middlewares/header.ts create mode 100644 api/src/middlewares/mock.ts create mode 100644 api/src/routes/api/folders.ts create mode 100644 api/src/utils/mock/header.ts create mode 100644 api/src/utils/mock/index.ts create mode 100644 api/src/utils/mock/query.ts diff --git a/api/src/controllers/folder.ts b/api/src/controllers/folder.ts index 30fffff..4009906 100644 --- a/api/src/controllers/folder.ts +++ b/api/src/controllers/folder.ts @@ -1,11 +1,56 @@ -import Folder, { MemberType } from '../model/Folder' +import { Body } from 'tsoa' +import Folder, { FolderPayload, MemberType } from '../model/Folder' export class FolderController { + public async createFolder(@Body() body: FolderPayload) { + return createFolder(body) + } + public async addRootFolder() { await addRootFolder() } } +interface FolderDetailsResponse { + name: string + parentFolderUri: string + children: [] +} + +const createFolder = async ({ + name, + parentFolderUri, + type +}: FolderPayload): Promise => { + parentFolderUri = parentFolderUri.replace(/\/folders\/folders\//i, '') + const parentFolder = await Folder.findById(parentFolderUri).catch( + (_: any) => { + throw new Error( + `No folder with an URI '${parentFolderUri}' has been found.` + ) + } + ) + + const folder = new Folder({ + name, + parentFolderUri, + type + }) + + const savedFolder = await folder.save().catch((err: any) => { + // TODO: log error + throw new Error(`Error while saving folder.`) + }) + + await parentFolder?.addMember(savedFolder._id) + + return { + name: savedFolder.name, + parentFolderUri: savedFolder.parentFolderUri, + children: [] + } +} + const addRootFolder = async () => { let folder = await Folder.findOne({ name: '/' }) @@ -20,3 +65,5 @@ const addRootFolder = async () => { return await folder.save() } + +const getItem = async({ path }) diff --git a/api/src/middlewares/authenticateToken.ts b/api/src/middlewares/authenticateToken.ts index b53900e..ce30b98 100644 --- a/api/src/middlewares/authenticateToken.ts +++ b/api/src/middlewares/authenticateToken.ts @@ -1,5 +1,7 @@ import jwt from 'jsonwebtoken' +import { Request, Response } from 'express' import { verifyTokenInDB } from '../utils' +import { headerIsNotPresentMessage, headerIsNotValidMessage } from './header' export const authenticateAccessToken = (req: any, res: any, next: any) => { authenticateToken( @@ -21,6 +23,18 @@ export const authenticateRefreshToken = (req: any, res: any, next: any) => { ) } +export const verifyAuthHeaderIsPresent = (req: Request, res: Response) => { + console.log(`🤖[verifyAuthHeaderIsPresent]🤖`) + + const authHeader = req.headers.authorization + + if (!authHeader) { + return res.status(401).json(headerIsNotPresentMessage('Authorization')) + } else if (!/^Bearer\s.{1}/.test(authHeader)) { + return res.status(401).json(headerIsNotValidMessage('Authorization')) + } +} + const authenticateToken = ( req: any, res: any, diff --git a/api/src/middlewares/header.ts b/api/src/middlewares/header.ts new file mode 100644 index 0000000..9ee8f8e --- /dev/null +++ b/api/src/middlewares/header.ts @@ -0,0 +1,29 @@ +import { Request, Response } from 'express' + +export const verifyAcceptHeader = (req: Request, res: Response) => { + const acceptHeader = req.headers.accept + + if (!acceptHeader) { + return res.status(406).json(headerIsNotPresentMessage('Accept')) + } else if (acceptHeader !== 'application/json') { + return res.status(406).json(headerIsNotValidMessage('Accept')) + } +} + +export const verifyContentTypeHeader = (req: Request, res: Response) => { + const contentTypeHeader = req.headers['content-type'] + + if (!contentTypeHeader) { + return res.status(406).json(headerIsNotPresentMessage('Content-Type')) + } else if (contentTypeHeader !== 'application/json') { + return res.status(406).json(headerIsNotValidMessage('Content-Type')) + } +} + +export const headerIsNotPresentMessage = (header: string) => ({ + message: `${header} header is not present.` +}) + +export const headerIsNotValidMessage = (header: string) => ({ + message: `${header} header is not valid.` +}) diff --git a/api/src/middlewares/index.ts b/api/src/middlewares/index.ts index 7798de3..c3bb7fb 100644 --- a/api/src/middlewares/index.ts +++ b/api/src/middlewares/index.ts @@ -2,3 +2,5 @@ export * from './authenticateToken' export * from './desktop' export * from './verifyAdmin' export * from './verifyAdminIfNeeded' +export * from './header' +export * from './mock' diff --git a/api/src/middlewares/mock.ts b/api/src/middlewares/mock.ts new file mode 100644 index 0000000..5248e27 --- /dev/null +++ b/api/src/middlewares/mock.ts @@ -0,0 +1,24 @@ +import { + verifyAuthHeaderIsPresent, + verifyAcceptHeader, + verifyContentTypeHeader +} from './' +import { Request, Response, NextFunction } from 'express' + +export const verifyHeaders = ( + req: Request, + res: Response, + next: NextFunction +) => { + switch (true) { + case verifyAuthHeaderIsPresent(req, res) !== undefined: + break + case verifyAcceptHeader(req, res) !== undefined: + break + case verifyContentTypeHeader(req, res) !== undefined: + break + + default: + return next() + } +} diff --git a/api/src/model/Folder.ts b/api/src/model/Folder.ts index 550571c..61972d5 100644 --- a/api/src/model/Folder.ts +++ b/api/src/model/Folder.ts @@ -1,4 +1,80 @@ +import { Document, Schema, Model, model } from 'mongoose' +import {} from '@sasjs/utils' + +export interface FolderPayload { + parentFolderUri: string + name: string + type: MemberType +} + export enum MemberType { Folder = 'Folder', File = 'File' } + +const isMemberType = (value: string) => value in MemberType + +export const getMemberType = (value: string) => { + value +} + +interface IFolderDocument extends FolderPayload, Document { + members: Schema.Types.ObjectId[] + type: MemberType +} + +interface IFolder extends IFolderDocument { + addMember(memberId: Schema.Types.ObjectId): Promise +} + +interface IFolderModel extends Model {} + +const folderSchema = new Schema({ + name: { type: String, required: true }, + parentFolderUri: { type: String, required: true }, + members: [{ type: Schema.Types.ObjectId, refPath: 'member' }], + type: { type: String, required: true } +}) + +folderSchema.post('save', (folder: IFolder, next: Function) => { + folder.populate('members', '').then(() => next()) + + next() +}) + +// folderSchema.get('item', (folder: IFolder, next: Function) => { + +// next() +// }) + +folderSchema.method( + 'addMember', + async function (memberId: Schema.Types.ObjectId) { + const folderIdIndex = this.members.indexOf(memberId) + + if (folderIdIndex === -1) this.members.push(memberId) + + this.markModified('folders') + + return this.save() + } +) + +folderSchema.method('getItem', async function (path: string) { + console.log(`🤖[getItem]🤖`) + console.log(`🤖[path]🤖`, path) + // const folderIdIndex = this.members.indexOf(memberId) + + // if (folderIdIndex === -1) this.members.push(memberId) + + // this.markModified('folders') + + // return this.save() +}) + +export const Folder: IFolderModel = model( + 'Folder', + folderSchema +) + +export default Folder diff --git a/api/src/routes/api/auth.ts b/api/src/routes/api/auth.ts index df359bc..89f2ce8 100644 --- a/api/src/routes/api/auth.ts +++ b/api/src/routes/api/auth.ts @@ -8,11 +8,7 @@ import { authenticateRefreshToken } from '../../middlewares' -import { - authorizeValidation, - getDesktopFields, - tokenValidation -} from '../../utils' +import { authorizeValidation, tokenValidation } from '../../utils' import { InfoJWT } from '../../types' const authRouter = express.Router() diff --git a/api/src/routes/api/folders.ts b/api/src/routes/api/folders.ts new file mode 100644 index 0000000..83ced8f --- /dev/null +++ b/api/src/routes/api/folders.ts @@ -0,0 +1,67 @@ +import express from 'express' +import { verifyHeaders } from '../../middlewares' +import { verifyQuery, setHeaders } from '../../utils' +import { FolderController } from '../../controllers' + +const foldersRouter = express.Router() +const controller = new FolderController() + +// https://sas.analytium.co.uk/folders/folders?parentFolderUri=/folders/folders/9e442a90-2c5b-40bb-982a-5fe3ff8a66b7 +foldersRouter.post('/folders', verifyHeaders, async (req, res) => { + console.log(`🤖[req.query]🤖`, req.query) + console.log(`🤖[req.body]🤖`, req.body) + + try { + const response = await controller.createFolder({ + ...req.query, + ...req.body + }) + + console.log(`🤖[response]🤖`, response) + + res.send(response) + } catch (err: any) { + console.log(`🤖[error]🤖`, err) + + res.status(403).send(err.toString()) + } +}) + +foldersRouter.get('/folders/@item', verifyHeaders, async (req, res) => { + const queryParam = 'path' + + try { + const response = await controller.getItem({ + ...req.query, + ...req.body + }) + + console.log(`🤖[response]🤖`, response) + + res.send(response) + } catch (err: any) { + console.log(`🤖[error]🤖`, err) + + res.status(403).send(err.toString()) + } + + // if (verifyQuery(req, res, [queryParam])) { + // const folderExist = Math.random() > 0.5 + + // setHeaders(res, folderExist) + + // if (folderExist) { + // res.status(200).json({ message: 'Folder exists!' }) + // } else { + // res.status(404).json({ + // errorCode: 11512, + // message: 'No folders match the search criteria.', + // details: [`${queryParam}: ${req.query[queryParam]}`], + // links: [], + // version: 2 + // }) + // } + // } +}) + +export default foldersRouter diff --git a/api/src/routes/api/index.ts b/api/src/routes/api/index.ts index b91715c..74d733d 100644 --- a/api/src/routes/api/index.ts +++ b/api/src/routes/api/index.ts @@ -16,6 +16,7 @@ import groupRouter from './group' import clientRouter from './client' import authRouter from './auth' import sessionRouter from './session' +import foldersRouter from './folders' const router = express.Router() @@ -32,6 +33,7 @@ router.use('/drive', authenticateAccessToken, driveRouter) router.use('/group', desktopRestrict, groupRouter) router.use('/stp', authenticateAccessToken, stpRouter) router.use('/user', desktopRestrict, userRouter) +router.use('/folders', foldersRouter) router.use( '/', swaggerUi.serve, diff --git a/api/src/utils/index.ts b/api/src/utils/index.ts index 3cf1039..b78b734 100644 --- a/api/src/utils/index.ts +++ b/api/src/utils/index.ts @@ -10,3 +10,4 @@ export * from './sleep' export * from './upload' export * from './validation' export * from './verifyTokenInDB' +export * from './mock' diff --git a/api/src/utils/mock/header.ts b/api/src/utils/mock/header.ts new file mode 100644 index 0000000..c5ee1fd --- /dev/null +++ b/api/src/utils/mock/header.ts @@ -0,0 +1,38 @@ +import { Response } from 'express' +import { uuidv4 } from '@sasjs/utils' + +export const setHeaders = (res: Response, isSuccess: boolean) => { + res.setHeader( + 'cache-control', + `no-cache, no-store, max-age=0, must-revalidate` + ) + res.setHeader( + 'content-security-policy', + `default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' *.sas.com blob: data:; style-src 'self' 'unsafe-inline'; child-src 'self' blob: data: mailto:;` + ) + res.setHeader( + 'content-type', + `application/vnd.sas.${isSuccess ? 'content.folder' : 'error'}+json${ + isSuccess ? '' : '; version=2;charset=UTF-8' + }` + ) + res.setHeader('pragma', `no-cache`) + res.setHeader('server', `Apache/2.4`) + res.setHeader('strict-transport-security', `max-age=31536000`) + res.setHeader('Transfer-Encoding', `chunked`) + res.setHeader('vary', `User-Agent`) + res.setHeader('x-content-type-options', `nosniff`) + res.setHeader('x-frame-options', `SAMEORIGIN`) + res.setHeader('x-xss-protection', `1; mode=block`) + + if (isSuccess) { + const uuid = uuidv4() + + res.setHeader('content-location', `/folders/folders/${uuid}`) + res.setHeader('etag', `-2066812946`) + res.setHeader('last-modified', `${new Date(Date.now()).toUTCString()}`) + res.setHeader('location', `/folders/folders/${uuid}`) + } else { + res.setHeader('sas-service-response-flag', `true`) + } +} diff --git a/api/src/utils/mock/index.ts b/api/src/utils/mock/index.ts new file mode 100644 index 0000000..b53c96c --- /dev/null +++ b/api/src/utils/mock/index.ts @@ -0,0 +1,2 @@ +export * from './query' +export * from './header' diff --git a/api/src/utils/mock/query.ts b/api/src/utils/mock/query.ts new file mode 100644 index 0000000..0d24a9e --- /dev/null +++ b/api/src/utils/mock/query.ts @@ -0,0 +1,18 @@ +import { Request, Response } from 'express' + +export const verifyQuery = (req: Request, res: Response, args: string[]) => { + let isValid = true + const { query } = req + + args.forEach((arg: string) => { + if (!Object.keys(query).includes(arg)) { + res.status(400).json({ message: `${arg} query argument is not present.` }) + isValid = false + } else if (!query[arg]) { + res.status(400).json({ message: `${arg} query argument is not valid.` }) + isValid = false + } + }) + + return isValid +}