Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 59 additions & 0 deletions src/app/api/drum-layouts/[id]/delete/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from 'next/server';
import { DeleteDrumLayoutResponse } from 'schema/drum_layouts';
import { error } from 'services/helpers';
import { getServerContext } from 'services/server_context';
import { getUserSession } from 'services/session/session';

const send = (res: DeleteDrumLayoutResponse) =>
NextResponse.json(DeleteDrumLayoutResponse.parse(res));

export async function POST(
_req: NextRequest,
props: { params: Promise<{ id: string }> }
): Promise<NextResponse> {
const params = await props.params;
const { id } = params;

const session = await getUserSession();
if (!session) {
return error({
statusCode: 403,
message: 'You must be logged in to delete drum layouts.',
errorBody: {},
errorSerializer: DeleteDrumLayoutResponse.parse,
});
}

const { drumLayoutsRepo } = await getServerContext();
const layoutResult = await drumLayoutsRepo.getDrumLayout(id);
if (!layoutResult.success) {
return error({
statusCode: 404,
message: 'Drum layout not found.',
errorBody: {},
errorSerializer: DeleteDrumLayoutResponse.parse,
});
}

if (layoutResult.value.uploader !== session.id) {
return error({
statusCode: 403,
message: 'You are not authorized to delete this drum layout.',
errorBody: {},
errorSerializer: DeleteDrumLayoutResponse.parse,
});
}

const deleteResult = await drumLayoutsRepo.deleteDrumLayout({ id });
if (!deleteResult.success) {
return error({
statusCode: 500,
message: 'Could not delete drum layout.',
errorBody: {},
resultError: deleteResult,
errorSerializer: DeleteDrumLayoutResponse.parse,
});
}

return send({ success: true });
}
23 changes: 23 additions & 0 deletions src/app/api/drum-layouts/[id]/download/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getEnvVars } from 'services/env';
import { getServerContext } from 'services/server_context';
import { NextRequest, NextResponse } from 'next/server';
import { redirect } from 'next/navigation';

export async function GET(_req: NextRequest, props: { params: Promise<{ id: string }> }) {
const params = await props.params;
const { id } = params;
const { drumLayoutsRepo } = await getServerContext();
const result = await drumLayoutsRepo.getDrumLayout(id);
if (result.success === false) {
return new NextResponse('Drum layout not found', { status: 404 });
}

const filename = sanitizeForDownload(result.value.name);
redirect(
`${getEnvVars().publicS3BaseUrl}/drumLayouts/${result.value.id}.zip?title=${filename}.zip`
);
}

function sanitizeForDownload(filename: string) {
return filename.replace(/[^a-z0-9\-\(\)\[\]]/gi, '_');
}
26 changes: 26 additions & 0 deletions src/app/api/drum-layouts/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from 'next/server';
import { GetDrumLayoutResponse } from 'schema/drum_layouts';
import { getServerContext } from 'services/server_context';

const send = (res: GetDrumLayoutResponse) => NextResponse.json(GetDrumLayoutResponse.parse(res));

export async function GET(
_req: NextRequest,
props: { params: Promise<{ id: string }> }
): Promise<NextResponse> {
const params = await props.params;
const { id } = params;
const { drumLayoutsRepo } = await getServerContext();
const result = await drumLayoutsRepo.getDrumLayout(id);
if (!result.success) {
return send({
success: false,
statusCode: 404,
errorMessage: 'Drum layout not found',
});
}
return send({
success: true,
drumLayout: result.value,
});
}
22 changes: 22 additions & 0 deletions src/app/api/drum-layouts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextResponse } from 'next/server';
import { FindDrumLayoutsResponse } from 'schema/drum_layouts';
import { getServerContext } from 'services/server_context';

const send = (res: FindDrumLayoutsResponse) =>
NextResponse.json(FindDrumLayoutsResponse.parse(res));

export async function GET(): Promise<NextResponse> {
const { drumLayoutsRepo } = await getServerContext();
const result = await drumLayoutsRepo.findDrumLayouts({ by: 'all' });
if (!result.success) {
return send({
success: false,
statusCode: 500,
errorMessage: 'Could not fetch drum layouts',
});
}
return send({
success: true,
drumLayouts: result.value,
});
}
99 changes: 99 additions & 0 deletions src/app/api/drum-layouts/submit/complete/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'use server';

import { MapValidity } from 'schema/drum_layouts';
import { actionError } from 'services/helpers';
import { submitDrumLayoutErrorMap } from 'services/drum_layouts/drum_layouts_repo';
import { deleteDrumLayoutFiles, getDrumLayoutFile } from 'services/maps/s3_handler';
import { getServerContext } from 'services/server_context';
import { getUserSession } from 'services/session/session';

/**
* Handles the completion and validation of a new drum layout upload. This will publish the layout
* if it is valid.
*
* If it is a reupload, it will replace the existing layout data. If it is a new layout and it is
* invalid, it will rollback and delete the temporary DrumLayout record.
*/
export async function reportDrumLayoutUploadComplete(id: string) {
const { drumLayoutsRepo } = await getServerContext();

const validityResult = await drumLayoutsRepo.setValidity(id, MapValidity.UPLOADED);
if (!validityResult.success) {
return actionError({
errorBody: {},
message: 'Could not update drum layout upload status.',
resultError: validityResult,
});
}

const dbLayoutResult = await drumLayoutsRepo.getDrumLayout(id);
const previousValidity = dbLayoutResult.success ? dbLayoutResult.value.validity : undefined;

async function cleanupFailedUpload() {
if (previousValidity === MapValidity.PENDING_UPLOAD) {
await drumLayoutsRepo.setValidity(id, MapValidity.INVALID);
await drumLayoutsRepo.deleteDrumLayout({ id });
} else {
await deleteDrumLayoutFiles(id, true);
await drumLayoutsRepo.setValidity(id, MapValidity.VALID);
}
}

const session = await getUserSession();
if (!session) {
await cleanupFailedUpload();
return actionError({
errorBody: {},
message: 'You must be logged in to submit drum layouts.',
});
}

if (dbLayoutResult.success && dbLayoutResult.value.uploader !== session.id) {
await cleanupFailedUpload();
if (previousValidity === MapValidity.PENDING_UPLOAD) {
return actionError({
errorBody: {},
message: 'You must begin and complete the upload while logged into the same user session.',
});
} else {
return actionError({
errorBody: {},
message: 'Only the original uploader can reupload a drum layout.',
});
}
}

const getFileResult = await getDrumLayoutFile(id, true);
if (!getFileResult.success) {
await cleanupFailedUpload();
return actionError({
errorBody: {},
message: 'The file could not be processed.',
resultError: getFileResult,
});
}
const layoutFile = getFileResult.value;
if (layoutFile.byteLength > 1024 * 1024 * 100) {
await cleanupFailedUpload();
return actionError({
message: 'File is over the filesize limit (100MB)',
errorBody: {},
});
}
const processResult = await drumLayoutsRepo.validateUploadedDrumLayout({
id,
layoutFile,
uploader: session.id,
});
if (!processResult.success) {
await cleanupFailedUpload();
const [_statusCode, message] = submitDrumLayoutErrorMap[processResult.errors[0].type];
return actionError({
message: processResult.errors[0].userMessage || message,
errorBody: {},
resultError: processResult,
});
}

return { success: true, value: processResult.value } as const;
}
98 changes: 98 additions & 0 deletions src/app/api/drum-layouts/submit/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { NextRequest, NextResponse } from 'next/server';
import {
MapValidity,
SubmitDrumLayoutRequest,
SubmitDrumLayoutResponse,
} from 'schema/drum_layouts';
import { error } from 'services/helpers';
import { mintDrumLayoutUploadUrl } from 'services/maps/s3_handler';
import { getServerContext } from 'services/server_context';
import { getUserSession } from 'services/session/session';

const send = (res: SubmitDrumLayoutResponse) =>
NextResponse.json(SubmitDrumLayoutResponse.parse(res));

/**
* Handles a request for uploading a new drum layout or reuploading an existing one.
* If it is a new layout, it creates a new temporary hidden DrumLayout record in the DB.
*
* This endpoint will return a presigned S3 upload URL and the layout ID.
* The client is expected to then upload to the specified URL and then call
* /api/drum-layouts/submit/complete, which will process/validate the layout and publish it.
*/
export async function POST(req: NextRequest): Promise<NextResponse> {
const session = await getUserSession();
if (!session) {
return error({
statusCode: 403,
message: 'You must be logged in to submit drum layouts.',
errorBody: {},
errorSerializer: SubmitDrumLayoutResponse.parse,
});
}

const submitReqResult = SubmitDrumLayoutRequest.safeParse(await req.json());
if (!submitReqResult.success) {
console.log(JSON.stringify(submitReqResult.error));
return error({
statusCode: 400,
message: 'Invalid submit drum layout request.',
errorBody: {},
errorSerializer: SubmitDrumLayoutResponse.parse,
});
}
const submitReq = submitReqResult.data;

const { drumLayoutsRepo } = await getServerContext();
let id;
if (submitReq.id != null) {
const layoutResult = await drumLayoutsRepo.getDrumLayout(submitReq.id);
if (!layoutResult.success) {
return error({
statusCode: 404,
message: `Could not find specified drum layout to resubmit: ${submitReq.id}`,
errorBody: {},
errorSerializer: SubmitDrumLayoutResponse.parse,
});
}
if (layoutResult.value.uploader !== session.id) {
return error({
statusCode: 403,
message: `Not authorized to modify the specified drum layout: ${submitReq.id}`,
errorBody: {},
errorSerializer: SubmitDrumLayoutResponse.parse,
});
}
id = submitReq.id;
await drumLayoutsRepo.setValidity(id, MapValidity.PENDING_REUPLOAD);
} else {
const createResult = await drumLayoutsRepo.createNewDrumLayout({
name: submitReq.name,
uploader: session.id,
});
if (!createResult.success) {
return error({
statusCode: 500,
message: 'Could not create placeholder drum layout when preparing for upload.',
errorBody: {},
resultError: createResult,
errorSerializer: SubmitDrumLayoutResponse.parse,
});
}
id = createResult.value.id;
}
const urlResp = await mintDrumLayoutUploadUrl(id);
if (!urlResp.success) {
return error({
statusCode: 500,
message: 'Could not create the URL for uploading the drum layout.',
errorBody: {},
errorSerializer: SubmitDrumLayoutResponse.parse,
});
}
return send({
success: true,
id,
url: urlResp.value,
});
}
Loading