├── .npmrc ├── frontend ├── .npmrc ├── src │ ├── lib │ │ ├── utils │ │ │ ├── navigate.ts │ │ │ ├── api.ts │ │ │ ├── cn.ts │ │ │ └── error.ts │ │ ├── components │ │ │ ├── ui │ │ │ │ ├── sonner │ │ │ │ │ ├── index.ts │ │ │ │ │ └── sonner.svelte │ │ │ │ ├── input │ │ │ │ │ ├── index.ts │ │ │ │ │ └── input.svelte │ │ │ │ ├── label │ │ │ │ │ ├── index.ts │ │ │ │ │ └── label.svelte │ │ │ │ └── button │ │ │ │ │ ├── index.ts │ │ │ │ │ └── button.svelte │ │ │ └── base │ │ │ │ ├── teal-background.svelte │ │ │ │ └── big-quote.svelte │ │ ├── stores │ │ │ └── user.svelte.ts │ │ └── hooks │ │ │ └── is-mobile.svelte.ts │ ├── routes │ │ ├── +layout.ts │ │ ├── (root) │ │ │ ├── +page.svelte │ │ │ └── page.svelte.ts │ │ ├── +layout.svelte │ │ ├── sign-in │ │ │ ├── page.svelte.ts │ │ │ └── +page.svelte │ │ ├── sign-up │ │ │ ├── page.svelte.ts │ │ │ └── +page.svelte │ │ └── layout.svelte.ts │ ├── app.d.ts │ ├── app.html │ └── app.css ├── .prettierignore ├── static │ └── favicon.png ├── .gitignore ├── .prettierrc ├── vite.config.ts ├── svelte.config.js ├── components.json ├── tsconfig.json ├── eslint.config.js └── package.json ├── bun.lockb ├── backend ├── src │ ├── lib │ │ ├── utils │ │ │ ├── factory.ts │ │ │ ├── validators.ts │ │ │ └── generator.ts │ │ └── types │ │ │ ├── worker.d.ts │ │ │ └── app.d.ts │ ├── api │ │ ├── hello.ts │ │ ├── sign-out.ts │ │ ├── sign-in.ts │ │ └── sign-up.ts │ ├── services │ │ ├── auth │ │ │ ├── index.ts │ │ │ ├── authorization-middleware.ts │ │ │ ├── authentication-middleware.ts │ │ │ ├── password.ts │ │ │ └── token.ts │ │ ├── db │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ └── error │ │ │ └── index.ts │ ├── router.ts │ └── index.ts ├── migrations │ ├── meta │ │ ├── _journal.json │ │ └── 0000_snapshot.json │ └── 0000_minor_loners.sql ├── drizzle.config.ts ├── tsconfig.json ├── .gitignore ├── wrangler.jsonc └── package.json ├── package.json ├── LICENSE ├── README.md └── .gitignore /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /frontend/src/lib/utils/navigate.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MosheRivkin/hono-svelte-5/HEAD/bun.lockb -------------------------------------------------------------------------------- /frontend/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | export const ssr = false; 3 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /frontend/src/lib/components/ui/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Toaster } from "./sonner.svelte"; 2 | -------------------------------------------------------------------------------- /frontend/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MosheRivkin/hono-svelte-5/HEAD/frontend/static/favicon.png -------------------------------------------------------------------------------- /frontend/src/lib/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./input.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Input, 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/src/lib/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./label.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Label, 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/src/lib/utils/api.ts: -------------------------------------------------------------------------------- 1 | import { hc } from 'hono/client'; 2 | import type { AppType } from '@backend/index'; 3 | 4 | export const { api } = hc('/'); 5 | -------------------------------------------------------------------------------- /backend/src/lib/utils/factory.ts: -------------------------------------------------------------------------------- 1 | import { createFactory } from "hono/factory"; 2 | import { Env } from "@backend/lib/types/app"; 3 | 4 | export const factory = createFactory(); 5 | -------------------------------------------------------------------------------- /backend/src/api/hello.ts: -------------------------------------------------------------------------------- 1 | import { factory } from "@backend/lib/utils/factory"; 2 | 3 | export const hello = factory.createHandlers(async (c) => { 4 | return c.text("Hello, Hono!"); 5 | }); 6 | -------------------------------------------------------------------------------- /frontend/src/lib/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/lib/utils/validators.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | export const validators = { 3 | name: () => z.string().max(255), 4 | email: () => z.string().email().max(255), 5 | password: () => z.string().min(8).max(255), 6 | }; 7 | -------------------------------------------------------------------------------- /backend/src/lib/types/worker.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by Wrangler by running `wrangler types --env-interface WorkerEnv ./src/lib/types/worker.d.ts` 2 | 3 | interface WorkerEnv { 4 | KV: KVNamespace; 5 | JWT_SECRET: string; 6 | DB: D1Database; 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/services/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { passwordActions } from "@backend/services/auth/password"; 2 | import { tokenActions } from "@backend/services/auth/token"; 3 | 4 | export const auth = { 5 | password: passwordActions, 6 | token: tokenActions, 7 | }; 8 | -------------------------------------------------------------------------------- /backend/src/api/sign-out.ts: -------------------------------------------------------------------------------- 1 | import { factory } from "@backend/lib/utils/factory"; 2 | import { auth } from "@backend/services/auth"; 3 | 4 | export const signOut = factory.createHandlers(async (c) => { 5 | auth.token.delete(c); 6 | return c.json(null); 7 | }); 8 | -------------------------------------------------------------------------------- /frontend/src/routes/(root)/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /backend/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1741196989676, 9 | "tag": "0000_minor_loners", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /frontend/src/lib/stores/user.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { EnvUser } from '@backend/lib/types/app'; 2 | 3 | class UserStore { 4 | user = $state(null); 5 | setUser(user: EnvUser) { 6 | this.user = user; 7 | } 8 | } 9 | 10 | const userStore = new UserStore(); 11 | export default userStore; 12 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 200, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /frontend/src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import Root, { 2 | type ButtonProps, 3 | type ButtonSize, 4 | type ButtonVariant, 5 | buttonVariants, 6 | } from "./button.svelte"; 7 | 8 | export { 9 | Root, 10 | type ButtonProps as Props, 11 | // 12 | Root as Button, 13 | buttonVariants, 14 | type ButtonProps, 15 | type ButtonSize, 16 | type ButtonVariant, 17 | }; 18 | -------------------------------------------------------------------------------- /backend/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | 3 | export default defineConfig({ 4 | schema: "./src/services/db/schema.ts", 5 | out: "./migrations", 6 | dialect: "sqlite", 7 | dbCredentials: { 8 | url: ".wrangler/state/v3/d1/miniflare-D1DatabaseObject/a86f68350a2049cfb9ccc403a4d90a90ce260fbee30ba8a76dc6e8322c15ea1e.sqlite", 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite'; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit(), tailwindcss()], 7 | server: { 8 | proxy: { 9 | '/api': { 10 | target: 'http://localhost:8000', 11 | changeOrigin: true 12 | } 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: vitePreprocess(), 7 | 8 | kit: { 9 | adapter: adapter({ 10 | fallback: 'index.html' 11 | }), 12 | alias: { 13 | '@/*': './src/*' 14 | } 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /backend/src/router.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { hello } from "@backend/api/hello"; 3 | import { signUp } from "@backend/api/sign-up"; 4 | import { signIn } from "@backend/api/sign-in"; 5 | import { signOut } from "./api/sign-out"; 6 | 7 | export const router = new Hono() 8 | // 9 | .get("/hello", ...hello) 10 | .post("/user/signUp", ...signUp) 11 | .post("/user/signOut", ...signOut) 12 | .post("/user/signIn", ...signIn); 13 | -------------------------------------------------------------------------------- /backend/src/lib/types/app.d.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | import { DB } from "@backend/services/db"; 3 | import { User } from "@backend/services/db/schema"; 4 | import { StatusCode } from "hono/utils/http-status"; 5 | 6 | type EnvUser = Omit | null; 7 | 8 | interface Env { 9 | Bindings: WorkerEnv & {}; 10 | Variables: { 11 | db: DB; 12 | user: EnvUser; 13 | }; 14 | } 15 | 16 | type Context = Context; 17 | -------------------------------------------------------------------------------- /frontend/src/lib/components/base/teal-background.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
8 | {@render children()} 9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /backend/src/services/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/d1"; 2 | import { factory } from "@backend/lib/utils/factory"; 3 | import * as schema from "@backend/services/db/schema"; 4 | 5 | export type DB = ReturnType; 6 | 7 | const createDB = (binding: D1Database) => { 8 | return drizzle(binding, { schema }); 9 | }; 10 | 11 | export const dbMiddleware = factory.createMiddleware((c, next) => { 12 | c.set("db", createDB(c.env.DB)); 13 | return next(); 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://next.shadcn-svelte.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.ts", 6 | "css": "src\\app.css", 7 | "baseColor": "slate" 8 | }, 9 | "aliases": { 10 | "components": "@/lib/components", 11 | "utils": "@/lib/utils/cn", 12 | "ui": "@/lib/components/ui", 13 | "hooks": "@/lib/hooks" 14 | }, 15 | "typescript": true, 16 | "registry": "https://next.shadcn-svelte.com/registry" 17 | } 18 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "lib": [ 9 | "ESNext" 10 | ], 11 | "types": [ 12 | "@cloudflare/workers-types/2023-07-01" 13 | ], 14 | "jsx": "react-jsx", 15 | "jsxImportSource": "hono/jsx", 16 | "paths": { 17 | "@backend/*": [ 18 | "./src/*" 19 | ] 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # prod 2 | dist/ 3 | 4 | # dev 5 | .yarn/ 6 | !.yarn/releases 7 | .vscode/* 8 | !.vscode/launch.json 9 | !.vscode/*.code-snippets 10 | .idea/workspace.xml 11 | .idea/usage.statistics.xml 12 | .idea/shelf 13 | 14 | # deps 15 | node_modules/ 16 | .wrangler 17 | 18 | # env 19 | .env 20 | .env.production 21 | .dev.vars 22 | 23 | # logs 24 | logs/ 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | pnpm-debug.log* 30 | lerna-debug.log* 31 | 32 | # misc 33 | .DS_Store 34 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler", 13 | "paths": { 14 | "@backend/*": [ 15 | "../backend/src/*" 16 | ], 17 | "@/*": [ 18 | "./src/*" 19 | ] 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /backend/src/lib/utils/generator.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from "nanoid"; 2 | 3 | export const gen = { 4 | id: () => nanoid(32), 5 | ms_of_30_minutes: () => 30 * 60 * 1000, 6 | ms_of_24_hours: () => 24 * 60 * 60 * 1000, 7 | ms_of_7_days: () => 7 * 24 * 60 * 60 * 1000, 8 | x_hours_from_now_in_sec: (x: number) => Date.now() / 1000 + x * 60 * 60, 9 | x_hours_from_now_in_ms: (x: number) => Date.now() + x * 60 * 60 * 1000, 10 | x_days_from_now_in_sec: (x: number) => Date.now() / 1000 + x * 60 * 60 * 24, 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/src/lib/components/ui/label/label.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /frontend/src/routes/(root)/page.svelte.ts: -------------------------------------------------------------------------------- 1 | import { goto } from '$app/navigation'; 2 | import { api } from '@/lib/utils/api'; 3 | import { toastResponseError } from '@/lib/utils/error'; 4 | 5 | class RootPageHandler { 6 | signOut = async () => { 7 | const serverRes = await api.user.signOut.$post(); 8 | const serverResData = await serverRes.json(); 9 | if (!serverRes.ok) { 10 | toastResponseError(serverResData); 11 | return; 12 | } 13 | goto('/'); 14 | }; 15 | } 16 | 17 | export const rootPageHandler = new RootPageHandler(); 18 | -------------------------------------------------------------------------------- /frontend/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | {@render children()} 17 |
18 | 19 | -------------------------------------------------------------------------------- /frontend/src/lib/components/base/big-quote.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 | " 8 |

9 | {@render children()} 10 |

11 | " 12 |
13 |
∼ {author} ∽
14 |
15 | -------------------------------------------------------------------------------- /frontend/src/routes/sign-in/page.svelte.ts: -------------------------------------------------------------------------------- 1 | import { goto } from '$app/navigation'; 2 | import { api } from '@/lib/utils/api'; 3 | import { toastResponseError } from '@/lib/utils/error'; 4 | 5 | class SighInPageHandler { 6 | email = $state(''); 7 | password = $state(''); 8 | 9 | signIn = async () => { 10 | const serverRes = await api.user.signIn.$post({ json: { email: this.email, password: this.password } }); 11 | const serverResData = await serverRes.json(); 12 | if (!serverRes.ok) { 13 | toastResponseError(serverResData); 14 | return; 15 | } 16 | goto('/'); 17 | }; 18 | } 19 | 20 | export const sighInPageHandler = new SighInPageHandler(); 21 | -------------------------------------------------------------------------------- /frontend/src/lib/utils/error.ts: -------------------------------------------------------------------------------- 1 | import { toast } from 'svelte-sonner'; 2 | import { ZodError } from 'zod'; 3 | 4 | export function toastResponseError(data: any) { 5 | if (data.error?.issues?.length) { 6 | const issues = (data.error as ZodError).issues; 7 | issues.forEach((issue, i) => { 8 | setTimeout(() => { 9 | toast.error(`Error at ${issue.path} value: ${issue.message}`); 10 | }, i * 500); 11 | }); 12 | return; 13 | } 14 | 15 | if (data?.error?.message) { 16 | toast.error(data.error.message); 17 | return; 18 | } 19 | 20 | if (typeof data.error === 'string') { 21 | toast.error(data.error); 22 | return; 23 | } 24 | toast.error('An error occurred'); 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/routes/sign-up/page.svelte.ts: -------------------------------------------------------------------------------- 1 | import { goto } from '$app/navigation'; 2 | import { api } from '@/lib/utils/api'; 3 | import { toastResponseError } from '@/lib/utils/error'; 4 | 5 | class SighUpPageHandler { 6 | name = $state(''); 7 | email = $state(''); 8 | password = $state(''); 9 | 10 | signUp = async () => { 11 | const serverRes = await api.user.signUp.$post({ json: { email: this.email, password: this.password, name: this.name } }); 12 | const serverResData = await serverRes.json(); 13 | if (!serverRes.ok) { 14 | toastResponseError(serverResData); 15 | return; 16 | } 17 | goto('/'); 18 | }; 19 | } 20 | 21 | export const sighUpPageHandler = new SighUpPageHandler(); 22 | -------------------------------------------------------------------------------- /frontend/src/lib/hooks/is-mobile.svelte.ts: -------------------------------------------------------------------------------- 1 | import { untrack } from 'svelte'; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export class IsMobile { 6 | #current = $state(false); 7 | 8 | constructor() { 9 | $effect(() => { 10 | return untrack(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | this.#current = window.innerWidth < MOBILE_BREAKPOINT; 14 | }; 15 | mql.addEventListener('change', onChange); 16 | onChange(); 17 | return () => { 18 | mql.removeEventListener('change', onChange); 19 | }; 20 | }); 21 | }); 22 | } 23 | 24 | get current() { 25 | return this.#current; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { router } from "@backend/router"; 2 | import { factory } from "@backend/lib/utils/factory"; 3 | import { errorMiddleware } from "@backend/services/error"; 4 | import { dbMiddleware } from "@backend/services/db"; 5 | import { authenticationMiddleware } from "@backend/services/auth/authentication-middleware"; 6 | import { authorizationMiddleware } from "@backend/services/auth/authorization-middleware"; 7 | 8 | const app = factory 9 | // 10 | .createApp() 11 | .use(dbMiddleware) 12 | .use(authenticationMiddleware) 13 | .use(authorizationMiddleware) 14 | .route("/api", router) 15 | .onError(errorMiddleware); 16 | 17 | export default app; 18 | 19 | export type AppType = typeof app; 20 | -------------------------------------------------------------------------------- /backend/src/services/error/index.ts: -------------------------------------------------------------------------------- 1 | import { HTTPException } from "hono/http-exception"; 2 | import { ContentfulStatusCode } from "hono/utils/http-status"; 3 | import { ErrorHandler } from "hono"; 4 | const fail = ( 5 | code: ContentfulStatusCode, 6 | message: string, 7 | additionalData?: { res: Response; cause: Error } 8 | ) => new HTTPException(code, { message, ...additionalData }); 9 | 10 | const errorMiddleware: ErrorHandler = (err, c) => { 11 | console.info("error: ", err); 12 | if (err instanceof HTTPException) { 13 | const { status, message } = err; 14 | return c.json({ error: message }, { status }); 15 | } 16 | return c.json({ error: "Internal Server Error" }, { status: 500 }); 17 | }; 18 | export { errorMiddleware, fail }; 19 | -------------------------------------------------------------------------------- /frontend/src/lib/components/ui/sonner/sonner.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /frontend/src/routes/layout.svelte.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | import userStore from '@/lib/stores/user.svelte'; 3 | import { page } from '$app/state'; 4 | import type { EnvUser } from '@backend/lib/types/app'; 5 | import { goto } from '$app/navigation'; 6 | 7 | export function authBasedRedirection() { 8 | const user = JSON.parse(decodeURI(Cookies.get('user-data') ?? 'null')) as EnvUser; 9 | userStore.setUser(user); 10 | const authPaths = ['/sign-in', '/sign-up']; 11 | const publicPaths = [...authPaths]; // and more... 12 | const isPublicPath = publicPaths.includes(page.url.pathname); 13 | const isAuthPath = authPaths.includes(page.url.pathname); 14 | 15 | if (!user && !isPublicPath) { 16 | goto('/sign-in'); 17 | } 18 | if (user && isAuthPath) { 19 | goto('/'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/services/auth/authorization-middleware.ts: -------------------------------------------------------------------------------- 1 | import { factory } from "@backend/lib/utils/factory"; 2 | import { Context } from "@backend/lib/types/app"; 3 | import { fail } from "../error"; 4 | 5 | type Exposer = (c: Context) => boolean; 6 | 7 | const exposers: Exposer[] = [ 8 | // 9 | (c) => !!c.var.user?.isAdmin, 10 | (c) => c.req.path === "/api/user/signIn" && c.req.method === "POST", 11 | (c) => c.req.path === "/api/user/signUp" && c.req.method === "POST", 12 | (c) => c.req.path === "/api/user/signOut" && c.req.method === "POST", 13 | ]; 14 | 15 | export const authorizationMiddleware = factory.createMiddleware( 16 | async (c, next) => { 17 | if (!exposers.some((exposer) => exposer(c))) { 18 | throw fail(401, "Unauthorized"); 19 | } 20 | 21 | await next(); 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /backend/src/services/auth/authentication-middleware.ts: -------------------------------------------------------------------------------- 1 | import { factory } from "@backend/lib/utils/factory"; 2 | import { gen } from "@backend/lib/utils/generator"; 3 | import { auth } from "@backend/services/auth"; 4 | export const authenticationMiddleware = factory.createMiddleware( 5 | async (c, next) => { 6 | try { 7 | const jwt = await auth.token.loadFromCookie(c); 8 | if (!jwt.exp) throw new Error("No expiration date found"); 9 | if (jwt.exp < gen.x_hours_from_now_in_sec(0.5)) { 10 | const newToken = await auth.token.create(jwt.payload, c); 11 | auth.token.saveToCookie(newToken, c); 12 | } 13 | c.set("user", jwt.payload); 14 | } catch (error) { 15 | console.info("user not authenticated."); 16 | c.set("user", null); 17 | auth.token.delete(c); 18 | } 19 | await next(); 20 | } 21 | ); 22 | -------------------------------------------------------------------------------- /backend/migrations/0000_minor_loners.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `post` ( 2 | `id` text PRIMARY KEY NOT NULL, 3 | `title` text NOT NULL, 4 | `description` text DEFAULT '' NOT NULL, 5 | `content` text NOT NULL, 6 | `authorId` text NOT NULL, 7 | `likes` integer DEFAULT 0, 8 | `created_at` integer DEFAULT current_timestamp NOT NULL, 9 | `updated_at` integer DEFAULT current_timestamp NOT NULL, 10 | FOREIGN KEY (`authorId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action 11 | ); 12 | --> statement-breakpoint 13 | CREATE TABLE `user` ( 14 | `id` text PRIMARY KEY NOT NULL, 15 | `name` text DEFAULT '' NOT NULL, 16 | `email` text NOT NULL, 17 | `hashedPassword` text NOT NULL, 18 | `isAdmin` integer DEFAULT false NOT NULL, 19 | `created_at` integer DEFAULT current_timestamp NOT NULL 20 | ); 21 | --> statement-breakpoint 22 | CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); -------------------------------------------------------------------------------- /backend/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "backend", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-03-05", 6 | // "compatibility_flags": [ 7 | // "nodejs_compat" 8 | // ], 9 | // "vars": { 10 | // "MY_VAR": "my-variable" 11 | // }, 12 | "kv_namespaces": [ 13 | { 14 | "binding": "KV", 15 | "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 16 | } 17 | ], 18 | // "r2_buckets": [ 19 | // { 20 | // "binding": "MY_BUCKET", 21 | // "bucket_name": "my-bucket" 22 | // } 23 | // ], 24 | "d1_databases": [ 25 | { 26 | "binding": "DB", 27 | "database_name": "example-database", 28 | "database_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 29 | } 30 | ] 31 | // "ai": { 32 | // "binding": "AI" 33 | // }, 34 | // "observability": { 35 | // "enabled": true, 36 | // "head_sampling_rate": 1 37 | // } 38 | } -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "scripts": { 4 | "dev": "wrangler dev --port 8000", 5 | "deploy": "wrangler deploy --minify", 6 | "type": "wrangler types --env-interface WorkerEnv ./src/lib/types/worker.d.ts", 7 | "db:init": "wrangler d1 execute DB --local --command=\"SELECT 1;\"", 8 | "db:generate": "drizzle-kit generate", 9 | "db:migrate": "wrangler d1 migrations apply DB --local", 10 | "db:migrate:remote": "wrangler d1 migrations apply DB --remote", 11 | "db:push": "bun db:generate && bun db:migrate", 12 | "db:push:remote": "bun db:generate && bun db:migrate:remote", 13 | "db:studio": "drizzle-kit studio" 14 | }, 15 | "dependencies": { 16 | "@hono/zod-validator": "^0.4.3", 17 | "@libsql/client": "^0.14.0", 18 | "date-fns": "^4.1.0", 19 | "drizzle-orm": "^0.40.0", 20 | "hono": "^4.7.4" 21 | }, 22 | "devDependencies": { 23 | "@cloudflare/workers-types": "^4.20250214.0", 24 | "drizzle-kit": "^0.30.5", 25 | "wrangler": "^3.109.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hono-svelte-5", 3 | "version": "1.0.0", 4 | "author": "Moshe Rivkin", 5 | "scripts": {}, 6 | "main": "index.ts", 7 | "module": "index.ts", 8 | "dependencies": { 9 | "eslint-config-prettier": "^10.0.2", 10 | "hono": "^4.7.4" 11 | }, 12 | "peerDependencies": { 13 | "typescript": "^5.7.3" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/mosherivkin/hono-svelte-5/issues" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/yourusername/hono-svelte-5.git" 21 | }, 22 | "description": "A full-stack project using Hono as the backend framework and Svelte 5 for the frontend.", 23 | "keywords": [ 24 | "Hono", 25 | "Svelte 5", 26 | "Sveltekit", 27 | "Cloudflare Workers", 28 | "SQLite", 29 | "Drizzle ORM", 30 | "Zod", 31 | "Tailwind CSS", 32 | "Tailwind 4", 33 | "ShadCN-Svelte" 34 | ], 35 | "license": "MIT", 36 | "type": "module", 37 | "workspaces": [ 38 | "backend", 39 | "frontend" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Moshe Rivkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import { includeIgnoreFile } from '@eslint/compat'; 4 | import svelte from 'eslint-plugin-svelte'; 5 | import globals from 'globals'; 6 | import { fileURLToPath } from 'node:url'; 7 | import ts from 'typescript-eslint'; 8 | import svelteConfig from './svelte.config.js'; 9 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 10 | 11 | export default ts.config( 12 | includeIgnoreFile(gitignorePath), 13 | js.configs.recommended, 14 | ...ts.configs.recommended, 15 | ...svelte.configs.recommended, 16 | prettier, 17 | ...svelte.configs.prettier, 18 | { 19 | languageOptions: { 20 | globals: { 21 | ...globals.browser, 22 | ...globals.node 23 | } 24 | } 25 | }, 26 | { 27 | files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 28 | ignores: ['eslint.config.js', 'svelte.config.js'], 29 | 30 | languageOptions: { 31 | parserOptions: { 32 | projectService: true, 33 | extraFileExtensions: ['.svelte'], 34 | parser: ts.parser, 35 | svelteConfig 36 | } 37 | } 38 | } 39 | ); 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔥Hono-Svelte-5 2 | 3 | A full-stack project using **Hono** as the backend framework and **Svelte 5** as SPA for the frontend. 4 | 5 | ## Features 6 | 7 | - 🚀 Fast and lightweight backend with Hono 8 | - ⚡ Modern frontend with Svelte 5 9 | - 📦 SQLite database with Cloudflare D1 10 | - 🔒 Type-safe database operations with Drizzle ORM 11 | - ✨ Beautiful UI components with ShadCN-Svelte 12 | - 🎨 Styling with Tailwind CSS V4 13 | - 📝 Schema validation with Zod 14 | - 🔐 Authentication and authorization 15 | - 🌐 API route handling 16 | - 🚦 Environment variables management 17 | 18 | ## Tech Stack 19 | 20 | - **Frontend:** Svelte 5 (Sveltekit + static adapter) 21 | - **Backend:** Hono (Cloudflare Workers) 22 | - **Database:** SQLite with Cloudflare D1 23 | - **ORM:** Drizzle ORM 24 | - **Validation:** Zod 25 | - **UI Components:** Tailwind CSS + ShadCN-Svelte 26 | 27 | ## Prerequisites 28 | 29 | - Bun >= 1.0.0 (for development) 30 | - Node.js >= 18 31 | - Cloudflare account (for Workers and D1) 32 | - Wrangler CLI installed globally (`npm i -g wrangler`) 33 | 34 | ## Project Structure 35 | 36 | ## ⭐ Getting Started 37 | 38 | 1. Clone the repository 39 | 40 | ```bash 41 | git clone https://github.com/mosherivkin/hono-svelte-5.git 42 | cd hono-svelte-5 43 | ``` 44 | 45 | ## 🛠️ Development 46 | 47 | ## 🚀 Deployment 48 | 49 | ## 🤝 Contributing 50 | 51 | ## 📄 License 52 | 53 | ## 💬 Support 54 | -------------------------------------------------------------------------------- /backend/src/services/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { int, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; 2 | import { relations, sql } from "drizzle-orm"; 3 | 4 | const id = text().primaryKey().notNull(); 5 | const createdAt = int("created_at", { mode: "timestamp" }) 6 | .default(sql`current_timestamp`) 7 | .notNull(); 8 | export const updatedAt = int("updated_at", { mode: "timestamp" }) 9 | .default(sql`current_timestamp`) 10 | .notNull(); 11 | 12 | export const users = sqliteTable("user", { 13 | id, 14 | name: text().notNull().notNull().default(""), 15 | email: text().notNull().unique(), 16 | hashedPassword: text().notNull(), 17 | isAdmin: integer({ mode: "boolean" }).notNull().default(false), 18 | createdAt, 19 | }); 20 | export const posts = sqliteTable("post", { 21 | id, 22 | title: text().notNull(), 23 | description: text().notNull().default(""), 24 | content: text().notNull(), 25 | authorId: text() 26 | .notNull() 27 | .references(() => users.id), 28 | likes: integer().default(0), 29 | createdAt, 30 | updatedAt, 31 | }); 32 | 33 | export const userRelations = relations(users, ({ many }) => ({ 34 | posts: many(posts), 35 | })); 36 | export const postRelations = relations(posts, ({ one }) => ({ 37 | author: one(users, { fields: [posts.authorId], references: [users.id] }), 38 | })); 39 | 40 | export type User = typeof users.$inferSelect; 41 | export type NewUser = typeof users.$inferInsert; 42 | export type Post = typeof posts.$inferSelect; 43 | export type NewPost = typeof posts.$inferInsert; 44 | -------------------------------------------------------------------------------- /backend/src/api/sign-in.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@backend/services/auth"; 2 | import { factory } from "@backend/lib/utils/factory"; 3 | import { gen } from "@backend/lib/utils/generator"; 4 | import { validators } from "@backend/lib/utils/validators"; 5 | import { zValidator } from "@hono/zod-validator"; 6 | import { z } from "zod"; 7 | import { fail } from "@backend/services/error"; 8 | 9 | export const signIn = factory.createHandlers( 10 | zValidator( 11 | "json", 12 | z.object({ 13 | email: validators.email(), 14 | password: validators.password(), 15 | }) 16 | ), 17 | async (c) => { 18 | const requestPayload = c.req.valid("json"); 19 | 20 | const userData = await c.var.db.query.users.findFirst({ 21 | where({ email }, { eq }) { 22 | return eq(email, requestPayload.email); 23 | }, 24 | }); 25 | 26 | if (!userData) { 27 | throw fail(401, "Invalid email or password"); 28 | } 29 | const { hashedPassword, ...publicUserData } = userData; 30 | const validPassword = await auth.password.verify( 31 | requestPayload.password, 32 | userData.hashedPassword 33 | ); 34 | 35 | if (!validPassword) { 36 | throw fail(401, "Invalid email or password"); 37 | } 38 | const accessTokenPayload = { 39 | exp: gen.x_hours_from_now_in_sec(1), 40 | ...publicUserData, 41 | }; 42 | const accessToken = await auth.token.create(accessTokenPayload, c); 43 | auth.token.saveUserToCookie(publicUserData, c); 44 | auth.token.saveToCookie(accessToken, c); 45 | return c.json(null); 46 | } 47 | ); 48 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "prepare": "svelte-kit sync || echo ''", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 | "format": "prettier --write .", 14 | "lint": "prettier --check . && eslint ." 15 | }, 16 | "devDependencies": { 17 | "@eslint/compat": "^1.2.5", 18 | "@eslint/js": "^9.18.0", 19 | "@sveltejs/adapter-auto": "^4.0.0", 20 | "@sveltejs/adapter-static": "^3.0.8", 21 | "@sveltejs/kit": "^2.16.0", 22 | "@sveltejs/vite-plugin-svelte": "^5.0.0", 23 | "@tailwindcss/vite": "^4.0.0", 24 | "@types/js-cookie": "^3.0.6", 25 | "bits-ui": "^1.3.6", 26 | "eslint": "^9.18.0", 27 | "eslint-config-prettier": "^10.0.1", 28 | "eslint-plugin-svelte": "^3.0.0", 29 | "globals": "^16.0.0", 30 | "mode-watcher": "^0.5.1", 31 | "prettier": "^3.4.2", 32 | "prettier-plugin-svelte": "^3.3.3", 33 | "prettier-plugin-tailwindcss": "^0.6.11", 34 | "svelte": "^5.0.0", 35 | "svelte-check": "^4.0.0", 36 | "tailwindcss": "^4.0.0", 37 | "typescript": "^5.0.0", 38 | "typescript-eslint": "^8.20.0", 39 | "vite": "^6.0.0" 40 | }, 41 | "dependencies": { 42 | "clsx": "^2.1.1", 43 | "js-cookie": "^3.0.5", 44 | "lucide-svelte": "^0.477.0", 45 | "svelte-sonner": "^0.3.28", 46 | "tailwind-merge": "^3.0.2", 47 | "tailwind-variants": "^0.3.1", 48 | "tailwindcss-animate": "^1.0.7" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /backend/src/api/sign-up.ts: -------------------------------------------------------------------------------- 1 | import * as schema from "@backend/services/db/schema"; 2 | import { zValidator } from "@hono/zod-validator"; 3 | import { factory } from "@backend/lib/utils/factory"; 4 | import { gen } from "@backend/lib/utils/generator"; 5 | import { z } from "zod"; 6 | import { auth } from "@backend/services/auth"; 7 | import { validators } from "@backend/lib/utils/validators"; 8 | 9 | export const signUp = factory.createHandlers( 10 | zValidator( 11 | "json", 12 | z.object({ 13 | name: validators.name(), 14 | email: validators.email(), 15 | password: validators.password(), 16 | }) 17 | ), 18 | async (c) => { 19 | const requestPayload = c.req.valid("json"); 20 | 21 | const existingUser = await c.var.db.query.users.findFirst({ 22 | where({ email }, { eq }) { 23 | return eq(email, requestPayload.email); 24 | }, 25 | }); 26 | if (existingUser) return c.json({ error: "User already exists" }, 400); 27 | 28 | const id = gen.id(); 29 | const passwordHash = await auth.password.hash(requestPayload.password); 30 | const [{ hashedPassword: _, ...userData }] = await c.var.db 31 | .insert(schema.users) 32 | .values({ 33 | id, 34 | email: requestPayload.email, 35 | hashedPassword: passwordHash, 36 | name: requestPayload.name, 37 | }) 38 | .returning(); 39 | 40 | const jwtAccessPayload = { 41 | exp: gen.x_hours_from_now_in_sec(1), 42 | ...userData, 43 | }; 44 | 45 | const accessToken = await auth.token.create(jwtAccessPayload, c); 46 | auth.token.saveUserToCookie(userData, c); 47 | auth.token.saveToCookie(accessToken, c); 48 | return c.json(null); 49 | } 50 | ); 51 | -------------------------------------------------------------------------------- /frontend/src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | {#if type === "file"} 24 | 35 | {:else} 36 | 46 | {/if} 47 | -------------------------------------------------------------------------------- /backend/src/services/auth/password.ts: -------------------------------------------------------------------------------- 1 | export const passwordActions = { 2 | async hash(password: string, existingSalt?: Uint8Array) { 3 | const encoder = new TextEncoder(); 4 | const salt = existingSalt ?? crypto.getRandomValues(new Uint8Array(16)); 5 | const keyMaterial = await crypto.subtle.importKey( 6 | "raw", 7 | encoder.encode(password), 8 | { name: "PBKDF2" }, 9 | false, 10 | ["deriveBits", "deriveKey"] 11 | ); 12 | const key = await crypto.subtle.deriveKey( 13 | { 14 | name: "PBKDF2", 15 | salt: salt.buffer as ArrayBuffer, 16 | iterations: 100000, 17 | hash: "SHA-256", 18 | }, 19 | keyMaterial, 20 | { name: "AES-GCM", length: 256 }, 21 | true, 22 | ["encrypt", "decrypt"] 23 | ); 24 | const exportedKey = (await crypto.subtle.exportKey( 25 | "raw", 26 | key 27 | )) as ArrayBuffer; 28 | const hashBuffer = new Uint8Array(exportedKey); 29 | const hashArray = Array.from(hashBuffer); 30 | const hashHex = hashArray 31 | .map((b) => b.toString(16).padStart(2, "0")) 32 | .join(""); 33 | const saltHex = Array.from(salt) 34 | .map((b) => b.toString(16).padStart(2, "0")) 35 | .join(""); 36 | return `${saltHex}:${hashHex}`; 37 | }, 38 | 39 | async verify(password: string, storedHash: string) { 40 | const [saltHex, originalHash] = storedHash.split(":"); 41 | const matchResult = saltHex.match(/.{1,2}/g); 42 | if (!matchResult) { 43 | throw new Error("Invalid salt format"); 44 | } 45 | const salt = new Uint8Array(matchResult.map((byte) => parseInt(byte, 16))); 46 | const attemptHashWithSalt = await passwordActions.hash(password, salt); 47 | const [, attemptHash] = attemptHashWithSalt.split(":"); 48 | return attemptHash === originalHash; 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /backend/src/services/auth/token.ts: -------------------------------------------------------------------------------- 1 | import { Context, EnvUser } from "@backend/lib/types/app"; 2 | import { gen } from "@backend/lib/utils/generator"; 3 | import { deleteCookie, getCookie, setCookie } from "hono/cookie"; 4 | import { sign, verify } from "hono/jwt"; 5 | import { JWTPayload } from "hono/utils/jwt/types"; 6 | 7 | export const tokenActions = { 8 | async create(payload: NonNullable, c: Context) { 9 | if (!c.env.JWT_SECRET) 10 | throw new Error("Please add JWT_SECRET to your .dev.vars file"); 11 | return await sign(payload, c.env.JWT_SECRET, "HS256"); 12 | }, 13 | 14 | async verify(token: string, c: Context) { 15 | if (!c.env.JWT_SECRET) 16 | throw new Error("Please add JWT_SECRET to your .dev.vars file"); 17 | try { 18 | const jwt = await verify(token, c.env.JWT_SECRET, "HS256"); 19 | return jwt as JWTPayload & { payload: NonNullable }; 20 | } catch { 21 | throw new Error("Invalid or expired token"); 22 | } 23 | }, 24 | 25 | saveToCookie(token: string, c: Context) { 26 | setCookie(c, "access-token", token, { 27 | httpOnly: true, 28 | secure: true, 29 | sameSite: "strict", 30 | path: "/", 31 | expires: new Date(gen.x_hours_from_now_in_ms(1)), 32 | }); 33 | }, 34 | saveUserToCookie(user: EnvUser, c: Context) { 35 | setCookie(c, "user-data", encodeURI(JSON.stringify(user)), { 36 | httpOnly: false, 37 | secure: true, 38 | sameSite: "strict", 39 | path: "/", 40 | expires: new Date(gen.x_hours_from_now_in_ms(1)), 41 | }); 42 | }, 43 | async loadFromCookie(c: Context) { 44 | const tokenCookie = getCookie(c, "access-token"); 45 | if (!tokenCookie) { 46 | throw new Error("No token found in cookie"); 47 | } 48 | return await tokenActions.verify(tokenCookie, c); 49 | }, 50 | delete(c: Context) { 51 | deleteCookie(c, "access-token"); 52 | deleteCookie(c, "user-data"); 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /frontend/src/routes/sign-in/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |
12 |
13 |
14 |

Sign In

15 |

Enter your email below to login to your account

16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 | 25 | Forgot your password? 26 |
27 | 28 |
29 | 30 | 31 |
32 |
33 | Don't have an account? 34 | Sign up 35 |
36 |
37 |
38 | 43 |
44 | -------------------------------------------------------------------------------- /frontend/src/routes/sign-up/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |
13 |
14 |
15 |

Sign Up

16 |

Enter your details below to create your account

17 |
18 |
19 |
20 | 21 | 22 |
23 | 24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 | 33 | 34 |
35 |
36 | Already have an account? 37 | Sign in 38 |
39 |
40 |
41 | 46 |
47 | -------------------------------------------------------------------------------- /frontend/src/lib/components/ui/button/button.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | 45 | 46 | {#if href} 47 | 48 | {@render children?.()} 49 | 50 | {:else} 51 | 54 | {/if} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # prod 2 | dist/ 3 | 4 | # dev 5 | .yarn/ 6 | !.yarn/releases 7 | .vscode/* 8 | !.vscode/launch.json 9 | !.vscode/*.code-snippets 10 | .idea/workspace.xml 11 | .idea/usage.statistics.xml 12 | .idea/shelf 13 | 14 | # deps 15 | node_modules 16 | 17 | # Output 18 | .output 19 | .vercel 20 | .svelte-kit 21 | .wrangler 22 | build 23 | 24 | # OS 25 | .DS_Store 26 | Thumbs.db 27 | 28 | # Env 29 | .env 30 | .env.* 31 | !.env.example 32 | !.env.test 33 | 34 | # Vite 35 | vite.config.js.timestamp-* 36 | vite.config.ts.timestamp-* 37 | 38 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 39 | 40 | # Logs 41 | logs 42 | *.log 43 | npm-debug.log* 44 | yarn-debug.log* 45 | yarn-error.log* 46 | lerna-debug.log* 47 | .pnpm-debug.log* 48 | 49 | # Caches 50 | .cache 51 | 52 | # Diagnostic reports (https://nodejs.org/api/report.html) 53 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 54 | 55 | # Runtime data 56 | pids 57 | *.pid 58 | *.seed 59 | *.pid.lock 60 | 61 | # Directory for instrumented libs generated by jscoverage/JSCover 62 | lib-cov 63 | 64 | # Coverage directory used by tools like istanbul 65 | coverage 66 | *.lcov 67 | 68 | # nyc test coverage 69 | .nyc_output 70 | 71 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 72 | .grunt 73 | 74 | # Bower dependency directory (https://bower.io/) 75 | bower_components 76 | 77 | # node-waf configuration 78 | .lock-wscript 79 | 80 | # Compiled binary addons (https://nodejs.org/api/addons.html) 81 | build/Release 82 | 83 | # Dependency directories 84 | node_modules/ 85 | jspm_packages/ 86 | 87 | # Snowpack dependency directory (https://snowpack.dev/) 88 | web_modules/ 89 | 90 | # TypeScript cache 91 | *.tsbuildinfo 92 | 93 | # Optional npm cache directory 94 | .npm 95 | 96 | # Optional eslint cache 97 | .eslintcache 98 | 99 | # Optional stylelint cache 100 | .stylelintcache 101 | 102 | # Microbundle cache 103 | .rpt2_cache/ 104 | .rts2_cache_cjs/ 105 | .rts2_cache_es/ 106 | .rts2_cache_umd/ 107 | 108 | # Optional REPL history 109 | .node_repl_history 110 | 111 | # Output of 'npm pack' 112 | *.tgz 113 | 114 | # Yarn Integrity file 115 | .yarn-integrity 116 | 117 | # dotenv environment variable files 118 | .env 119 | .env.development.local 120 | .env.test.local 121 | .env.production.local 122 | .env.local 123 | 124 | # parcel-bundler cache (https://parceljs.org/) 125 | .parcel-cache 126 | 127 | # Next.js build output 128 | .next 129 | out 130 | 131 | # Nuxt.js build / generate output 132 | .nuxt 133 | dist 134 | 135 | # Gatsby files 136 | # Comment in the public line in if your project uses Gatsby and not Next.js 137 | # https://nextjs.org/blog/next-9-1#public-directory-support 138 | # public 139 | 140 | # vuepress build output 141 | .vuepress/dist 142 | 143 | # vuepress v2.x temp and cache directory 144 | .temp 145 | 146 | # Docusaurus cache and generated files 147 | .docusaurus 148 | 149 | # Serverless directories 150 | .serverless/ 151 | 152 | # FuseBox cache 153 | .fusebox/ 154 | 155 | # DynamoDB Local files 156 | .dynamodb/ 157 | 158 | # TernJS port file 159 | .tern-port 160 | 161 | # Stores VSCode versions used for testing VSCode extensions 162 | .vscode-test 163 | 164 | # yarn v2 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | .dev.vars -------------------------------------------------------------------------------- /frontend/src/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @plugin 'tailwindcss-animate'; 4 | 5 | @custom-variant dark (&:is(.dark *)); 6 | 7 | :root { 8 | --background: oklch(1 0 0); 9 | --foreground: oklch(0.145 0 0); 10 | --card: oklch(1 0 0); 11 | --card-foreground: oklch(0.145 0 0); 12 | --popover: oklch(1 0 0); 13 | --popover-foreground: oklch(0.145 0 0); 14 | --primary: oklch(0.205 0 0); 15 | --primary-foreground: oklch(0.985 0 0); 16 | --secondary: oklch(0.97 0 0); 17 | --secondary-foreground: oklch(0.205 0 0); 18 | --muted: oklch(0.97 0 0); 19 | --muted-foreground: oklch(0.556 0 0); 20 | --accent: oklch(0.97 0 0); 21 | --accent-foreground: oklch(0.205 0 0); 22 | --destructive: oklch(0.577 0.245 27.325); 23 | --destructive-foreground: oklch(0.577 0.245 27.325); 24 | --border: oklch(0.922 0 0); 25 | --input: oklch(0.922 0 0); 26 | --ring: oklch(0.708 0 0); 27 | --chart-1: oklch(0.646 0.222 41.116); 28 | --chart-2: oklch(0.6 0.118 184.704); 29 | --chart-3: oklch(0.398 0.07 227.392); 30 | --chart-4: oklch(0.828 0.189 84.429); 31 | --chart-5: oklch(0.769 0.188 70.08); 32 | --radius: 0.625rem; 33 | --sidebar: oklch(0.985 0 0); 34 | --sidebar-foreground: oklch(0.145 0 0); 35 | --sidebar-primary: oklch(0.205 0 0); 36 | --sidebar-primary-foreground: oklch(0.985 0 0); 37 | --sidebar-accent: oklch(0.97 0 0); 38 | --sidebar-accent-foreground: oklch(0.205 0 0); 39 | --sidebar-border: oklch(0.922 0 0); 40 | --sidebar-ring: oklch(0.708 0 0); 41 | } 42 | 43 | .dark { 44 | --background: oklch(0.145 0 0); 45 | --foreground: oklch(0.985 0 0); 46 | --card: oklch(0.145 0 0); 47 | --card-foreground: oklch(0.985 0 0); 48 | --popover: oklch(0.145 0 0); 49 | --popover-foreground: oklch(0.985 0 0); 50 | --primary: oklch(0.985 0 0); 51 | --primary-foreground: oklch(0.205 0 0); 52 | --secondary: oklch(0.269 0 0); 53 | --secondary-foreground: oklch(0.985 0 0); 54 | --muted: oklch(0.269 0 0); 55 | --muted-foreground: oklch(0.708 0 0); 56 | --accent: oklch(0.269 0 0); 57 | --accent-foreground: oklch(0.985 0 0); 58 | --destructive: oklch(0.396 0.141 25.723); 59 | --destructive-foreground: oklch(0.637 0.237 25.331); 60 | --border: oklch(0.269 0 0); 61 | --input: oklch(0.269 0 0); 62 | --ring: oklch(0.439 0 0); 63 | --chart-1: oklch(0.488 0.243 264.376); 64 | --chart-2: oklch(0.696 0.17 162.48); 65 | --chart-3: oklch(0.769 0.188 70.08); 66 | --chart-4: oklch(0.627 0.265 303.9); 67 | --chart-5: oklch(0.645 0.246 16.439); 68 | --sidebar: oklch(0.205 0 0); 69 | --sidebar-foreground: oklch(0.985 0 0); 70 | --sidebar-primary: oklch(0.488 0.243 264.376); 71 | --sidebar-primary-foreground: oklch(0.985 0 0); 72 | --sidebar-accent: oklch(0.269 0 0); 73 | --sidebar-accent-foreground: oklch(0.985 0 0); 74 | --sidebar-border: oklch(0.269 0 0); 75 | --sidebar-ring: oklch(0.439 0 0); 76 | } 77 | 78 | @theme inline { 79 | --color-background: var(--background); 80 | --color-foreground: var(--foreground); 81 | --color-card: var(--card); 82 | --color-card-foreground: var(--card-foreground); 83 | --color-popover: var(--popover); 84 | --color-popover-foreground: var(--popover-foreground); 85 | --color-primary: var(--primary); 86 | --color-primary-foreground: var(--primary-foreground); 87 | --color-secondary: var(--secondary); 88 | --color-secondary-foreground: var(--secondary-foreground); 89 | --color-muted: var(--muted); 90 | --color-muted-foreground: var(--muted-foreground); 91 | --color-accent: var(--accent); 92 | --color-accent-foreground: var(--accent-foreground); 93 | --color-destructive: var(--destructive); 94 | --color-destructive-foreground: var(--destructive-foreground); 95 | --color-border: var(--border); 96 | --color-input: var(--input); 97 | --color-ring: var(--ring); 98 | --color-chart-1: var(--chart-1); 99 | --color-chart-2: var(--chart-2); 100 | --color-chart-3: var(--chart-3); 101 | --color-chart-4: var(--chart-4); 102 | --color-chart-5: var(--chart-5); 103 | --radius-sm: calc(var(--radius) - 4px); 104 | --radius-md: calc(var(--radius) - 2px); 105 | --radius-lg: var(--radius); 106 | --radius-xl: calc(var(--radius) + 4px); 107 | --color-sidebar: var(--sidebar); 108 | --color-sidebar-foreground: var(--sidebar-foreground); 109 | --color-sidebar-primary: var(--sidebar-primary); 110 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 111 | --color-sidebar-accent: var(--sidebar-accent); 112 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 113 | --color-sidebar-border: var(--sidebar-border); 114 | --color-sidebar-ring: var(--sidebar-ring); 115 | } 116 | 117 | @layer base { 118 | * { 119 | @apply border-border outline-ring/50; 120 | } 121 | body { 122 | @apply bg-background text-foreground; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /backend/migrations/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "fba0c594-faf6-4319-801b-7bdcf8cad39e", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "post": { 8 | "name": "post", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "text", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "title": { 18 | "name": "title", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false 23 | }, 24 | "description": { 25 | "name": "description", 26 | "type": "text", 27 | "primaryKey": false, 28 | "notNull": true, 29 | "autoincrement": false, 30 | "default": "''" 31 | }, 32 | "content": { 33 | "name": "content", 34 | "type": "text", 35 | "primaryKey": false, 36 | "notNull": true, 37 | "autoincrement": false 38 | }, 39 | "authorId": { 40 | "name": "authorId", 41 | "type": "text", 42 | "primaryKey": false, 43 | "notNull": true, 44 | "autoincrement": false 45 | }, 46 | "likes": { 47 | "name": "likes", 48 | "type": "integer", 49 | "primaryKey": false, 50 | "notNull": false, 51 | "autoincrement": false, 52 | "default": 0 53 | }, 54 | "created_at": { 55 | "name": "created_at", 56 | "type": "integer", 57 | "primaryKey": false, 58 | "notNull": true, 59 | "autoincrement": false, 60 | "default": "current_timestamp" 61 | }, 62 | "updated_at": { 63 | "name": "updated_at", 64 | "type": "integer", 65 | "primaryKey": false, 66 | "notNull": true, 67 | "autoincrement": false, 68 | "default": "current_timestamp" 69 | } 70 | }, 71 | "indexes": {}, 72 | "foreignKeys": { 73 | "post_authorId_user_id_fk": { 74 | "name": "post_authorId_user_id_fk", 75 | "tableFrom": "post", 76 | "tableTo": "user", 77 | "columnsFrom": [ 78 | "authorId" 79 | ], 80 | "columnsTo": [ 81 | "id" 82 | ], 83 | "onDelete": "no action", 84 | "onUpdate": "no action" 85 | } 86 | }, 87 | "compositePrimaryKeys": {}, 88 | "uniqueConstraints": {}, 89 | "checkConstraints": {} 90 | }, 91 | "user": { 92 | "name": "user", 93 | "columns": { 94 | "id": { 95 | "name": "id", 96 | "type": "text", 97 | "primaryKey": true, 98 | "notNull": true, 99 | "autoincrement": false 100 | }, 101 | "name": { 102 | "name": "name", 103 | "type": "text", 104 | "primaryKey": false, 105 | "notNull": true, 106 | "autoincrement": false, 107 | "default": "''" 108 | }, 109 | "email": { 110 | "name": "email", 111 | "type": "text", 112 | "primaryKey": false, 113 | "notNull": true, 114 | "autoincrement": false 115 | }, 116 | "hashedPassword": { 117 | "name": "hashedPassword", 118 | "type": "text", 119 | "primaryKey": false, 120 | "notNull": true, 121 | "autoincrement": false 122 | }, 123 | "isAdmin": { 124 | "name": "isAdmin", 125 | "type": "integer", 126 | "primaryKey": false, 127 | "notNull": true, 128 | "autoincrement": false, 129 | "default": false 130 | }, 131 | "created_at": { 132 | "name": "created_at", 133 | "type": "integer", 134 | "primaryKey": false, 135 | "notNull": true, 136 | "autoincrement": false, 137 | "default": "current_timestamp" 138 | } 139 | }, 140 | "indexes": { 141 | "user_email_unique": { 142 | "name": "user_email_unique", 143 | "columns": [ 144 | "email" 145 | ], 146 | "isUnique": true 147 | } 148 | }, 149 | "foreignKeys": {}, 150 | "compositePrimaryKeys": {}, 151 | "uniqueConstraints": {}, 152 | "checkConstraints": {} 153 | } 154 | }, 155 | "views": {}, 156 | "enums": {}, 157 | "_meta": { 158 | "schemas": {}, 159 | "tables": {}, 160 | "columns": {} 161 | }, 162 | "internal": { 163 | "indexes": {} 164 | } 165 | } --------------------------------------------------------------------------------