移除首页,将根路径重定向到登录页,添加注册链接
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
390ab98b86
commit
c2d5619855
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@
|
||||
/.omc
|
||||
/node_modules
|
||||
/package-lock.json
|
||||
others/
|
||||
|
||||
127
CLAUDE.md
Normal file
127
CLAUDE.md
Normal file
@ -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 `<html>` 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<Pool>`) |
|
||||
| `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.
|
||||
8
Makefile
8
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
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
"还没有账号?去注册"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
"注册"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user