Initial commit

This commit is contained in:
Pantheon 2025-04-02 15:57:39 -04:00
commit a9734dc4a5
49 changed files with 2790 additions and 0 deletions

7
src/app.css Normal file
View file

@ -0,0 +1,7 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
:root {
@apply text-base-content;
}

24
src/app.d.ts vendored Normal file
View file

@ -0,0 +1,24 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
import type { Tenant, User } from '@prisma/client';
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
interface Locals {
auth: {
userId?: string;
orgId?: string | null;
sessionId?: string;
};
user: User;
tenant: Tenant;
}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

7
src/demo.spec.ts Normal file
View file

@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

5
src/hooks.server.ts Normal file
View file

@ -0,0 +1,5 @@
import { validateSession } from '$lib/server/middleware';
import { sequence } from '@sveltejs/kit/hooks';
import { withClerkHandler } from 'clerk-sveltekit/server';
export const handle = sequence(withClerkHandler(), validateSession());

4
src/lib/i18n/index.ts Normal file
View file

@ -0,0 +1,4 @@
import * as runtime from '$lib/paraglide/runtime';
import { createI18n } from '@inlang/paraglide-sveltekit';
export * as messages from '$lib/paraglide/messages';
export const i18n = createI18n(runtime);

0
src/lib/index.ts Normal file
View file

View file

@ -0,0 +1,34 @@
import { NODE_ENV } from '$env/static/private';
import { type YogaLogger } from 'graphql-yoga';
import pino from 'pino';
export const logger = pino({
// Only use pino-pretty when NOT production
...(NODE_ENV !== 'production' && {
transport: {
target: 'pino-pretty',
options: {
colorize: true,
},
},
}),
});
export const yogaLogger: YogaLogger = {
debug(...args) {
// @ts-expect-error types dont match
logger.debug(...args);
},
info(...args) {
// @ts-expect-error types dont match
logger.info(...args);
},
warn(...args) {
// @ts-expect-error types dont match
logger.warn(...args);
},
error(...args) {
// @ts-expect-error types dont match
logger.error(...args);
},
};

View file

@ -0,0 +1 @@
export * from './validateSesssion';

View file

@ -0,0 +1,105 @@
import { logger } from '$lib/server/logger';
import { prisma } from '$lib/server/prisma';
import { type Handle } from '@sveltejs/kit';
import { clerkClient } from 'clerk-sveltekit/server';
const publicRoutes = ['/login'];
const loginRedirect = () =>
new Response(null, {
status: 307,
headers: {
location: '/login',
},
});
async function findOrCreateTenant(tenantClerkId: string) {
const tenant = await prisma.tenant.findUnique({
where: {
clerkOrganizationId: tenantClerkId,
},
});
if (tenant) {
return tenant;
}
const organization = await clerkClient.organizations.getOrganization({
organizationId: tenantClerkId,
});
return await prisma.tenant.create({
data: {
clerkOrganizationId: tenantClerkId,
name: organization.name,
slug: organization.slug ?? `tenant-${tenantClerkId}`,
},
});
}
export function validateSession(): Handle {
return async ({ event: { locals, url, ...rest }, resolve }) => {
// Public route? LET THEM PASS!
if (url !== null && publicRoutes.includes(url.pathname)) {
return resolve({ locals, url, ...rest });
}
// No session, redirect!
if (!locals.auth.sessionId) {
return loginRedirect();
}
// No user, revoke session and redirect!
if (!locals.auth.userId) {
await clerkClient.sessions.revokeSession(locals.auth.sessionId);
return loginRedirect();
}
// No org, signout and redirect!
if (!locals.auth.orgId) {
await clerkClient.sessions.revokeSession(locals.auth.sessionId);
return loginRedirect();
}
// Make sure that a tenant exists for the clerk org
const tenant = await findOrCreateTenant(locals.auth.orgId);
// Make sure a user exists for the clerk user
const clerkUser = await clerkClient.users.getUser(locals.auth.userId);
let user = await prisma.user.findFirst({
where: {
clerkId: clerkUser.id,
tenantId: tenant.id,
},
});
if (!user) {
if (clerkUser.emailAddresses.length === 0) {
logger.error('User has no email address');
await clerkClient.sessions.revokeSession(locals.auth.sessionId);
return loginRedirect();
}
user = await prisma.user.create({
data: {
clerkId: clerkUser.id,
email: clerkUser.emailAddresses[0].emailAddress,
name: clerkUser.fullName ?? '',
tenantId: tenant.id,
},
});
if (clerkUser.fullName === null) {
logger.warn(`User {${user.id}} has no name!`);
}
}
// Load user and tenant into locals
locals.user = user;
locals.tenant = tenant;
return resolve({ locals, url, ...rest });
};
}

View file

@ -0,0 +1,17 @@
import { PrismaClient } from '@prisma/client';
import { logger } from '../logger';
export const prisma = new PrismaClient({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'event', level: 'info' },
],
});
prisma.$on('query', (event) => {
logger.debug(`Query [${event.duration}ms]: ${event.query}`);
});
prisma.$on('info', (event) => {
logger.info(event.message);
});

20
src/routes/+error.svelte Normal file
View file

@ -0,0 +1,20 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { messages } from '$lib/i18n';
</script>
<main class="flex h-full w-full flex-col items-center justify-center gap-8">
<div class="flex items-center gap-2">
<h1 class="text-5xl font-bold tracking-widest text-white">
{page.status}
</h1>
<div class="divider divider-horizontal"></div>
<h1 class="text-2xl font-bold">
{page.error?.message}
</h1>
</div>
<button onclick={() => goto('/')} class="btn btn-outline btn-neutral btn-wide"
>{messages.error_page_go_home()}</button
>
</main>

23
src/routes/+layout.svelte Normal file
View file

@ -0,0 +1,23 @@
<script lang="ts">
import ClerkProvider from 'clerk-sveltekit/client/ClerkProvider.svelte';
import { i18n } from '$lib/i18n';
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
import '../app.css';
import { PUBLIC_CLERK_PUBLISHABLE_KEY } from '$env/static/public';
let { children } = $props();
</script>
<ParaglideJS {i18n}>
<ClerkProvider publishableKey={PUBLIC_CLERK_PUBLISHABLE_KEY}>
<div class="layout">
{@render children()}
</div>
</ClerkProvider>
</ParaglideJS>
<style>
.layout {
@apply h-screen w-screen p-8;
}
</style>

0
src/routes/+page.svelte Normal file
View file