init commit

This commit is contained in:
Andrea
2026-03-20 16:56:03 +01:00
commit 6771126324
33 changed files with 7310 additions and 0 deletions

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,7 @@
<script lang="ts">
</script>
<style>
</style>

View 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>

View 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
View 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
View File

@@ -0,0 +1 @@
export type MediaProvider = 'youtube' | 'youtube-music' | 'spotify';

15
src/lib/server/auth.ts Normal file
View 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
});

View 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],
}),
}));

View 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 });

View 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
View 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)
});
}
)
}
});