init commit
This commit is contained in:
11
.env.example
Normal file
11
.env.example
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Drizzle
|
||||||
|
DATABASE_URL=file:local.db
|
||||||
|
|
||||||
|
ORIGIN=""
|
||||||
|
|
||||||
|
# Better Auth
|
||||||
|
# For production use 32 characters and generated with high entropy
|
||||||
|
# https://www.better-auth.com/docs/installation
|
||||||
|
BETTER_AUTH_SECRET=""
|
||||||
|
|
||||||
|
STORAGE_PATH="./storage"
|
||||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
# SQLite
|
||||||
|
*.db
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["svelte.svelte-vscode"]
|
||||||
|
}
|
||||||
1
AGENTS.md
Normal file
1
AGENTS.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- Non modificare mai i file direttamente, proponi la soluzione nella chat.
|
||||||
42
README.md
Normal file
42
README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# sv
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# create a new project
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
To recreate this project with the same configuration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# recreate this project
|
||||||
|
npx sv@0.12.8 create --template minimal --types ts --add sveltekit-adapter="adapter:auto" drizzle="database:sqlite+sqlite:libsql" better-auth="demo:password" devtools-json --install npm ./
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||||
11
drizzle.config.ts
Normal file
11
drizzle.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: './src/lib/server/db/schema.ts',
|
||||||
|
dialect: 'sqlite',
|
||||||
|
dbCredentials: { url: process.env.DATABASE_URL },
|
||||||
|
verbose: true,
|
||||||
|
strict: true
|
||||||
|
});
|
||||||
6590
package-lock.json
generated
Normal file
6590
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
package.json
Normal file
39
package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "gongnotes",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev --port 3000",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:migrate": "drizzle-kit migrate",
|
||||||
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"auth:schema": "better-auth generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@better-auth/cli": "~1.4.21",
|
||||||
|
"@libsql/client": "^0.17.0",
|
||||||
|
"@sveltejs/adapter-auto": "^7.0.0",
|
||||||
|
"@sveltejs/kit": "^2.50.2",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
|
"@types/node": "^22",
|
||||||
|
"better-auth": "~1.4.21",
|
||||||
|
"drizzle-kit": "^0.31.8",
|
||||||
|
"drizzle-orm": "^0.45.1",
|
||||||
|
"svelte": "^5.51.0",
|
||||||
|
"svelte-check": "^4.4.2",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.3.1",
|
||||||
|
"vite-plugin-devtools-json": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"imapflow": "^1.2.16",
|
||||||
|
"nanoid": "^5.1.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app.d.ts
vendored
Normal file
16
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { User, Session } from 'better-auth/minimal';
|
||||||
|
|
||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
interface Locals { user?: User; session?: Session }
|
||||||
|
|
||||||
|
// interface Error {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
11
src/app.html
Normal file
11
src/app.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<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>
|
||||||
34
src/hooks.server.ts
Normal file
34
src/hooks.server.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { redirect, type Handle } from '@sveltejs/kit';
|
||||||
|
import { building } from '$app/environment';
|
||||||
|
import { auth } from '$lib/server/auth';
|
||||||
|
import { svelteKitHandler } from 'better-auth/svelte-kit';
|
||||||
|
import { sequence } from '@sveltejs/kit/hooks';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleBetterAuth: Handle = async ({ event, resolve }) => {
|
||||||
|
const session = await auth.api.getSession({ headers: event.request.headers });
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
event.locals.session = session.session;
|
||||||
|
event.locals.user = session.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return svelteKitHandler({ event, resolve, auth, building });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRouting: Handle = async ({ event, resolve }) => {
|
||||||
|
const isLoginRoute = event.url.pathname === '/login';
|
||||||
|
|
||||||
|
if (!isLoginRoute && !event.locals.user) {
|
||||||
|
throw redirect(302, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoginRoute && event.locals.user) {
|
||||||
|
throw redirect(302, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handle: Handle = sequence(handleBetterAuth, handleRouting);
|
||||||
4
src/lib/assets/composer/file.svg
Normal file
4
src/lib/assets/composer/file.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9 17H15M9 13H15M9 9H10M13 3H8.2C7.0799 3 6.51984 3 6.09202 3.21799C5.71569 3.40973 5.40973 3.71569 5.21799 4.09202C5 4.51984 5 5.0799 5 6.2V17.8C5 18.9201 5 19.4802 5.21799 19.908C5.40973 20.2843 5.71569 20.5903 6.09202 20.782C6.51984 21 7.0799 21 8.2 21H15.8C16.9201 21 17.4802 21 17.908 20.782C18.2843 20.5903 18.5903 20.2843 18.782 19.908C19 19.4802 19 18.9201 19 17.8V9M13 3L19 9M13 3V7.4C13 7.96005 13 8.24008 13.109 8.45399C13.2049 8.64215 13.3578 8.79513 13.546 8.89101C13.7599 9 14.0399 9 14.6 9H19" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 828 B |
6
src/lib/assets/composer/image.svg
Normal file
6
src/lib/assets/composer/image.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M23 4C23 2.34315 21.6569 1 20 1H4C2.34315 1 1 2.34315 1 4V20C1 21.6569 2.34315 23 4 23H20C21.6569 23 23 21.6569 23 20V4ZM21 4C21 3.44772 20.5523 3 20 3H4C3.44772 3 3 3.44772 3 4V20C3 20.5523 3.44772 21 4 21H20C20.5523 21 21 20.5523 21 20V4Z" fill="#0F0F0F"/>
|
||||||
|
<path d="M4.80665 17.5211L9.1221 9.60947C9.50112 8.91461 10.4989 8.91461 10.8779 9.60947L14.0465 15.4186L15.1318 13.5194C15.5157 12.8476 16.4843 12.8476 16.8682 13.5194L19.1451 17.5039C19.526 18.1705 19.0446 19 18.2768 19H5.68454C4.92548 19 4.44317 18.1875 4.80665 17.5211Z" fill="#0F0F0F"/>
|
||||||
|
<path d="M18 8C18 9.10457 17.1046 10 16 10C14.8954 10 14 9.10457 14 8C14 6.89543 14.8954 6 16 6C17.1046 6 18 6.89543 18 8Z" fill="#0F0F0F"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 968 B |
BIN
src/lib/assets/favicon.ico
Normal file
BIN
src/lib/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
7
src/lib/components/Note.svelte
Normal file
7
src/lib/components/Note.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
34
src/lib/components/NoteEditor.svelte
Normal file
34
src/lib/components/NoteEditor.svelte
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { FileAttachment, ImageAttachment, Note } from "$lib/editor";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import imageSvg from "$lib/assets/composer/image.svg"
|
||||||
|
import fileSvg from "$lib/assets/composer/file.svg"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
note?: Note;
|
||||||
|
onSubmit: (note: Note) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { note, onSubmit } : Props = $props();
|
||||||
|
|
||||||
|
const palette = ["#ffffff", "#fef3c7", "#dbeafe", "#dcfce7", "#fee2e2", "#f3e8ff"];
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!note) {
|
||||||
|
note = {
|
||||||
|
id: nanoid(),
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
color: palette[0],
|
||||||
|
date: new Date(),
|
||||||
|
images: [],
|
||||||
|
files: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
28
src/lib/components/NoteNavbar.svelte
Normal file
28
src/lib/components/NoteNavbar.svelte
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Note from "./Note.svelte";
|
||||||
|
import NoteEditor from "./NoteEditor.svelte";
|
||||||
|
|
||||||
|
let expanded = $state(false);
|
||||||
|
|
||||||
|
const onCreate = (note: Note) => {
|
||||||
|
console.log("create note")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
onclick={() => {
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === "n" && e.metaKey) {
|
||||||
|
expanded = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if !expanded}
|
||||||
|
<div>Scrivi una nota...</div>
|
||||||
|
{:else}
|
||||||
|
<NoteEditor note={undefined} onSubmit={onCreate} />
|
||||||
|
{/if}
|
||||||
25
src/lib/editor.ts
Normal file
25
src/lib/editor.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export type Note = {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
color: string;
|
||||||
|
date: Date;
|
||||||
|
|
||||||
|
images: ImageAttachment[];
|
||||||
|
files: FileAttachment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type ImageAttachment = NoteAttachment & { kind: "image" };
|
||||||
|
export type FileAttachment = NoteAttachment & { kind: "file" };
|
||||||
|
|
||||||
|
interface NoteAttachment {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
mimeType: string;
|
||||||
|
previewUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
1
src/lib/media.ts
Normal file
1
src/lib/media.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type MediaProvider = 'youtube' | 'youtube-music' | 'spotify';
|
||||||
15
src/lib/server/auth.ts
Normal file
15
src/lib/server/auth.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { betterAuth } from 'better-auth/minimal';
|
||||||
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
||||||
|
import { sveltekitCookies } from 'better-auth/svelte-kit';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
import { getRequestEvent } from '$app/server';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { imapAuth } from '$lib/server/imap';
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
baseURL: env.ORIGIN,
|
||||||
|
secret: env.BETTER_AUTH_SECRET,
|
||||||
|
database: drizzleAdapter(db, { provider: 'sqlite' }),
|
||||||
|
emailAndPassword: { enabled: true },
|
||||||
|
plugins: [imapAuth(), sveltekitCookies(getRequestEvent)] // make sure sveltekitCookies is the last plugin in the array
|
||||||
|
});
|
||||||
107
src/lib/server/db/auth.schema.ts
Normal file
107
src/lib/server/db/auth.schema.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { relations, sql } from "drizzle-orm";
|
||||||
|
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
export const user = sqliteTable("user", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
email: text("email").notNull().unique(),
|
||||||
|
emailVerified: integer("email_verified", { mode: "boolean" })
|
||||||
|
.default(false)
|
||||||
|
.notNull(),
|
||||||
|
image: text("image"),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||||
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
|
.notNull(),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||||
|
.notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const session = sqliteTable(
|
||||||
|
"session",
|
||||||
|
{
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
token: text("token").notNull().unique(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||||
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
|
.notNull(),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||||
|
.notNull(),
|
||||||
|
ipAddress: text("ip_address"),
|
||||||
|
userAgent: text("user_agent"),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
},
|
||||||
|
(table) => [index("session_userId_idx").on(table.userId)],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const account = sqliteTable(
|
||||||
|
"account",
|
||||||
|
{
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
accountId: text("account_id").notNull(),
|
||||||
|
providerId: text("provider_id").notNull(),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
accessToken: text("access_token"),
|
||||||
|
refreshToken: text("refresh_token"),
|
||||||
|
idToken: text("id_token"),
|
||||||
|
accessTokenExpiresAt: integer("access_token_expires_at", {
|
||||||
|
mode: "timestamp_ms",
|
||||||
|
}),
|
||||||
|
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
|
||||||
|
mode: "timestamp_ms",
|
||||||
|
}),
|
||||||
|
scope: text("scope"),
|
||||||
|
password: text("password"),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||||
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
|
.notNull(),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(table) => [index("account_userId_idx").on(table.userId)],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const verification = sqliteTable(
|
||||||
|
"verification",
|
||||||
|
{
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
identifier: text("identifier").notNull(),
|
||||||
|
value: text("value").notNull(),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||||
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
|
.notNull(),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const userRelations = relations(user, ({ many }) => ({
|
||||||
|
sessions: many(session),
|
||||||
|
accounts: many(account),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const sessionRelations = relations(session, ({ one }) => ({
|
||||||
|
user: one(user, {
|
||||||
|
fields: [session.userId],
|
||||||
|
references: [user.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const accountRelations = relations(account, ({ one }) => ({
|
||||||
|
user: one(user, {
|
||||||
|
fields: [account.userId],
|
||||||
|
references: [user.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
10
src/lib/server/db/index.ts
Normal file
10
src/lib/server/db/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { drizzle } from 'drizzle-orm/libsql';
|
||||||
|
import { createClient } from '@libsql/client';
|
||||||
|
import * as schema from './schema';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
|
||||||
|
if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||||
|
|
||||||
|
const client = createClient({ url: env.DATABASE_URL });
|
||||||
|
|
||||||
|
export const db = drizzle(client, { schema });
|
||||||
34
src/lib/server/db/schema.ts
Normal file
34
src/lib/server/db/schema.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||||
|
import { user } from './auth.schema';
|
||||||
|
|
||||||
|
export * from './auth.schema';
|
||||||
|
|
||||||
|
export const notes = sqliteTable('notes', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
||||||
|
|
||||||
|
title: text('title').notNull(),
|
||||||
|
content: text('text').notNull(),
|
||||||
|
|
||||||
|
x: real("x").default(0).notNull(),
|
||||||
|
y: real("y").default(0).notNull(),
|
||||||
|
z: integer("z").default(1).notNull(),
|
||||||
|
|
||||||
|
color: text("color").default("#ffffff").notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => new Date()),
|
||||||
|
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const noteFiles = sqliteTable('note_files', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
noteId: text('note_id').notNull().references(() => notes.id, { onDelete: 'cascade' }),
|
||||||
|
|
||||||
|
kind: text('kind', { enum: ['image', 'file'] }).notNull().default('file'),
|
||||||
|
position: integer('position').notNull().default(0),
|
||||||
|
|
||||||
|
name: text('name').notNull(),
|
||||||
|
size: integer('size').notNull(),
|
||||||
|
mimeType: text('mime_type').notNull(),
|
||||||
|
|
||||||
|
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date())
|
||||||
|
});
|
||||||
138
src/lib/server/imap.ts
Normal file
138
src/lib/server/imap.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { createAuthEndpoint } from 'better-auth/api';
|
||||||
|
import { setSessionCookie } from 'better-auth/cookies';
|
||||||
|
import { parseUserOutput } from 'better-auth/db';
|
||||||
|
import { APIError } from 'better-call';
|
||||||
|
import { ImapFlow } from 'imapflow';
|
||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
const signInImapBodySchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
email: z.email(),
|
||||||
|
password: z.string().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function authenticateImap(user: string, password: string): Promise<boolean> {
|
||||||
|
const client = new ImapFlow({
|
||||||
|
host: 'mail.gongbao.it',
|
||||||
|
port: 993,
|
||||||
|
secure: true,
|
||||||
|
auth: {
|
||||||
|
user,
|
||||||
|
pass: password
|
||||||
|
},
|
||||||
|
logger: false
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
await client.logout();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('IMAP auth error:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const imapAuth = () => ({
|
||||||
|
id: 'imap-auth',
|
||||||
|
endpoints: {
|
||||||
|
signInImap: createAuthEndpoint(
|
||||||
|
'/sign-in/imap',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: signInImapBodySchema
|
||||||
|
},
|
||||||
|
async (ctx) => {
|
||||||
|
const name = ctx.body.name;
|
||||||
|
const email = ctx.body.email.toLowerCase();
|
||||||
|
const password = ctx.body.password;
|
||||||
|
|
||||||
|
const adapter = ctx.context.internalAdapter;
|
||||||
|
const existingUser = await adapter.findUserByEmail(email, { includeAccounts: true });
|
||||||
|
|
||||||
|
const credentialAccount = existingUser?.accounts.find((account) => account.providerId === 'credential');
|
||||||
|
const credentialPassword = credentialAccount?.password;
|
||||||
|
const isDbPasswordValid =
|
||||||
|
typeof credentialPassword === 'string' &&
|
||||||
|
(await ctx.context.password.verify({
|
||||||
|
hash: credentialPassword,
|
||||||
|
password
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (isDbPasswordValid && existingUser) {
|
||||||
|
const session = await adapter.createSession(existingUser.user.id);
|
||||||
|
if (!session) {
|
||||||
|
throw new APIError('INTERNAL_SERVER_ERROR', {
|
||||||
|
message: 'Failed to create session'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await setSessionCookie(ctx, {
|
||||||
|
session,
|
||||||
|
user: existingUser.user
|
||||||
|
});
|
||||||
|
|
||||||
|
return ctx.json({
|
||||||
|
token: session.token,
|
||||||
|
user: parseUserOutput(ctx.context.options, existingUser.user)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Fallback to IMAP auth.
|
||||||
|
const isImapValid = await authenticateImap(name, password);
|
||||||
|
if (!isImapValid) {
|
||||||
|
throw new APIError('UNAUTHORIZED', {
|
||||||
|
message: 'Invalid name or password'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) IMAP success: create/update local credential account.
|
||||||
|
const hash = await ctx.context.password.hash(password);
|
||||||
|
let user = existingUser?.user;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
user = await adapter.createUser({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
emailVerified: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await adapter.linkAccount({
|
||||||
|
userId: user.id,
|
||||||
|
providerId: 'credential',
|
||||||
|
accountId: user.id,
|
||||||
|
password: hash
|
||||||
|
});
|
||||||
|
} else if (credentialAccount) {
|
||||||
|
await adapter.updateAccount(credentialAccount.id, {
|
||||||
|
password: hash
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await adapter.linkAccount({
|
||||||
|
userId: user.id,
|
||||||
|
providerId: 'credential',
|
||||||
|
accountId: user.id,
|
||||||
|
password: hash
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await adapter.createSession(user.id);
|
||||||
|
if (!session) {
|
||||||
|
throw new APIError('INTERNAL_SERVER_ERROR', {
|
||||||
|
message: 'Failed to create session'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await setSessionCookie(ctx, {
|
||||||
|
session,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
|
||||||
|
return ctx.json({
|
||||||
|
token: session.token,
|
||||||
|
user: parseUserOutput(ctx.context.options, user)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
11
src/routes/+layout.svelte
Normal file
11
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import favicon from '$lib/assets/favicon.ico';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<link rel="icon" href={favicon} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
7
src/routes/+page.svelte
Normal file
7
src/routes/+page.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
31
src/routes/login/+page.server.ts
Normal file
31
src/routes/login/+page.server.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
|
import type { Actions } from './$types';
|
||||||
|
import { auth } from '$lib/server/auth';
|
||||||
|
import { APIError } from 'better-auth/api';
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
signInEmail: async (event) => {
|
||||||
|
const formData = await event.request.formData();
|
||||||
|
const name = formData.get('name')?.toString() ?? '';
|
||||||
|
const email = `${name}@gongbao.it`;
|
||||||
|
const password = formData.get('password')?.toString() ?? '';
|
||||||
|
try {
|
||||||
|
await auth.api.signInImap({
|
||||||
|
body: {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof APIError) {
|
||||||
|
return fail(400, { message: error.message || 'Signin failed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('IMAP signin error:', error);
|
||||||
|
return fail(500, { message: 'Unexpected error' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect(302, '/');
|
||||||
|
}
|
||||||
|
};
|
||||||
27
src/routes/login/+page.svelte
Normal file
27
src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import type { ActionData } from './$types';
|
||||||
|
|
||||||
|
let { form }: { form: ActionData } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>Login</h1>
|
||||||
|
|
||||||
|
<form method="post" action="?/signInEmail" use:enhance>
|
||||||
|
<label>
|
||||||
|
Login
|
||||||
|
<input type="text" name="name" required placeholder="user" />
|
||||||
|
<small>@gongbao.it</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Password
|
||||||
|
<input type="password" name="password" required />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="submit">Accedi</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#if form?.message}
|
||||||
|
<p style="color: red">{form.message}</p>
|
||||||
|
{/if}
|
||||||
3
static/robots.txt
Normal file
3
static/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# allow crawling everything by default
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
13
svelte.config.js
Normal file
13
svelte.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
kit: {
|
||||||
|
adapter: adapter()
|
||||||
|
},
|
||||||
|
vitePlugin: {
|
||||||
|
dynamicCompileOptions: ({ filename }) => filename.includes('node_modules') ? undefined : { runes: true }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rewriteRelativeImportExtensions": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||||
|
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||||
|
}
|
||||||
5
vite.config.ts
Normal file
5
vite.config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import devtoolsJson from 'vite-plugin-devtools-json';
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({ plugins: [sveltekit(), devtoolsJson()] });
|
||||||
Reference in New Issue
Block a user