diff --git a/deployment/development/docker-compose.yml b/deployment/development/docker-compose.yml index b83eb832d..a4134fe68 100644 --- a/deployment/development/docker-compose.yml +++ b/deployment/development/docker-compose.yml @@ -16,6 +16,7 @@ volumes: redis-data: outdir: + services: db: image: postgres:10 @@ -98,8 +99,9 @@ services: dockerfile: Dockerfile ports: - "5173:3000" - env_file: .env + env_file: source/SIL.AppBuilder.Portal/.env environment: + VITE_DATABASE_URL: "postgresql://db-user:1234@db:5432/development?schema=public" # MUST be included (the path the application will be accessed on) and MUST NOT have a trailing slash ORIGIN: "http://localhost:5173" redis: diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/index.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/index.ts index 3f4aabefb..06b941263 100644 --- a/source/SIL.AppBuilder.Portal/common/databaseProxy/index.ts +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/index.ts @@ -28,6 +28,8 @@ const handlers = { const obj: DataType = {}; for (const prop in Prisma.ModelName) { const uncapitalized = prop[0].toLowerCase() + prop.substring(1); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore obj[uncapitalized] = prisma[uncapitalized]; } diff --git a/source/SIL.AppBuilder.Portal/common/prisma.ts b/source/SIL.AppBuilder.Portal/common/prisma.ts index 8f91983a1..637b1a42a 100644 --- a/source/SIL.AppBuilder.Portal/common/prisma.ts +++ b/source/SIL.AppBuilder.Portal/common/prisma.ts @@ -11,7 +11,9 @@ import { ReadonlyClient } from './ReadonlyPrisma.js'; // other packages, but we keep the writable client behind the abstraction layer. if (!process.env.VITE_DATABASE_URL) + // @ts-expect-error This is necessary for browser, where import.meta.env will in fact exist process.env.VITE_DATABASE_URL = import.meta.env.VITE_DATABASE_URL; + const prisma = new PrismaClient(); export type PrismaClientExact = PrismaClient; diff --git a/source/SIL.AppBuilder.Portal/common/public/prisma.ts b/source/SIL.AppBuilder.Portal/common/public/prisma.ts index 520b54da5..f05642058 100644 --- a/source/SIL.AppBuilder.Portal/common/public/prisma.ts +++ b/source/SIL.AppBuilder.Portal/common/public/prisma.ts @@ -12,31 +12,3 @@ export enum ProductTransitionType { CancelWorkflow, ProjectAccess } - -// export async function getOrganizationsForUser(userId: number) { -// const user = await prisma.users.findUnique({ -// where: { -// Id: userId -// }, -// include: { UserRoles: true, Organizations: true } -// }); -// const organizations = user?.UserRoles.find((roleDef) => roleDef.RoleId === RoleId.SuperAdmin) -// ? await prisma.organizations.findMany({ -// include: { -// Owner: true -// } -// }) -// : await prisma.organizations.findMany({ -// where: { -// OrganizationMemberships: { -// every: { -// UserId: userId -// } -// } -// }, -// include: { -// Owner: true -// } -// }); -// return organizations; -// } diff --git a/source/SIL.AppBuilder.Portal/src/auth.ts b/source/SIL.AppBuilder.Portal/src/auth.ts index e9ac22f11..09af99cf8 100644 --- a/source/SIL.AppBuilder.Portal/src/auth.ts +++ b/source/SIL.AppBuilder.Portal/src/auth.ts @@ -2,6 +2,7 @@ import { SvelteKitAuth, type DefaultSession, type SvelteKitAuthConfig } from '@a import Auth0Provider from '@auth/sveltekit/providers/auth0'; import { redirect, type Handle } from '@sveltejs/kit'; import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { RoleId } from 'sil.appbuilder.portal.common/prisma'; declare module '@auth/sveltekit' { interface Session { @@ -44,11 +45,10 @@ const config: SvelteKitAuthConfig = { // safest method is just handle such values in session below (see user.roles) // user.isSuperAdmin is a special case handled here to give the /admin/jobs route // access to see if the user has permission to see the BullMQ bull-board queue - console.log('SVELTE @jwt', token); if (!profile) return token; const dbUser = await DatabaseWrites.users.getOrCreateUser(profile); token.userId = dbUser.Id; - token.isSuperAdmin = await isUserSuperAdmin(dbUser.Id); + token.isSuperAdmin = !!dbUser.UserRoles.find((r) => r.RoleId === RoleId.SuperAdmin); return token; }, async session({ session, token }) { diff --git a/source/SIL.AppBuilder.Portal/src/node-server/index.ts b/source/SIL.AppBuilder.Portal/src/node-server/index.ts deleted file mode 100644 index 6dbcadbb4..000000000 --- a/source/SIL.AppBuilder.Portal/src/node-server/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ExpressAuthConfig, getSession } from '@auth/express'; -import Auth0Provider from '@auth/sveltekit/providers/auth0'; -import { createBullBoard } from '@bull-board/api'; -import { BullAdapter } from '@bull-board/api/bullAdapter.js'; -import { ExpressAdapter } from '@bull-board/express'; -import { Queue } from 'bullmq'; -import express, { NextFunction, Request, Response } from 'express'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -// Do not import any functional code from the sveltekit codebase -// unless you are positive you know what you are doing - -const app = express(); - -// Workaround for prisma expecting CommonJS (__dirname and __filename polyfill) -// Polyfill https://github.com/prisma/prisma/issues/15614#issuecomment-2126271831 -globalThis.__filename = fileURLToPath(import.meta.url); -globalThis.__dirname = path.dirname(__filename); - -const authConfig: ExpressAuthConfig = { - secret: process.env.VITE_AUTH0_SECRET, - providers: [ - Auth0Provider({ - id: 'auth0', - name: 'Auth0', - clientId: process.env.VITE_AUTH0_CLIENT_ID, - clientSecret: process.env.VITE_AUTH0_CLIENT_SECRET, - issuer: `https://${process.env.VITE_AUTH0_DOMAIN}/`, - wellKnown: `https://${process.env.VITE_AUTH0_DOMAIN}/.well-known/openid-configuration` - }) - ], - callbacks: { - async session({ session, token }) { - // I really don't like this. I ought to be able to check the jwt without passing this information - // to the client, but @auth/express doesn't seem to support it. Doesn't matter, it's like 20 bytes - // and it's not sensitive information to pass to the client. It will always say yes or not pass - // @ts-expect-error isSuperAdmin is not defined in source - session.user.isSuperAdmin = token.isSuperAdmin; - return session; - } - } -}; - -function verifyAuthenticated(requireAdmin: boolean) { - return async (req: Request, res: Response, next: NextFunction) => { - const session = res.locals.session ?? (await getSession(req, authConfig)); - if (!session?.user) { - res.redirect('/login'); - } else if (requireAdmin && !session?.user?.isSuperAdmin) { - res.status(403); - res.end('You do not have permission to access this resource'); - } else { - next(); - } - }; -} - -// No auth -app.get('/healthcheck', (req, res) => { - res.end('ok'); -}); - -// BullMQ variables -const jobQueue = new Queue('scriptoria', { - connection: { - host: 'redis' - } -}); -const serverAdapter = new ExpressAdapter(); -serverAdapter.setBasePath('/admin/jobs'); -createBullBoard({ - queues: [new BullAdapter(jobQueue)], - serverAdapter -}); - -// Require admin auth -app.use('/admin/jobs', verifyAuthenticated(true), serverAdapter.getRouter()); - -// build folder does not exist normally, but does during build -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -const handler = await import('./build/handler.js'); -// Svelte application handles authentication already, including login and logout -app.use(handler.handler); - -app.listen(3000, () => console.log('Server started!')); diff --git a/source/SIL.AppBuilder.Portal/src/node-server/tsconfig.json b/source/SIL.AppBuilder.Portal/src/node-server/tsconfig.json deleted file mode 100644 index 6772abe3a..000000000 --- a/source/SIL.AppBuilder.Portal/src/node-server/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "esModuleInterop": true, - "skipLibCheck": true, - "moduleResolution": "NodeNext", - "module": "NodeNext", - "outDir": "../../out" - } -} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/+layout.svelte index 45e110b11..ca87ed689 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/+layout.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/+layout.svelte @@ -20,10 +20,6 @@ ]; -
- -
- { const userId = (await locals.auth())?.user.userId; if (!userId) return error(400); - // const orgs = await getOrganizationsForUser(userId); const projects = await prisma.projects.findMany({ where: { IsPublic: true diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/flow/[businessflow]/[id]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/flow/[businessflow]/[id]/+page.server.ts index 8d3d19694..7ba5424f6 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/flow/[businessflow]/[id]/+page.server.ts +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/flow/[businessflow]/[id]/+page.server.ts @@ -1,4 +1,4 @@ -import prisma from '$lib/prisma'; +import { prisma } from 'sil.appbuilder.portal.common'; import type { PageServerLoad } from './$types'; export const load = (async ({ params }) => { diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/+page.server.ts index 1f745b6d8..163e2dd07 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/+page.server.ts +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/+page.server.ts @@ -1,10 +1,34 @@ import { redirect } from '@sveltejs/kit'; -import { getOrganizationsForUser } from 'sil.appbuilder.portal.common/prisma'; +import { prisma } from 'sil.appbuilder.portal.common'; +import { RoleId } from 'sil.appbuilder.portal.common/prisma'; import type { PageServerLoad } from './$types'; export const load = (async (event) => { const auth = await event.locals.auth(); - const organizations = await getOrganizationsForUser(auth!.user.userId); + const user = await prisma.users.findUnique({ + where: { + Id: auth?.user.userId + }, + include: { UserRoles: true, Organizations: true } + }); + const organizations = user?.UserRoles.find((roleDef) => roleDef.RoleId === RoleId.SuperAdmin) + ? await prisma.organizations.findMany({ + include: { + Owner: true + } + }) + : await prisma.organizations.findMany({ + where: { + OrganizationMemberships: { + every: { + UserId: auth?.user.userId + } + } + }, + include: { + Owner: true + } + }); if (organizations.length === 1) { return redirect(302, '/organizations/' + organizations[0].Id + '/settings'); } diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.server.ts index 1d221f79f..baec07805 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.server.ts +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.server.ts @@ -1,7 +1,6 @@ -import { updateProject } from '$lib/server/relationVerification/projects'; import { idSchema } from '$lib/valibot'; import { error } from '@sveltejs/kit'; -import { prisma } from 'sil.appbuilder.portal.common'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; import { fail, superValidate } from 'sveltekit-superforms'; import { valibot } from 'sveltekit-superforms/adapters'; import * as v from 'valibot'; @@ -57,7 +56,7 @@ export const actions: Actions = { const form = await superValidate(event.request, valibot(projectPropertyEditSchema)); if (!form.valid) return fail(400, { form, ok: false }); if (isNaN(parseInt(event.params.id))) return fail(400, { form, ok: false }); - const success = await updateProject(parseInt(event.params.id), { + const success = await DatabaseWrites.projects.update(parseInt(event.params.id), { Name: form.data.name, GroupId: form.data.group, OwnerId: form.data.owner,