From 15c0bedb598499bfa4e0c1075c9c3312766d95ac Mon Sep 17 00:00:00 2001 From: Okiniri Date: Tue, 26 Nov 2024 17:24:57 +0700 Subject: [PATCH] v0.3.1 (#28) --- .../api/admin/org/v1/projects/+server.ts | 191 ++++++++++++++++++ .../org/v1/projects/[project_id]/+server.ts | 93 +++++++++ .../projects/[project_id]/export/+server.ts | 65 ++++++ .../import/[organization_id]/+server.ts | 66 ++++++ 4 files changed, 415 insertions(+) create mode 100644 services/app/src/routes/api/admin/org/v1/projects/+server.ts create mode 100644 services/app/src/routes/api/admin/org/v1/projects/[project_id]/+server.ts create mode 100644 services/app/src/routes/api/admin/org/v1/projects/[project_id]/export/+server.ts create mode 100644 services/app/src/routes/api/admin/org/v1/projects/import/[organization_id]/+server.ts diff --git a/services/app/src/routes/api/admin/org/v1/projects/+server.ts b/services/app/src/routes/api/admin/org/v1/projects/+server.ts new file mode 100644 index 0000000..c75e6f3 --- /dev/null +++ b/services/app/src/routes/api/admin/org/v1/projects/+server.ts @@ -0,0 +1,191 @@ +import { PUBLIC_IS_LOCAL } from '$env/static/public'; +import { JAMAI_URL, JAMAI_SERVICE_KEY } from '$env/static/private'; +import { json } from '@sveltejs/kit'; +import { projectIDPattern } from '$lib/constants.js'; +import logger, { APIError } from '$lib/logger.js'; +import type { UserRead } from '$lib/types.js'; + +const headers = { + Authorization: `Bearer ${JAMAI_SERVICE_KEY}` +}; + +export const GET = async ({ cookies, locals, url }) => { + const activeOrganizationId = cookies.get('activeOrganizationId'); + + if (PUBLIC_IS_LOCAL === 'false') { + if (!activeOrganizationId) { + return json(new APIError('No active organization'), { status: 400 }); + } + + //* Verify user perms + if (!locals.user) { + return json(new APIError('Unauthorized'), { status: 401 }); + } + + const userApiRes = await fetch(`${JAMAI_URL}/api/admin/backend/v1/users/${locals.user.sub}`, { + headers + }); + const userApiBody = (await userApiRes.json()) as UserRead; + if (userApiRes.ok) { + const targetOrg = userApiBody.member_of.find( + (org) => org.organization_id === activeOrganizationId + ); + if (!targetOrg) { + return json(new APIError('Forbidden'), { status: 403 }); + } + } else { + logger.error('PROJECT_LIST_GETUSER', userApiBody); + return json(new APIError('Failed to get user info', userApiBody as any), { + status: userApiRes.status + }); + } + } + + const searchParams = new URLSearchParams({ organization_id: activeOrganizationId ?? '' }); + url.searchParams.forEach((value, key) => { + if (key === 'organization_id' && PUBLIC_IS_LOCAL === 'false') return; + searchParams.set(key, value); + }); + + const projectsListRes = await fetch(`${JAMAI_URL}/api/admin/org/v1/projects?${searchParams}`, { + headers + }); + const projectsListBody = await projectsListRes.json(); + + if (!projectsListRes.ok) { + logger.error('PROJECT_LIST_LIST', projectsListBody); + return json(new APIError('Failed to get projects list', projectsListBody), { + status: projectsListRes.status + }); + } else { + return json(projectsListBody); + } +}; + +export const POST = async ({ cookies, fetch, locals, request }) => { + const activeOrganizationId = cookies.get('activeOrganizationId'); + + const { name: project_name } = await request.json(); + if (!project_name || typeof project_name !== 'string' || project_name.trim() === '') { + return json(new APIError('Invalid project name'), { status: 400 }); + } + + if (!projectIDPattern.test(project_name)) { + return json( + new APIError( + 'Project name must contain only alphanumeric characters and underscores/hyphens/spaces/periods, and start and end with alphanumeric characters, between 2 and 100 characters.' + ), + { status: 400 } + ); + } + + if (PUBLIC_IS_LOCAL === 'false') { + //* Verify user perms + if (!locals.user) { + return json(new APIError('Unauthorized'), { status: 401 }); + } + + const userApiRes = await fetch(`${JAMAI_URL}/api/admin/backend/v1/users/${locals.user.sub}`, { + headers + }); + const userApiBody = (await userApiRes.json()) as UserRead; + if (userApiRes.ok) { + const targetOrg = userApiBody.member_of.find( + (org) => org.organization_id === activeOrganizationId + ); + if (!targetOrg || (targetOrg.role !== 'admin' && targetOrg.role !== 'member')) { + return json(new APIError('Forbidden'), { status: 403 }); + } + } else { + logger.error('PROJECT_CREATE_GETUSER', userApiBody); + return json(new APIError('Failed to get user info', userApiBody as any), { + status: userApiRes.status + }); + } + } + + const createProjectRes = await fetch(`${JAMAI_URL}/api/admin/org/v1/projects`, { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: project_name, + organization_id: activeOrganizationId + }) + }); + + const createProjectBody = await createProjectRes.json(); + if (!createProjectRes.ok) { + logger.error('PROJECT_CREATE_CREATE', createProjectBody); + return json(new APIError('Failed to create project', createProjectBody), { + status: createProjectRes.status + }); + } else { + return json(createProjectBody); + } +}; + +export const PATCH = async ({ locals, request }) => { + const { id: projectId, name: project_name } = await request.json(); + if (!project_name || typeof project_name !== 'string' || project_name.trim() === '') { + return json(new APIError('Invalid project name'), { status: 400 }); + } + + if (PUBLIC_IS_LOCAL === 'false') { + if (!locals.user) { + return json(new APIError('Unauthorized'), { status: 401 }); + } + + const projectApiRes = await fetch(`${JAMAI_URL}/api/admin/org/v1/projects/${projectId}`, { + headers + }); + const projectApiBody = await projectApiRes.json(); + + if (!projectApiRes.ok) { + logger.error('PROJECT_PATCH_GETPROJ', projectApiBody); + } + + const userApiRes = await fetch(`${JAMAI_URL}/api/admin/backend/v1/users/${locals.user.sub}`, { + headers + }); + const userApiBody = (await userApiRes.json()) as UserRead; + + if (userApiRes.ok) { + const targetOrg = userApiBody.member_of.find( + (org) => org.organization_id === projectApiBody.organization_id + ); + if (!targetOrg || targetOrg.role !== 'admin') { + return json(new APIError('Forbidden'), { status: 403 }); + } + } else { + logger.error('PROJECT_PATCH_GETUSER', userApiBody); + return json(new APIError('Failed to get user info', userApiBody as any), { + status: userApiRes.status + }); + } + } + + const patchProjectRes = await fetch(`${JAMAI_URL}/api/admin/org/v1/projects`, { + method: 'PATCH', + headers: { + ...headers, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id: projectId, + name: project_name + }) + }); + + const patchProjectBody = await patchProjectRes.json(); + if (!patchProjectRes.ok) { + logger.error('PROJECT_PATCH_PATCH', patchProjectBody); + return json(new APIError('Failed to update project', patchProjectBody as any), { + status: patchProjectRes.status + }); + } else { + return json({ ok: true }); + } +}; diff --git a/services/app/src/routes/api/admin/org/v1/projects/[project_id]/+server.ts b/services/app/src/routes/api/admin/org/v1/projects/[project_id]/+server.ts new file mode 100644 index 0000000..dfbe582 --- /dev/null +++ b/services/app/src/routes/api/admin/org/v1/projects/[project_id]/+server.ts @@ -0,0 +1,93 @@ +import { PUBLIC_IS_LOCAL } from '$env/static/public'; +import { JAMAI_URL, JAMAI_SERVICE_KEY } from '$env/static/private'; +import { json } from '@sveltejs/kit'; +import logger, { APIError } from '$lib/logger.js'; +import type { Project, UserRead } from '$lib/types.js'; + +const headers = { + Authorization: `Bearer ${JAMAI_SERVICE_KEY}` +}; + +export const GET = async ({ locals, params }) => { + if (PUBLIC_IS_LOCAL === 'false') { + if (!locals.user) { + return json(new APIError('Unauthorized'), { status: 401 }); + } + } + + const projectRes = await fetch(`${JAMAI_URL}/api/admin/org/v1/projects/${params.project_id}`, { + headers + }); + const projectBody = await projectRes.json(); + + if (projectRes.ok) { + if ( + PUBLIC_IS_LOCAL !== 'false' || + (projectBody as Project).organization.members?.find( + (user) => user.user_id === locals.user?.sub + ) + ) { + return json(projectBody); + } else { + return json(new APIError('Project not found'), { status: 404 }); + } + } else { + return json(new APIError('Failed to get project', projectBody as any), { + status: projectRes.status + }); + } +}; + +export const DELETE = async ({ locals, params }) => { + const projectId = params.project_id; + + if (PUBLIC_IS_LOCAL === 'false') { + //* Verify user perms + if (!locals.user) { + return json(new APIError('Unauthorized'), { status: 401 }); + } + + const projectApiRes = await fetch(`${JAMAI_URL}/api/admin/org/v1/projects/${projectId}`, { + headers + }); + const projectApiBody = await projectApiRes.json(); + + if (!projectApiRes.ok) { + logger.error('PROJECT_DELETE_GETPROJ', projectApiBody); + } + + const userApiRes = await fetch(`${JAMAI_URL}/api/admin/backend/v1/users/${locals.user.sub}`, { + headers + }); + const userApiBody = (await userApiRes.json()) as UserRead; + + if (userApiRes.ok) { + const targetOrg = userApiBody.member_of.find( + (org) => org.organization_id === projectApiBody.organization_id + ); + if (!targetOrg || targetOrg.role !== 'admin') { + return json(new APIError('Forbidden'), { status: 403 }); + } + } else { + logger.error('PROJECT_DELETE_GETUSER', userApiBody); + return json(new APIError('Failed to get user info', userApiBody as any), { + status: userApiRes.status + }); + } + } + + const deleteProjectRes = await fetch(`${JAMAI_URL}/api/admin/org/v1/projects/${projectId}`, { + method: 'DELETE', + headers + }); + + const deleteProjectBody = await deleteProjectRes.json(); + if (!deleteProjectRes.ok) { + logger.error('PROJECT_DELETE_DELETE', deleteProjectBody); + return json(new APIError('Failed to delete project', deleteProjectBody as any), { + status: deleteProjectRes.status + }); + } else { + return json({ ok: true }); + } +}; diff --git a/services/app/src/routes/api/admin/org/v1/projects/[project_id]/export/+server.ts b/services/app/src/routes/api/admin/org/v1/projects/[project_id]/export/+server.ts new file mode 100644 index 0000000..4c0671f --- /dev/null +++ b/services/app/src/routes/api/admin/org/v1/projects/[project_id]/export/+server.ts @@ -0,0 +1,65 @@ +import { PUBLIC_IS_LOCAL } from '$env/static/public'; +import { JAMAI_URL, JAMAI_SERVICE_KEY } from '$env/static/private'; +import { json } from '@sveltejs/kit'; +import logger, { APIError } from '$lib/logger.js'; +import type { UserRead } from '$lib/types.js'; + +const headers = { + Authorization: `Bearer ${JAMAI_SERVICE_KEY}` +}; + +export const GET = async ({ locals, params }) => { + const projectId = params.project_id; + + if (PUBLIC_IS_LOCAL === 'false') { + //* Verify user perms + if (!locals.user) { + return json(new APIError('Unauthorized'), { status: 401 }); + } + + const projectApiRes = await fetch(`${JAMAI_URL}/api/admin/org/v1/projects/${projectId}`, { + headers + }); + const projectApiBody = await projectApiRes.json(); + + if (!projectApiRes.ok) { + logger.error('PROJECT_EXPORT_GETPROJ', projectApiBody); + } + + const userApiRes = await fetch(`${JAMAI_URL}/api/admin/backend/v1/users/${locals.user.sub}`, { + headers + }); + const userApiBody = (await userApiRes.json()) as UserRead; + + if (userApiRes.ok) { + const targetOrg = userApiBody.member_of.find( + (org) => org.organization_id === projectApiBody.organization_id + ); + if (!targetOrg) { + return json(new APIError('Forbidden'), { status: 403 }); + } + } else { + logger.error('PROJECT_EXPORT_GETUSER', userApiBody); + return json(new APIError('Failed to get user info', userApiBody as any), { + status: userApiRes.status + }); + } + } + + const exportProjectRes = await fetch( + `${JAMAI_URL}/api/admin/org/v1/projects/${projectId}/export`, + { + headers + } + ); + + if (!exportProjectRes.ok) { + const exportProjectBody = await exportProjectRes.json(); + logger.error('PROJECT_EXPORT_EXPORT', exportProjectBody); + return json(new APIError('Failed to export project', exportProjectBody as any), { + status: exportProjectRes.status + }); + } else { + return exportProjectRes; + } +}; diff --git a/services/app/src/routes/api/admin/org/v1/projects/import/[organization_id]/+server.ts b/services/app/src/routes/api/admin/org/v1/projects/import/[organization_id]/+server.ts new file mode 100644 index 0000000..cb15352 --- /dev/null +++ b/services/app/src/routes/api/admin/org/v1/projects/import/[organization_id]/+server.ts @@ -0,0 +1,66 @@ +import { PUBLIC_IS_LOCAL } from '$env/static/public'; +import { JAMAI_URL, JAMAI_SERVICE_KEY } from '$env/static/private'; +import { json } from '@sveltejs/kit'; +import axios from 'axios'; +import logger, { APIError } from '$lib/logger.js'; +import type { UserRead } from '$lib/types.js'; + +const headers = { + Authorization: `Bearer ${JAMAI_SERVICE_KEY}` +}; + +export const POST = async ({ locals, params, request }) => { + const organizationId = params.organization_id; + + if (PUBLIC_IS_LOCAL === 'false') { + //* Verify user perms + if (!locals.user) { + return json(new APIError('Unauthorized'), { status: 401 }); + } + + const userApiRes = await fetch(`${JAMAI_URL}/api/admin/backend/v1/users/${locals.user.sub}`, { + headers + }); + const userApiBody = (await userApiRes.json()) as UserRead; + + if (userApiRes.ok) { + const targetOrg = userApiBody.member_of.find((org) => org.organization_id === organizationId); + if (!targetOrg || targetOrg.role === 'guest') { + return json(new APIError('Forbidden'), { status: 403 }); + } + } else { + logger.error('PROJECT_IMPORT_GETUSER', userApiBody); + return json(new APIError('Failed to get user info', userApiBody as any), { + status: userApiRes.status + }); + } + } + + try { + const importProjectRes = await axios.post( + `${JAMAI_URL}/api/admin/org/v1/projects/import/${organizationId}`, + await request.formData(), + { + headers: { + ...headers, + 'Content-Type': 'multipart/form-data' + } + } + ); + if (importProjectRes.status != 200) { + logger.error('PROJECT_IMPORT_IMPORT', importProjectRes.data); + return json(new APIError('Failed to import project', importProjectRes.data as any), { + status: importProjectRes.status + }); + } else { + return new Response(importProjectRes.data); + } + } catch (err) { + //@ts-expect-error AxiosError + logger.error('PROJECT_IMPORT_IMPORT', err?.response?.data); + //@ts-expect-error AxiosError + return json(new APIError('Failed to import project', err?.response?.data), { + status: 500 + }); + } +};