generated from pantheon/chaos
Initial commit
This commit is contained in:
commit
a9734dc4a5
49 changed files with 2790 additions and 0 deletions
7
src/app.css
Normal file
7
src/app.css
Normal 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
24
src/app.d.ts
vendored
Normal 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
12
src/app.html
Normal 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
7
src/demo.spec.ts
Normal 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
5
src/hooks.server.ts
Normal 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
4
src/lib/i18n/index.ts
Normal 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
0
src/lib/index.ts
Normal file
34
src/lib/server/logger/index.ts
Normal file
34
src/lib/server/logger/index.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
1
src/lib/server/middleware/auth/index.ts
Normal file
1
src/lib/server/middleware/auth/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './validateSesssion';
|
||||
105
src/lib/server/middleware/auth/validateSesssion.ts
Normal file
105
src/lib/server/middleware/auth/validateSesssion.ts
Normal 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 });
|
||||
};
|
||||
}
|
||||
17
src/lib/server/prisma/index.ts
Normal file
17
src/lib/server/prisma/index.ts
Normal 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
20
src/routes/+error.svelte
Normal 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
23
src/routes/+layout.svelte
Normal 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
0
src/routes/+page.svelte
Normal file
Loading…
Add table
Add a link
Reference in a new issue