feat: add Clerk Auth (#43)
This commit is contained in:
parent
1c5b37b24b
commit
ed2e18310e
17 changed files with 152 additions and 248 deletions
4
.env
4
.env
|
|
@ -5,3 +5,7 @@ TWILIO_PHONE_NUMBER=
|
||||||
|
|
||||||
# PRISMA
|
# PRISMA
|
||||||
DATABASE_URL="postgres://hestia:test123@localhost:5432/hestia"
|
DATABASE_URL="postgres://hestia:test123@localhost:5432/hestia"
|
||||||
|
|
||||||
|
# CLERK
|
||||||
|
PUBLIC_CLERK_PUBLISHABLE_KEY=secret_do_not_commit_or_change_this_create_.env.local_instead
|
||||||
|
CLERK_SECRET_KEY=secret_do_not_commit_or_change_this_create_.env.local_instead
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,6 +12,7 @@ node_modules
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
.idea
|
||||||
|
|
||||||
# Vite
|
# Vite
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
|
|
|
||||||
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
10
package.json
10
package.json
|
|
@ -3,10 +3,10 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun validate-env && bun database:up && bun prisma:push && vite dev",
|
"dev": "bun validate-env && bun database:up && bun prisma:generate && vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build",
|
||||||
"database:up": "docker compose -p hestia -f devops/docker-compose.dev.yml up -d && docker compose -p hestia -f devops/docker-compose.dev.yml -f devops/docker-compose.wait.yml run --rm wait -c hestia-database:5432",
|
"database:up": "docker compose -p hestia -f devops/docker-compose.dev.yml up -d && docker compose -p hestia -f devops/docker-compose.dev.yml -f devops/docker-compose.wait.yml run --rm wait -c hestia-database:5432 && bun prisma:push",
|
||||||
"database:down": "docker compose -p hestia -f devops/docker-compose.dev.yml down",
|
"database:down": "docker compose -p hestia -f devops/docker-compose.dev.yml down",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
|
@ -65,19 +65,19 @@
|
||||||
"vitest": "^2.0.4"
|
"vitest": "^2.0.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@clerk/backend": "1.21.4",
|
||||||
|
"@clerk/themes": "^2.2.3",
|
||||||
"@flaticon/flaticon-uicons": "^3.3.1",
|
"@flaticon/flaticon-uicons": "^3.3.1",
|
||||||
"@inlang/paraglide-sveltekit": "^0.15.0",
|
"@inlang/paraglide-sveltekit": "^0.15.0",
|
||||||
"@lucia-auth/adapter-prisma": "^4.0.1",
|
|
||||||
"@pothos/core": "^4.3.0",
|
"@pothos/core": "^4.3.0",
|
||||||
"@pothos/plugin-prisma": "^4.4.0",
|
"@pothos/plugin-prisma": "^4.4.0",
|
||||||
"@prisma/client": "6.0.1",
|
"@prisma/client": "6.0.1",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"clerk-sveltekit": "https://pkg.pr.new/wobsoriano/clerk-sveltekit@ca15d4e",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"eslint_d": "^14.3.0",
|
"eslint_d": "^14.3.0",
|
||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"graphql-yoga": "^5.10.4",
|
"graphql-yoga": "^5.10.4",
|
||||||
"lucia": "^3.2.2",
|
|
||||||
"oslo": "^1.2.1",
|
|
||||||
"pino": "^9.5.0",
|
"pino": "^9.5.0",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `password` on the `User` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- A unique constraint covering the columns `[clerkId]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Added the required column `clerkId` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "User_email_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" DROP COLUMN "password",
|
||||||
|
ADD COLUMN "clerkId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "Session";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_clerkId_key" ON "User"("clerkId");
|
||||||
|
|
@ -16,23 +16,11 @@ datasource db {
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
clerkId String @unique
|
||||||
|
|
||||||
email String? @unique
|
email String?
|
||||||
name String
|
name String
|
||||||
password String
|
|
||||||
posts Post[]
|
posts Post[]
|
||||||
sessions Session[]
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
model Session {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
|
|
||||||
expiresAt DateTime
|
|
||||||
user User @relation(references: [id], fields: [userId])
|
|
||||||
userId String
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
|
||||||
7
src/app.d.ts
vendored
7
src/app.d.ts
vendored
|
|
@ -5,8 +5,11 @@ declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
user: import('lucia').User | null;
|
auth: {
|
||||||
session: import('lucia').Session | null;
|
userId?: string;
|
||||||
|
orgId?: string | null;
|
||||||
|
sessionId?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
|
|
|
||||||
3
src/hooks.server.ts
Normal file
3
src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { withClerkHandler } from 'clerk-sveltekit/server';
|
||||||
|
|
||||||
|
export const handle = withClerkHandler();
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { messages } from '$lib/i18n';
|
import { messages } from '$lib/i18n';
|
||||||
|
import SignOutButton from 'clerk-sveltekit/client/SignOutButton.svelte';
|
||||||
|
|
||||||
let { title, username }: { title: string; username: string } = $props();
|
let { title, username }: { title: string; username: string } = $props();
|
||||||
|
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
<h2 class="prose prose-xl">Hestia</h2>
|
<h2 class="prose prose-xl">Hestia</h2>
|
||||||
<h1 class="prose prose-2xl">{title}</h1>
|
<h1 class="prose prose-2xl">{title}</h1>
|
||||||
<p class="prose prose-lg">{message}</p>
|
<p class="prose prose-lg">{message}</p>
|
||||||
|
<SignOutButton />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,55 @@
|
||||||
import { redirect, type ServerLoadEvent } from '@sveltejs/kit';
|
import { redirect, type ServerLoadEvent } from '@sveltejs/kit';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { prisma } from '../prisma';
|
import { prisma } from '../prisma';
|
||||||
|
import { createClerkClient } from '@clerk/backend';
|
||||||
|
import { CLERK_SECRET_KEY } from '$env/static/private';
|
||||||
|
import { clerkClient } from 'clerk-sveltekit/server';
|
||||||
|
import { logger } from '$lib/server/logger';
|
||||||
|
|
||||||
export async function validateSession(event: ServerLoadEvent) {
|
const clerkSessionClient = createClerkClient({
|
||||||
const sessionId = event.cookies.get('auth_session');
|
secretKey: CLERK_SECRET_KEY,
|
||||||
if (!sessionId) {
|
|
||||||
redirect(302, '/login');
|
|
||||||
}
|
|
||||||
const session = await prisma.session.findUnique({
|
|
||||||
where: { id: sessionId },
|
|
||||||
include: { user: true },
|
|
||||||
});
|
});
|
||||||
if (!session || !session.user) {
|
|
||||||
redirect(302, '/login');
|
export async function validateSession({ locals }: ServerLoadEvent) {
|
||||||
|
if (!locals.auth.userId || !locals.auth.sessionId) {
|
||||||
|
return redirect(307, '/login');
|
||||||
}
|
}
|
||||||
const expiry = session.expiresAt;
|
|
||||||
if (dayjs(expiry).isBefore(dayjs())) {
|
if (!locals.auth.orgId && locals.auth.sessionId) {
|
||||||
redirect(302, '/login');
|
// Sign out the user if they are not associated with an organization
|
||||||
|
await clerkSessionClient.sessions.revokeSession(locals.auth.sessionId);
|
||||||
|
return redirect(307, '/login');
|
||||||
}
|
}
|
||||||
return session;
|
|
||||||
|
const clerkUser = await clerkClient.users.getUser(locals.auth.userId);
|
||||||
|
|
||||||
|
let user = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
clerkId: clerkUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
if (clerkUser.emailAddresses.length === 0) {
|
||||||
|
logger.error('User has no email address');
|
||||||
|
await clerkSessionClient.sessions.revokeSession(locals.auth.sessionId);
|
||||||
|
|
||||||
|
return redirect(307, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
clerkId: clerkUser.id,
|
||||||
|
email: clerkUser.emailAddresses[0].emailAddress,
|
||||||
|
name: clerkUser.fullName ?? '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clerkUser.fullName === null) {
|
||||||
|
logger.error('User has no name');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: { name: user.name },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { Lucia } from 'lucia';
|
|
||||||
import { PrismaAdapter } from '@lucia-auth/adapter-prisma';
|
|
||||||
import { prisma } from '$lib/server/prisma';
|
|
||||||
|
|
||||||
const adapter = new PrismaAdapter(prisma.session, prisma.user);
|
|
||||||
// expect error (see next section)
|
|
||||||
export const auth = new Lucia(adapter, {
|
|
||||||
sessionCookie: {
|
|
||||||
attributes: {
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getUserAttributes: (attributes) => {
|
|
||||||
return {
|
|
||||||
email: attributes.email,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
declare module 'lucia' {
|
|
||||||
interface Register {
|
|
||||||
Lucia: typeof Lucia;
|
|
||||||
DatabaseUserAttributes: DatabaseUserAttributes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DatabaseUserAttributes {
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Auth = typeof auth;
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import ClerkProvider from 'clerk-sveltekit/client/ClerkProvider.svelte';
|
||||||
import { i18n } from '$lib/i18n';
|
import { i18n } from '$lib/i18n';
|
||||||
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
|
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
import { PUBLIC_CLERK_PUBLISHABLE_KEY } from '$env/static/public';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
|
<ClerkProvider publishableKey={PUBLIC_CLERK_PUBLISHABLE_KEY}>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
</ClerkProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ParaglideJS {i18n}>
|
<ParaglideJS {i18n}>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { validateSession } from '$lib/server/auth';
|
import { validateSession } from '$lib/server/auth';
|
||||||
|
|
||||||
export async function load(event) {
|
export const load = async (event) => validateSession(event);
|
||||||
await validateSession(event);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,3 @@
|
||||||
import { validateSession } from '$lib/server/auth';
|
import { validateSession } from '$lib/server/auth';
|
||||||
|
|
||||||
export async function load(event) {
|
export const load = async (event) => validateSession(event);
|
||||||
const {
|
|
||||||
user: { password: _, ...rest },
|
|
||||||
} = await validateSession(event);
|
|
||||||
return {
|
|
||||||
user: rest,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
import { Navbar } from '$lib/components/Navigation';
|
import { Navbar } from '$lib/components/Navigation';
|
||||||
import type { User } from '@prisma/client';
|
import type { User } from '@prisma/client';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import 'clerk-sveltekit/client';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
|
|
@ -11,6 +13,35 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
let { children, data }: Props = $props();
|
let { children, data }: Props = $props();
|
||||||
|
|
||||||
|
let clerk;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
clerk = window.Clerk;
|
||||||
|
|
||||||
|
if (clerk) {
|
||||||
|
if (!clerk.user || clerk.user?.organizationMemberships.length === 0) {
|
||||||
|
console.debug('No organizations found');
|
||||||
|
await clerk.signOut();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active organization to the first one in the list, this is temporary until we let the user choose the organization
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
// Set the active organization to the first one in the list
|
||||||
|
await clerk.setActive({
|
||||||
|
organization: clerk.user?.organizationMemberships[0].organization.id,
|
||||||
|
});
|
||||||
|
console.debug(
|
||||||
|
`Active organization set to ${clerk.user?.organizationMemberships[0].organization.id}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting active organization:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar title="Svelte" username={data.user.name} />
|
<Navbar title="Svelte" username={data.user.name} />
|
||||||
|
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
import { messages } from '$lib/i18n';
|
|
||||||
import { logger } from '$lib/server/logger';
|
|
||||||
import { auth } from '$lib/server/lucia.js';
|
|
||||||
import { prisma } from '$lib/server/prisma';
|
|
||||||
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
|
||||||
import { Argon2id } from 'oslo/password';
|
|
||||||
import { string } from 'zod';
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
login: async (event) => {
|
|
||||||
const form = await event.request.formData();
|
|
||||||
const email = form.get('email');
|
|
||||||
if (typeof email !== 'string') {
|
|
||||||
return fail(400, { email: { error: messages.login_error_email_required() } });
|
|
||||||
}
|
|
||||||
|
|
||||||
const password = form.get('password') as string;
|
|
||||||
if (!password) {
|
|
||||||
return fail(400, { password: { error: messages.login_error_password_required() } });
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
email: email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!user) {
|
|
||||||
logger.error(`User not found! ${email}`);
|
|
||||||
return fail(404, {
|
|
||||||
email: { value: email, error: messages.login_error_user_not_found() },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const validPassword = await new Argon2id().verify(user.password, password);
|
|
||||||
if (!validPassword) {
|
|
||||||
return fail(400, {
|
|
||||||
password: { error: messages.login_error_password_incorrect() },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const session = await auth.createSession(user.id, []);
|
|
||||||
const sessionCookie = auth.createSessionCookie(session.id);
|
|
||||||
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
|
||||||
path: '/',
|
|
||||||
maxAge: 120,
|
|
||||||
});
|
|
||||||
redirect(302, '/');
|
|
||||||
},
|
|
||||||
|
|
||||||
register: async (event) => {
|
|
||||||
const form = await event.request.formData();
|
|
||||||
if (!form.has('email')) {
|
|
||||||
return fail(400, { email: { error: messages.login_error_email_required() } });
|
|
||||||
}
|
|
||||||
const { success, data: email, error } = string().email().safeParse(form.get('email'));
|
|
||||||
if (!success) {
|
|
||||||
logger.error(error);
|
|
||||||
return fail(400, {
|
|
||||||
email: { value: email, error: messages.login_error_email_incorrect() },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const password = form.get('password');
|
|
||||||
if (typeof password !== 'string') {
|
|
||||||
return fail(400, { password: { error: messages.login_error_password_required() } });
|
|
||||||
}
|
|
||||||
const name = form.get('name');
|
|
||||||
if (typeof name !== 'string') {
|
|
||||||
return fail(400, { name: { error: messages.login_error_name_required() } });
|
|
||||||
}
|
|
||||||
|
|
||||||
const usersWithEmail = await prisma.user.count({ where: { email: email } });
|
|
||||||
if (usersWithEmail !== 0) {
|
|
||||||
return fail(409, {
|
|
||||||
email: { value: email, error: messages.login_error_email_inuse() },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedPassword = await new Argon2id().hash(password);
|
|
||||||
const user = await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
email: form.get('email') as string,
|
|
||||||
name: form.get('name') as string,
|
|
||||||
password: hashedPassword,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const session = await auth.createSession(user.id.toString(), {});
|
|
||||||
const sessionCookie = auth.createSessionCookie(session.id);
|
|
||||||
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
|
||||||
path: '/',
|
|
||||||
maxAge: 120,
|
|
||||||
});
|
|
||||||
|
|
||||||
redirect(303, '/');
|
|
||||||
},
|
|
||||||
} satisfies Actions;
|
|
||||||
|
|
@ -1,78 +1,24 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { TextInput } from '$lib/components/DataInput';
|
|
||||||
import { Button } from '$lib/components/Actions';
|
|
||||||
import Tabs from '$lib/components/Navigation/Tabs';
|
|
||||||
import { messages } from '$lib/i18n/index.js';
|
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
import SignIn from 'clerk-sveltekit/client/SignIn.svelte';
|
||||||
let { form } = $props();
|
import { dark } from '@clerk/themes';
|
||||||
|
|
||||||
let tab: 0 | 1 = $state(0);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet userIcon()}
|
|
||||||
<i class="fi fi-br-envelope"></i>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet passwordIcon()}
|
|
||||||
<i class="fi fi-br-key"></i>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet nameIcon()}
|
|
||||||
<i class="fi fi-rr-user"></i>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet alert(message: string)}
|
|
||||||
<div role="alert" class="alert alert-error absolute -top-20" transition:fade>
|
|
||||||
<i class="fi fi-bs-octagon-xmark h-6 w-6 shrink-0"></i>
|
|
||||||
<span>{message}</span>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet Form(variant: 'login' | 'register')}
|
|
||||||
<form method="POST" action={`?/${variant}`}>
|
|
||||||
<div class="card-body gap-4">
|
|
||||||
<TextInput
|
|
||||||
start={userIcon}
|
|
||||||
placeholder={messages.login_label_email()}
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
start={passwordIcon}
|
|
||||||
placeholder={messages.login_label_password()}
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
{#if variant === 'register'}
|
|
||||||
<TextInput
|
|
||||||
start={nameIcon}
|
|
||||||
placeholder={messages.login_label_name()}
|
|
||||||
name="name"
|
|
||||||
fade
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="card-actions px-4">
|
|
||||||
<Button block type="submit" outline>{messages.login_button_submit()}</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<div class="page" transition:fade>
|
<div class="page" transition:fade>
|
||||||
<div class="card bg-base-200 py-4 shadow-xl">
|
<div>
|
||||||
<div class="card-title">
|
<SignIn
|
||||||
{#if form}
|
appearance={{
|
||||||
{@render alert(Object.values(form)[0].error)}
|
baseTheme: dark,
|
||||||
{/if}
|
variables: {
|
||||||
<Tabs
|
colorPrimary: '#FFF',
|
||||||
variant="bordered"
|
},
|
||||||
bind:selected={tab}
|
elements: {
|
||||||
tabs={[messages.login_tab_login(), messages.login_tab_register()]}
|
// Remove the sign-up link
|
||||||
|
footerAction: { display: 'none' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{@render Form(tab === 0 ? 'login' : 'register')}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue