From c2d561985512e6d4c286f5de942c26730c65edf3 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 26 May 2026 10:22:05 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E9=A6=96=E9=A1=B5=EF=BC=8C?= =?UTF-8?q?=E5=B0=86=E6=A0=B9=E8=B7=AF=E5=BE=84=E9=87=8D=E5=AE=9A=E5=90=91?= =?UTF-8?q?=E5=88=B0=E7=99=BB=E5=BD=95=E9=A1=B5=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + CLAUDE.md | 127 +++++++++++++++++++++++++++++++++++++++++++++ Makefile | 8 +-- public/style.css | 42 +++++++-------- src/pages/login.rs | 5 ++ src/router.rs | 28 ---------- 6 files changed, 156 insertions(+), 55 deletions(-) create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 892047d..ce5918e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /.omc /node_modules /package-lock.json +others/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4f7e171 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,127 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Yggdrasil is a Dioxus 0.7 fullstack web application — a blog system with user authentication. It compiles to both a WASM frontend and a native server backend from a single Rust codebase. + +## Development Commands + +```bash +# Development server (Tailwind CSS watch + dx serve) +make dev + +# Production build (minified CSS + release binary) +make build + +# Build CSS only +make css + +# Watch CSS only +make css-watch + +# Standard Rust commands +cargo build +cargo clippy +cargo test +cargo clean && make clean + +# Dioxus CLI commands +dx serve # Dev server +dx build --release # Release build +dx check # Type-check the project +``` + +## Prerequisites + +- Rust 1.95+ with `wasm32-unknown-unknown` target +- `dx` CLI (Dioxus CLI) — install via `cargo install dioxus-cli` +- `tailwindcss` CLI v4 — standalone binary needed for `make dev`/`make build` +- PostgreSQL database running locally + +## Environment Setup + +Create a `.env` file with: + +``` +DATABASE_URL=postgres://postgres:postgres@localhost:5432/yggdrasil +``` + +Run the migration in `migrations/001_init.sql` to set up the `users` and `sessions` tables. + +## Architecture + +### Fullstack Dioxus + +The project uses Dioxus's fullstack mode with two Cargo features: + +- **`web`** (`dioxus/web`): Compiles the frontend to WASM. Activated by default. +- **`server`** (`dioxus/server`): Compiles the server-side code. Activated by default. + +Server functions are defined with `#[server(Name, "/api")]` in `src/api/auth.rs`. These are callable from both client and server code. Dioxus handles the HTTP transport automatically. + +### Conditional Compilation + +Code is gated by two mechanisms: + +- `#[cfg(feature = "server")]` — server-only code (database, background tasks, env loading). The `db` and `tasks` modules are entirely server-gated. +- `#[cfg(target_arch = "wasm32")]` — browser-only code (localStorage, cookie manipulation, DOM APIs). + +The `db::pool` module provides a `DummyPool` stub when the `server` feature is disabled, so the code compiles for WASM. + +### Authentication Flow + +1. **Registration** (`src/api/auth.rs:register`): First registration creates the admin account. Subsequent registrations are rejected (`"Registration is closed"`). Passwords are hashed with Argon2. +2. **Login** (`src/api/auth.rs:login`): Validates credentials against the `users` table, creates a session record with a UUID token, returns the token. +3. **Session Storage**: The token is stored in a browser cookie (client-side, not HttpOnly). The server checks the `sessions` table for valid, non-expired tokens. +4. **Logout** (`src/api/auth.rs:logout`): Clears the client cookie and deletes expired sessions from the database. + +### Database + +- PostgreSQL via `tokio-postgres` with `deadpool-postgres` connection pooling. +- The pool is a `LazyLock` global in `src/db/pool.rs`, initialized from `DATABASE_URL`. +- Pool max size is 10 connections. + +### Background Tasks + +On server startup (`src/main.rs`), a background thread runs `tasks::session_cleanup::run_cleanup()`, which deletes expired sessions every hour. + +### Routing + +Routes are defined in `src/router.rs` using `#[derive(Routable)]`: + +- `/` — Home (landing page with login/register links) +- `/login` — Login page +- `/register` — Registration page +- `/admin` — Admin dashboard (redirects to `/login` if not authenticated) + +### Styling + +- Tailwind CSS v4, compiled from `input.css` (`@import "tailwindcss"`) to `public/style.css`. +- Dark mode is implemented via the `dark:` variant with a `data-theme` attribute on the `` element. +- The `ThemeToggle` component persists the preference in `localStorage`. + +## Key Files + +| File | Purpose | +|------|---------| +| `src/router.rs` | Route definitions and root app component with theme wrapper | +| `src/api/auth.rs` | Server functions: `register`, `login`, `logout`, `get_current_user` | +| `src/db/pool.rs` | Database connection pool (`LazyLock`) | +| `src/auth/password.rs` | Argon2 password hashing and verification | +| `src/auth/session.rs` | UUID token generation and expiry calculation | +| `src/models/user.rs` | `User` struct and `UserRole` enum (`Admin` / `Blocked`) | +| `src/pages/admin.rs` | Admin dashboard with auth check and logout | +| `src/tasks/session_cleanup.rs` | Hourly background job to purge expired sessions | +| `src/theme.rs` | Dark/light theme state management and toggle button | +| `migrations/001_init.sql` | Database schema (users + sessions tables) | +| `Dioxus.toml` | Dioxus app config (default platform = web) | +| `Makefile` | Convenience targets for dev/build/css | + +## Important Notes + +- The session cookie is set client-side via `web_sys::HtmlDocument::set_cookie`, which means it is **not HttpOnly**. This is a known limitation of the current implementation. +- `get_current_user` returns the most recently created valid session (not necessarily the one matching the current request's cookie). The admin page relies on this for authentication state. +- The `#[allow(dead_code)]` attributes on auth utilities are needed because the compiler sees them as unused in WASM builds where server functions are stripped. +- `rand` with `getrandom` and `getrandom` with `js` feature are required for Argon2 salt generation in WASM builds. diff --git a/Makefile b/Makefile index df1a170..c220b3a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,9 @@ .PHONY: dev build css css-watch clean +build: + @tailwindcss -i input.css -o public/style.css --minify + @dx build --release + dev: @echo "Starting tailwindcss watch and dx serve..." @tailwindcss -i input.css -o public/style.css --watch & \ @@ -7,10 +11,6 @@ dev: trap 'kill $$TAILWIND_PID 2>/dev/null; exit' INT TERM EXIT; \ dx serve -build: - @tailwindcss -i input.css -o public/style.css --minify - @dx build --release - css: @tailwindcss -i input.css -o public/style.css diff --git a/public/style.css b/public/style.css index 5730b6d..38d7482 100644 --- a/public/style.css +++ b/public/style.css @@ -236,15 +236,15 @@ .mb-6 { margin-bottom: calc(var(--spacing) * 6); } - .mb-8 { - margin-bottom: calc(var(--spacing) * 8); - } .block { display: block; } .flex { display: flex; } + .table { + display: table; + } .min-h-screen { min-height: 100vh; } @@ -276,13 +276,6 @@ margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); } } - .space-x-4 { - :where(& > :not(:last-child)) { - --tw-space-x-reverse: 0; - margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse)); - margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse))); - } - } .rounded-2xl { border-radius: var(--radius-2xl); } @@ -332,9 +325,6 @@ .px-4 { padding-inline: calc(var(--spacing) * 4); } - .px-6 { - padding-inline: calc(var(--spacing) * 6); - } .py-2 { padding-block: calc(var(--spacing) * 2); } @@ -351,10 +341,6 @@ font-size: var(--text-2xl); line-height: var(--tw-leading, var(--text-2xl--line-height)); } - .text-4xl { - font-size: var(--text-4xl); - line-height: var(--tw-leading, var(--text-4xl--line-height)); - } .text-sm { font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)); @@ -429,6 +415,13 @@ } } } + .hover\:text-gray-700 { + &:hover { + @media (hover: hover) { + color: var(--color-gray-700); + } + } + } .hover\:underline { &:hover { @media (hover: hover) { @@ -522,17 +515,21 @@ } } } + .dark\:hover\:text-gray-200 { + @media (prefers-color-scheme: dark) { + &:hover { + @media (hover: hover) { + color: var(--color-gray-200); + } + } + } + } } @property --tw-space-y-reverse { syntax: "*"; inherits: false; initial-value: 0; } -@property --tw-space-x-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; -} @property --tw-border-style { syntax: "*"; inherits: false; @@ -611,7 +608,6 @@ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { --tw-space-y-reverse: 0; - --tw-space-x-reverse: 0; --tw-border-style: solid; --tw-font-weight: initial; --tw-shadow: 0 0 #0000; diff --git a/src/pages/login.rs b/src/pages/login.rs index d6965f6..dac5bc8 100644 --- a/src/pages/login.rs +++ b/src/pages/login.rs @@ -107,6 +107,11 @@ pub fn LoginPage() -> Element { onclick: on_submit, "登录" } + a { + class: "block w-full py-2 px-4 text-center text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 font-medium rounded-lg transition-colors", + href: "/register", + "还没有账号?去注册" + } } } } diff --git a/src/router.rs b/src/router.rs index e3cb48f..7ca1212 100644 --- a/src/router.rs +++ b/src/router.rs @@ -8,7 +8,6 @@ use crate::theme::{Theme, ThemeToggle, use_theme}; #[derive(Clone, Routable, Debug, PartialEq)] pub enum Route { #[route("/")] - Home {}, #[route("/login")] LoginPage {}, #[route("/register")] @@ -34,30 +33,3 @@ pub fn AppRouter() -> Element { } } -#[component] -pub fn Home() -> Element { - rsx! { - div { class: "min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900", - div { class: "text-center", - h1 { class: "text-4xl font-bold text-gray-900 dark:text-white mb-4", - "Yggdrasil Blog" - } - p { class: "text-gray-600 dark:text-gray-300 mb-8", - "以文字为主的简约博客系统" - } - div { class: "space-x-4", - a { - class: "px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors", - href: "/login", - "登录" - } - a { - class: "px-6 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-lg transition-colors", - href: "/register", - "注册" - } - } - } - } - } -}