diff --git a/.cursor/rules/arch.mdc b/.cursor/rules/arch.mdc deleted file mode 100644 index 495a519..0000000 --- a/.cursor/rules/arch.mdc +++ /dev/null @@ -1,220 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# Excalidraw-Complete 架构文档 - -本文档旨在详细阐述 `excalidraw-complete` 项目的系统架构、技术栈、模块设计和数据流,以便于开发者理解、维护和进行二次开发。 - -## 1. 概述 (Overview) - -`excalidraw-complete` 是一个将优秀的开源白板工具 [Excalidraw](https://github.com/excalidraw/excalidraw) 进行整合与封装的自托管解决方案。其核心目标是简化 Excalidraw 的私有化部署流程,将前端UI、后端数据存储和实时协作服务打包成一个单一的、易于部署的Go二进制文件。 - -**核心特性:** - -- **一体化部署**:将所有服务打包成单个可执行文件,无需复杂的依赖配置。 -- **可插拔存储**:通过环境变量支持多种数据持久化方案,包括内存、本地文件系统、SQLite和AWS S3。 -- **实时协作**:内置基于 Socket.IO 的实时协作服务器,允许多个用户同时在同一个画板上工作。 -- **Firebase 兼容层**:提供一个内存实现的 Firebase API 兼容层,以满足 Excalidraw 前端对 Firebase 的部分依赖。 - ---- - -## 2. 技术栈 (Tech Stack) - -项目采用了现代化的前后端技术栈。 - -### 后端 (Backend) - -- **语言**: [Go](https://go.dev/) (v1.21+) -- **Web框架**: [Chi (v5)](https://github.com/go-chi/chi) - 一个轻量级、高性能的 Go HTTP 路由器。 -- **实时通信**: [Socket.IO for Go](https://github.com/zishang520/socket.io/v2) - 实现了 Socket.IO 协议,用于实时协作。 -- **数据库驱动**: - - [go-sqlite3](https://github.com/mattn/go-sqlite3) - 用于 SQLite 存储。 - - [aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) - 用于与 AWS S3 交互。 -- **日志**: [Logrus](https://github.com/sirupsen/logrus) - 结构化的日志记录库。 -- **ID生成**: [ULID](https://github.com/oklog/ulid) - 用于生成唯一、可排序的文档ID。 - -### 前端 (Frontend) - -- **核心**: [Excalidraw](https://github.com/excalidraw/excalidraw) (作为 Git Submodule) -- **框架**: [React](https://reactjs.org/) -- **构建工具**: [Vite](https://vitejs.dev/) -- **语言**: [TypeScript](https://www.typescriptlang.org/) - -### 构建与部署 (Build & Deployment) - -- **容器化**: [Docker](https://www.docker.com/) & `Dockerfile` -- **构建自动化**: Go Build Tools, npm/yarn - ---- - -## 3. 系统架构 (System Architecture) - -`excalidraw-complete` 是一个典型的**单体架构 (Monolith)**,但内部逻辑分层清晰。 - -``` -+-------------------------------------------------------------------------+ -| User | -| (Browser with Excalidraw React App) | -+-------------------------------------------------------------------------+ - | ^ - | HTTP/S (API Calls) | HTTP/S (HTML/JS/CSS) - | WebSocket (Collaboration) | - v | -+-------------------------------------------------------------------------+ -| excalidraw-complete Go Binary | -| | -| +-------------------------+ +-----------------------------------+ | -| | HTTP Server (Chi) | | Socket.IO Server | | -| |-------------------------| |-----------------------------------| | -| | - API Routes (/api/v2) | <--> | - Connection Handling | | -| | - Firebase Routes | | - Room Management (Join/Leave) | | -| | - Static File Serving | | - Message Broadcasting | | -| +-------------------------+ +-----------------------------------+ | -| | ^ | -| | | | -| v | | -| +-------------------------------------------------------------------+ | -| | Core Logic & Modules | | -| |-------------------------------------------------------------------| | -| | | | | -| | +--------------------------+ | +-----------------------------+ | | -| | | Handlers (API Logic) | | | Embedded Frontend Assets | | | -| | +--------------------------+ | | (Patched Excalidraw UI) | | | -| | | | +-----------------------------+ | | -| | v | | | -| | +--------------------------+ | | | -| | | Storage Interface | | | | -| | | (core.DocumentStore) | | | | -| | +--------------------------+ | | | -| | | | | | | | | -| |----|------|--------|-------|--------------------------------------| | -| v v v v v | -| [S3] [SQLite] [FS] [Memory] (Storage Implementations) | -| | -+-------------------------------------------------------------------------+ -``` - -**架构说明:** - -1. **Go主程序 (`main.go`)**: 作为应用的入口,它初始化并启动所有服务。 -2. **HTTP服务器**: 使用 `Chi` 路由器来处理所有HTTP请求。这包括: - - **API服务**: 提供用于创建和获取文档的 RESTful API。 - - **Firebase兼容层**: 模拟 Excalidraw 前端所需的 Firebase API。 - - **静态文件服务**: 将嵌入的、经过修改的 Excalidraw 前端应用(HTML, JS, CSS等)提供给浏览器。 -3. **Socket.IO服务器**: 独立处理 WebSocket 连接,负责所有实时协作功能,如同步绘图数据、光标位置等。 -4. **存储层 (`stores`)**: 通过一个统一的 `core.DocumentStore` 接口,将数据存储逻辑抽象出来。可以根据环境变量在启动时选择不同的实现(S3、SQLite等)。 -5. **嵌入式前端**: 前端 `Excalidraw` UI 作为一个 Git 子模块被包含在内。在构建阶段,它会被编译,并通过 Go 的 `embed` 特性直接嵌入到最终的二进制文件中。 - ---- - -## 4. 模块与服务说明 (Modules & Services) - -### 4.1. 后端 (Backend) - -#### 主应用 (`main.go`) - -- **职责**: 应用的启动器和协调器。 -- **核心逻辑**: - - 解析命令行参数 (`-listen`, `-loglevel`)。 - - 根据环境变量初始化存储层 (`stores.GetStore()`)。 - - 设置 `Chi` 路由器 (`setupRouter`),定义所有API路由。 - - 设置 `Socket.IO` 服务器 (`setupSocketIO`),定义所有协作事件。 - - 将 `/socket.io/` 路径的请求代理到 Socket.IO 服务器。 - - **动态前端服务 (`handleUI`)**: - - 使用 Go 的 `embed` 包将编译后的前端文件打包进二进制文件。 - - 在提供前端文件时,动态替换文件内容中的URL(如将 `firestore.googleapis.com` 替换为 `localhost:3002`),以重定向API请求到自身。 - - 监听系统信号以实现优雅停机 (`waitForShutdown`)。 - -#### 核心模块 (`core/`) - -- **`core/entity.go`**: 定义了项目中最核心的数据结构和接口。 - - `Document`: 代表一个画板文档。 - - `DocumentStore`: 一个接口,定义了所有存储后端必须实现的两个方法:`FindID` 和 `Create`。这是实现可插拔存储的关键。 - -#### 存储层 (`stores/`) - -- **`stores/storage.go`**: 工厂模式的实现。`GetStore()` 函数根据环境变量 `STORAGE_TYPE` 的值,创建并返回一个具体的 `DocumentStore` 接口实例。 -- **存储实现**: - - `stores/memory/`: 将文档保存在内存中,服务重启后数据丢失。 - - `stores/filesystem/`: 将每个文档作为单独的文件保存在本地文件系统上。 - - `stores/sqlite/`: 使用 SQLite 数据库来存储文档数据。 - - `stores/aws/`: 使用 AWS S3 对象存储来保存文档。 - -#### HTTP处理器 (`handlers/`) - -- **`handlers/api/documents/`**: 实现了自定义的文档API (`/api/v2`)。 - - `HandleCreate`: 处理文档的创建请求。 - - `HandleGet`: 处理文档的读取请求。 -- **`handlers/api/firebase/`**: 一个内存实现的 Firebase API 模拟层。它拦截了原始 Excalidraw 前端对 Firebase 的 `batchGet` 和 `commit` 请求,并在内存中进行处理,以确保前端协作功能可以正常工作,而无需真实的 Firebase 后端。 - -### 4.2. 前端 (Frontend) - -#### Excalidraw UI (`excalidraw/` submodule) - -- 项目通过 Git Submodule 引入了官方的 `excalidraw` 仓库。这使得跟踪上游更新变得容易。 - -#### 前端补丁 (`frontend.patch`) - -- 这是一个至关重要的文件。由于我们是自托管,需要修改 Excalidraw 前端的一些硬编码配置。该补丁文件在构建时应用,主要做了以下修改: - - **重定向API端点**: 将所有对 `excalidraw.com` 官方后端的API请求(如 `VITE_APP_BACKEND_V2_GET_URL`, `VITE_APP_WS_SERVER_URL`)重定向到自托管服务的地址(如 `http://localhost:3002`)。 - - **修改Firebase配置**: 清空部分 Firebase 配置,因为后端已经提供了兼容层。 - - **禁用追踪**: 设置 `VITE_APP_DISABLE_TRACKING=yes` 以禁用官方的数据追踪。 - -### 4.3. 前端架构分析 (Frontend Architecture) - -`excalidraw` 自身是一个复杂的 `monorepo` 项目,其核心是可独立使用的 `@excalidraw/excalidraw` 包和一个完整的Web应用 `excalidraw-app`。我们的项目构建并嵌入的是 `excalidraw-app`。 - -#### `excalidraw-app` 项目地图 (Project Map) - -以下是 `excalidraw/excalidraw-app` 目录的关键结构: - -``` -excalidraw-app/ -├── public/ # 静态资源,如 a-icons, fonts, manifest -├── components/ # 应用的主要React组件 -│ ├── AppWelcomeScreen.tsx # 欢迎界面 -│ ├── CollabButton.tsx # 协作按钮 -│ ├── Library.tsx # 元素库UI -│ ├── Tooltip.tsx # 工具提示组件 -│ └── ... # 其他UI组件 -├── data/ # 数据处理与持久化相关的模块 -│ ├── localForage.ts # IndexedDB的封装 -│ ├── excalidraw.ts # Excalidraw核心库的导出与封装 -│ └── ... -├── collab/ # 实时协作相关逻辑 -│ ├── Collab.tsx # 协作功能的封装组件 -│ ├── Portal.ts # 管理协作房间和用户 -│ └── index.ts # 协作功能的初始化与管理 -├── tests/ # 测试文件 -├── App.tsx # 应用的根React组件,组织所有UI和逻辑 -├── index.tsx # 应用的入口文件,将App组件渲染到DOM中 -└── vite.config.mts # Vite构建配置文件 -``` - -#### 核心组件与逻辑 - -- **`App.tsx`**: 这是前端的"心脏"。它是一个巨大的组件,负责: - - 渲染主要的 Excalidraw 画布 (`` 组件)。 - - 管理整个应用的状态(如图形元素、应用状态如当前工具、缩放等)。 - - 处理用户输入事件。 - - 初始化并集成协作模块 (`collab`)。 - -- **`components/`**: 包含了构成 Excalidraw 界面的所有可复用React组件,例如工具栏、菜单、对话框等。这使得UI层具有良好的模块化。 - -- **`collab/`**: 封装了所有与实时协作相关的功能。 - - 它使用 `socket.io-client` 与后端的 Socket.IO 服务器建立连接。 - - 负责发送和接收绘图数据、光标位置、用户加入/离开等事件。 - - `Portal.ts` 是关键,它维护了当前协作会话的状态。 - -- **`data/`**: 负责数据的加载和保存。在自托管模式下,它通过 `fetch` API 与我们的Go后端进行通信,以保存和加载画板数据。原始的 Firebase 逻辑被我们后端的兼容层所替代。 - -**总结**: 前端是一个高度组件化的 React 应用。通过 `frontend.patch`,我们巧妙地将其数据和协作的"后端"从官方服务切换到了我们自己的一体化Go服务器上,实现了完全的自托管。 - ---- - -## 5. 构建与部署 (Build & Deployment) - - -` \ No newline at end of file diff --git a/.cursor/rules/plan.mdc b/.cursor/rules/plan.mdc deleted file mode 100644 index 37d1e41..0000000 --- a/.cursor/rules/plan.mdc +++ /dev/null @@ -1,108 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# Excalidraw-Complete 改造计划 (BYOC - Bring Your Own Cloud Edition) - -本文档旨在规划和跟踪将 `excalidraw-complete` 升级为一个支持用户认证、多画布管理,并具备前端直连云存储能力的协作平台所需要的开发任务。 - -**核心思想**: 后端负责 **"身份认证"** 与 **"默认存储"**,前端负责 **"存储适配与执行"**。 - ---- - -## ✅ 第一阶段:后端认证与用户体系基础 - -**目标**:为应用引入用户身份。这是所有个性化功能(如按用户存储画布)的基石。 - -### 后端 (Go) -- [x] **1.1.1**: 在 `go.mod` 中添加 `golang.org/x/oauth2` 依赖。 -- [x] **1.1.2**: 创建新的 HTTP 处理器用于处理 OAuth2 流程。 -- [x] **1.1.3**: 在 `main.go` 中添加认证路由: - - [x] `GET /auth/github/login` - - [x] `GET /auth/github/callback` -- [x] **1.1.4**: 实现从 GitHub API 获取用户信息的逻辑。 -- [x] **1.1.5**: 引入 JWT 库 (e.g., `github.com/golang-jwt/jwt/v5`)。 -- [x] **1.1.6**: 实现用户登录成功后生成和颁发 JWT 的逻辑。 -- [x] **1.1.7**: 创建 `core/user.go` 定义 `User` 实体。 -- [x] **1.1.8**: 创建一个可重用的 JWT 中间件,用于解析 Token 并将用户信息注入请求上下文。 - -### 前端 (React) -- [x] **1.2.1**: 在 UI 中AppWelcomeScreen中添加"使用 GitHub 登录"按钮。在excalidraw\excalidraw-app\components\AppMainMenu.tsx中添加"登录"按钮。 -- [x] **1.2.2**: 添加api层,实现点击按钮后跳转到后端 `/auth/github/login` 的逻辑。 -- [x] **1.2.3**: 创建一个用于处理登录回调的组件/页面,能从 URL 中解析出 JWT。 -- [x] **1.2.4**: 将获取到的 JWT 安全地存储在 `localStorage` 或 `sessionStorage` 中。 -- [x] **1.2.5**: 创建一个全局 API 请求封装(如 Axios 拦截器),为所有请求自动附加 `Authorization` 头。 -- [x] **1.2.6**: 建立全局用户状态管理 (e.g., Jotai/Zustand),并在登录后更新 UI(如显示用户头像)。 - ---- - -## ✅ 第二阶段:前端存储抽象层与UI框架 - -**目标**:在前端建立一个灵活的存储适配器架构和相应的UI,为后续接入多种存储后端做好准备。 - -### 前端 (React) -- [x] **2.1.1**: 在 `src/data/` 目录下创建 `storage.ts` 文件。 -- [x] **2.1.2**: 在 `storage.ts` 中定义 `IStorageAdapter` TypeScript 接口,包含 `listCanvases`, `loadCanvas`, `saveCanvas`, `createCanvas`, `deleteCanvas` 等方法。 -- [x] **2.1.3**: 设计并实现一个新的"数据源配置"设置页面或模态框。 -- [x] **2.1.4**: 在设置UI中,创建一个下拉菜单,包含未来的存储选项("默认后端", "Cloudflare KV", "Amazon S3")。 -- [x] **2.1.5**: 根据下拉菜单的选择,动态渲染用于输入凭证的表单。 -- [x] **2.1.6**: 在 UI 上添加明确的安全警告,告知用户密钥仅存储在浏览器会话中。 -- [x] **2.1.7**: 创建全局状态来管理存储配置,将敏感凭证存储在 `sessionStorage`,非敏感配置存储在 `localStorage`。 - ---- - -## ✅ 第三阶段:实现默认后端KV作为第一个存储适配器 - -**目标**:将项目自身的 Go 后端作为第一个可用的存储选项,实现并验证前端的存储适配器架构。 - -### 后端 (Go) -- [ ] **3.1.1**: 升级 `core/entity.go` 中的 `Document` 结构,增加 `UserID`, `Name`, `CreatedAt` 等字段。 -- [ ] **3.1.2**: 重构 `core.DocumentStore` 接口,增加 `ListByUser`, `Update`, `Delete` 等方法,并使所有方法都接受 `userID` 参数。 -- [ ] **3.1.3**: 更新所有现有的存储实现 (`sqlite`, `filesystem` 等) 以匹配新的 `DocumentStore` 接口,并确保数据按用户ID隔离。 -- [ ] **3.1.4**: 创建新的受 JWT 保护的 API 路由: - - [ ] `GET /api/v2/canvases` - - [ ] `POST /api/v2/canvases` - - [ ] `GET /api/v2/canvases/{id}` - - [ ] `PUT /api/v2/canvases/{id}` - - [ ] `DELETE /api/v2/canvases/{id}` - -### 前端 (React) -- [ ] **3.2.1**: 创建 `src/data/BackendStorageAdapter.ts` 文件,并使其实现 `IStorageAdapter` 接口。 -- [ ] **3.2.2**: 在该适配器内部,实现所有接口方法,使其通过 `fetch` 调用后端的 `/api/v2/canvases` 相关 API。 -- [ ] **3.2.3**: 实现多画布管理的侧边栏 UI。 -- [ ] **3.2.4**: 将侧边栏 UI 与 `BackendStorageAdapter` 连接,实现一个功能完整的、由后端驱动的多画布管理系统。 - ---- - -## ✅ 第四阶段:实现Cloudflare KV客户端适配器 - -**目标**:实现第一个纯前端的存储选项,数据直接从浏览器发送到用户的Cloudflare KV。 - -### 前端 (React) -- [ ] **4.1.1**: 创建 `src/data/CloudflareKVAdapter.ts` 文件,并使其实现 `IStorageAdapter` 接口。 -- [ ] **4.1.2**: 实现其构造函数,用于接收用户输入的 `accountId`, `namespaceId`, 和 `apiToken`。 -- [ ] **4.1.3**: 在适配器内部,使用 `fetch` 实现对 Cloudflare KV 官方 API 的直接调用。 -- [ ] **4.1.4**: 设计并在适配器中实现 KV 的键名(Key)管理策略。 -- [ ] **4.1.5**: 在主应用逻辑中,当用户在设置中选择并配置了 Cloudflare KV 后,实例化并切换到 `CloudflareKVAdapter`。 -- [ ] **4.1.6**: 验证所有画布操作(增删改查)都能在用户的 CF KV 上正确执行。 - ---- - -## ✅ 第五阶段:实现Amazon S3客户端适配器与最终打磨 - -**目标**:添加对S3的支持,并完善整个用户体验。 - -### 前端 (React) -- [ ] **5.1.1**: 在前端项目中添加 AWS SDK 依赖: `npm install @aws-sdk/client-s3`。 -- [ ] **5.1.2**: 创建 `src/data/S3StorageAdapter.ts` 文件,并使其实现 `IStorageAdapter` 接口。 -- [ ] **5.1.3**: 实现其构造函数,用于接收用户输入的 `accessKeyId`, `secretAccessKey`, `region`, `bucketName`。 -- [ ] **5.1.4**: 在适配器内部,使用 `@aws-sdk/client-s3` 实现对 S3 对象的 `List`, `Get`, `Put`, `Delete` 操作。 -- [ ] **5.1.5**: 设计并在适配器中实现 S3 的对象键(Key)管理策略。 -- [ ] **5.1.6**: 在主应用逻辑中,当用户在设置中选择并配置了 S3 后,实例化并切换到 `S3StorageAdapter`。 - -### UX/UI 打磨 -- [ ] **5.2.1**: 在每个数据源配置界面添加"测试连接"按钮,提供即时反馈。 -- [ ] **5.2.2**: 完善在不同数据源之间切换时的用户体验,如提示保存未保存的更改。 - -- [ ] **5.2.3**: 在文档和UI中提供详细的指南,说明如何获取各种云服务的API密钥。 \ No newline at end of file diff --git a/.env.example b/.env.example index 891833b..4def7be 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,14 @@ +# GitHub OAuth 配置 GITHUB_CLIENT_ID="xxxxxxxxxxxxxxxxxxx" GITHUB_CLIENT_SECRET="xxxxxxxxxxxxxx" -GITHUB_REDIRECT_URL="http://localhost:3000/auth/github/callback" # 或者你部署后的回调地址 +GITHUB_REDIRECT_URL="http://localhost:3002/auth/callback" # 或者你部署后的回调地址 + +# JWT 配置 JWT_SECRET="YOUR_SUPER_SECRET_RANDOM_STRING" +# OpenAI 配置 OPENAI_API_KEY=sk-xxxxxxxxxxxxxx OPENAI_BASE_URL=https://xxxxxx.xxxxx # 不加/v1... + +# 存储配置 STORAGE_TYPE=sqlite # 支持memory,filesystem, kv, s3,具体看README diff --git a/.env.example.dex b/.env.example.dex new file mode 100644 index 0000000..c8e9bac --- /dev/null +++ b/.env.example.dex @@ -0,0 +1,18 @@ +OIDC_ISSUER_URL=http://localhost:5556 +OIDC_CLIENT_ID=excalidraw +OIDC_CLIENT_SECRET=excalidraw-secret +OIDC_REDIRECT_URL=http://localhost:3000/auth/callback + +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD_HASH='$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W' +ADMIN_USERNAME=admin # Optional +ADMIN_USER_ID=admin1234 # Optional + +JWT_SECRET=your_super_secret_jwt_string + +STORAGE_TYPE=sqlite +DATA_SOURCE_NAME=excalidraw.db +LOCAL_STORAGE_PATH=./data + +OPENAI_API_KEY=sk-your_openai_api_key +OPENAI_BASE_URL=https://api.openai.com diff --git a/.gitignore b/.gitignore index 7c31eca..a9e4618 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ node_modules *.db data/ .idea +.htpasswd diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index cc0e44b..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,214 +0,0 @@ -# Excalidraw-Complete 架构文档 - -本文档旨在详细阐述 `excalidraw-complete` 项目的系统架构、技术栈、模块设计和数据流,以便于开发者理解、维护和进行二次开发。 - -## 1. 概述 (Overview) - -`excalidraw-complete` 是一个将优秀的开源白板工具 [Excalidraw](https://github.com/excalidraw/excalidraw) 进行整合与封装的自托管解决方案。其核心目标是简化 Excalidraw 的私有化部署流程,将前端UI、后端数据存储和实时协作服务打包成一个单一的、易于部署的Go二进制文件。 - -**核心特性:** - -- **一体化部署**:将所有服务打包成单个可执行文件,无需复杂的依赖配置。 -- **可插拔存储**:通过环境变量支持多种数据持久化方案,包括内存、本地文件系统、SQLite和AWS S3。 -- **实时协作**:内置基于 Socket.IO 的实时协作服务器,允许多个用户同时在同一个画板上工作。 -- **Firebase 兼容层**:提供一个内存实现的 Firebase API 兼容层,以满足 Excalidraw 前端对 Firebase 的部分依赖。 - ---- - -## 2. 技术栈 (Tech Stack) - -项目采用了现代化的前后端技术栈。 - -### 后端 (Backend) - -- **语言**: [Go](https://go.dev/) (v1.21+) -- **Web框架**: [Chi (v5)](https://github.com/go-chi/chi) - 一个轻量级、高性能的 Go HTTP 路由器。 -- **实时通信**: [Socket.IO for Go](https://github.com/zishang520/socket.io/v2) - 实现了 Socket.IO 协议,用于实时协作。 -- **数据库驱动**: - - [go-sqlite3](https://github.com/mattn/go-sqlite3) - 用于 SQLite 存储。 - - [aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) - 用于与 AWS S3 交互。 -- **日志**: [Logrus](https://github.com/sirupsen/logrus) - 结构化的日志记录库。 -- **ID生成**: [ULID](https://github.com/oklog/ulid) - 用于生成唯一、可排序的文档ID。 - -### 前端 (Frontend) - -- **核心**: [Excalidraw](https://github.com/excalidraw/excalidraw) (作为 Git Submodule) -- **框架**: [React](https://reactjs.org/) -- **构建工具**: [Vite](https://vitejs.dev/) -- **语言**: [TypeScript](https://www.typescriptlang.org/) - -### 构建与部署 (Build & Deployment) - -- **容器化**: [Docker](https://www.docker.com/) & `Dockerfile` -- **构建自动化**: Go Build Tools, npm/yarn - ---- - -## 3. 系统架构 (System Architecture) - -`excalidraw-complete` 是一个典型的**单体架构 (Monolith)**,但内部逻辑分层清晰。 - -``` -+-------------------------------------------------------------------------+ -| User | -| (Browser with Excalidraw React App) | -+-------------------------------------------------------------------------+ - | ^ - | HTTP/S (API Calls) | HTTP/S (HTML/JS/CSS) - | WebSocket (Collaboration) | - v | -+-------------------------------------------------------------------------+ -| excalidraw-complete Go Binary | -| | -| +-------------------------+ +-----------------------------------+ | -| | HTTP Server (Chi) | | Socket.IO Server | | -| |-------------------------| |-----------------------------------| | -| | - API Routes (/api/v2) | <--> | - Connection Handling | | -| | - Firebase Routes | | - Room Management (Join/Leave) | | -| | - Static File Serving | | - Message Broadcasting | | -| +-------------------------+ +-----------------------------------+ | -| | ^ | -| | | | -| v | | -| +-------------------------------------------------------------------+ | -| | Core Logic & Modules | | -| |-------------------------------------------------------------------| | -| | | | | -| | +--------------------------+ | +-----------------------------+ | | -| | | Handlers (API Logic) | | | Embedded Frontend Assets | | | -| | +--------------------------+ | | (Patched Excalidraw UI) | | | -| | | | +-----------------------------+ | | -| | v | | | -| | +--------------------------+ | | | -| | | Storage Interface | | | | -| | | (core.DocumentStore) | | | | -| | +--------------------------+ | | | -| | | | | | | | | -| |----|------|--------|-------|--------------------------------------| | -| v v v v v | -| [S3] [SQLite] [FS] [Memory] (Storage Implementations) | -| | -+-------------------------------------------------------------------------+ -``` - -**架构说明:** - -1. **Go主程序 (`main.go`)**: 作为应用的入口,它初始化并启动所有服务。 -2. **HTTP服务器**: 使用 `Chi` 路由器来处理所有HTTP请求。这包括: - - **API服务**: 提供用于创建和获取文档的 RESTful API。 - - **Firebase兼容层**: 模拟 Excalidraw 前端所需的 Firebase API。 - - **静态文件服务**: 将嵌入的、经过修改的 Excalidraw 前端应用(HTML, JS, CSS等)提供给浏览器。 -3. **Socket.IO服务器**: 独立处理 WebSocket 连接,负责所有实时协作功能,如同步绘图数据、光标位置等。 -4. **存储层 (`stores`)**: 通过一个统一的 `core.DocumentStore` 接口,将数据存储逻辑抽象出来。可以根据环境变量在启动时选择不同的实现(S3、SQLite等)。 -5. **嵌入式前端**: 前端 `Excalidraw` UI 作为一个 Git 子模块被包含在内。在构建阶段,它会被编译,并通过 Go 的 `embed` 特性直接嵌入到最终的二进制文件中。 - ---- - -## 4. 模块与服务说明 (Modules & Services) - -### 4.1. 后端 (Backend) - -#### 主应用 (`main.go`) - -- **职责**: 应用的启动器和协调器。 -- **核心逻辑**: - - 解析命令行参数 (`-listen`, `-loglevel`)。 - - 根据环境变量初始化存储层 (`stores.GetStore()`)。 - - 设置 `Chi` 路由器 (`setupRouter`),定义所有API路由。 - - 设置 `Socket.IO` 服务器 (`setupSocketIO`),定义所有协作事件。 - - 将 `/socket.io/` 路径的请求代理到 Socket.IO 服务器。 - - **动态前端服务 (`handleUI`)**: - - 使用 Go 的 `embed` 包将编译后的前端文件打包进二进制文件。 - - 在提供前端文件时,动态替换文件内容中的URL(如将 `firestore.googleapis.com` 替换为 `localhost:3002`),以重定向API请求到自身。 - - 监听系统信号以实现优雅停机 (`waitForShutdown`)。 - -#### 核心模块 (`core/`) - -- **`core/entity.go`**: 定义了项目中最核心的数据结构和接口。 - - `Document`: 代表一个画板文档。 - - `DocumentStore`: 一个接口,定义了所有存储后端必须实现的两个方法:`FindID` 和 `Create`。这是实现可插拔存储的关键。 - -#### 存储层 (`stores/`) - -- **`stores/storage.go`**: 工厂模式的实现。`GetStore()` 函数根据环境变量 `STORAGE_TYPE` 的值,创建并返回一个具体的 `DocumentStore` 接口实例。 -- **存储实现**: - - `stores/memory/`: 将文档保存在内存中,服务重启后数据丢失。 - - `stores/filesystem/`: 将每个文档作为单独的文件保存在本地文件系统上。 - - `stores/sqlite/`: 使用 SQLite 数据库来存储文档数据。 - - `stores/aws/`: 使用 AWS S3 对象存储来保存文档。 - -#### HTTP处理器 (`handlers/`) - -- **`handlers/api/documents/`**: 实现了自定义的文档API (`/api/v2`)。 - - `HandleCreate`: 处理文档的创建请求。 - - `HandleGet`: 处理文档的读取请求。 -- **`handlers/api/firebase/`**: 一个内存实现的 Firebase API 模拟层。它拦截了原始 Excalidraw 前端对 Firebase 的 `batchGet` 和 `commit` 请求,并在内存中进行处理,以确保前端协作功能可以正常工作,而无需真实的 Firebase 后端。 - -### 4.2. 前端 (Frontend) - -#### Excalidraw UI (`excalidraw/` submodule) - -- 项目通过 Git Submodule 引入了官方的 `excalidraw` 仓库。这使得跟踪上游更新变得容易。 - -#### 前端补丁 (`frontend.patch`) - -- 这是一个至关重要的文件。由于我们是自托管,需要修改 Excalidraw 前端的一些硬编码配置。该补丁文件在构建时应用,主要做了以下修改: - - **重定向API端点**: 将所有对 `excalidraw.com` 官方后端的API请求(如 `VITE_APP_BACKEND_V2_GET_URL`, `VITE_APP_WS_SERVER_URL`)重定向到自托管服务的地址(如 `http://localhost:3002`)。 - - **修改Firebase配置**: 清空部分 Firebase 配置,因为后端已经提供了兼容层。 - - **禁用追踪**: 设置 `VITE_APP_DISABLE_TRACKING=yes` 以禁用官方的数据追踪。 - -### 4.3. 前端架构分析 (Frontend Architecture) - -`excalidraw` 自身是一个复杂的 `monorepo` 项目,其核心是可独立使用的 `@excalidraw/excalidraw` 包和一个完整的Web应用 `excalidraw-app`。我们的项目构建并嵌入的是 `excalidraw-app`。 - -#### `excalidraw-app` 项目地图 (Project Map) - -以下是 `excalidraw/excalidraw-app` 目录的关键结构: - -``` -excalidraw-app/ -├── public/ # 静态资源,如 a-icons, fonts, manifest -├── components/ # 应用的主要React组件 -│ ├── AppWelcomeScreen.tsx # 欢迎界面 -│ ├── CollabButton.tsx # 协作按钮 -│ ├── Library.tsx # 元素库UI -│ ├── Tooltip.tsx # 工具提示组件 -│ └── ... # 其他UI组件 -├── data/ # 数据处理与持久化相关的模块 -│ ├── localForage.ts # IndexedDB的封装 -│ ├── excalidraw.ts # Excalidraw核心库的导出与封装 -│ └── ... -├── collab/ # 实时协作相关逻辑 -│ ├── Collab.tsx # 协作功能的封装组件 -│ ├── Portal.ts # 管理协作房间和用户 -│ └── index.ts # 协作功能的初始化与管理 -├── tests/ # 测试文件 -├── App.tsx # 应用的根React组件,组织所有UI和逻辑 -├── index.tsx # 应用的入口文件,将App组件渲染到DOM中 -└── vite.config.mts # Vite构建配置文件 -``` - -#### 核心组件与逻辑 - -- **`App.tsx`**: 这是前端的"心脏"。它是一个巨大的组件,负责: - - 渲染主要的 Excalidraw 画布 (`` 组件)。 - - 管理整个应用的状态(如图形元素、应用状态如当前工具、缩放等)。 - - 处理用户输入事件。 - - 初始化并集成协作模块 (`collab`)。 - -- **`components/`**: 包含了构成 Excalidraw 界面的所有可复用React组件,例如工具栏、菜单、对话框等。这使得UI层具有良好的模块化。 - -- **`collab/`**: 封装了所有与实时协作相关的功能。 - - 它使用 `socket.io-client` 与后端的 Socket.IO 服务器建立连接。 - - 负责发送和接收绘图数据、光标位置、用户加入/离开等事件。 - - `Portal.ts` 是关键,它维护了当前协作会话的状态。 - -- **`data/`**: 负责数据的加载和保存。在自托管模式下,它通过 `fetch` API 与我们的Go后端进行通信,以保存和加载画板数据。原始的 Firebase 逻辑被我们后端的兼容层所替代。 - -**总结**: 前端是一个高度组件化的 React 应用。通过 `frontend.patch`,我们巧妙地将其数据和协作的"后端"从官方服务切换到了我们自己的一体化Go服务器上,实现了完全的自托管。 - ---- - -## 5. 构建与部署 (Build & Deployment) - -` \ No newline at end of file diff --git a/PROJECT_REFACTOR_PLAN.md b/PROJECT_REFACTOR_PLAN.md deleted file mode 100644 index a6ac9b7..0000000 --- a/PROJECT_REFACTOR_PLAN.md +++ /dev/null @@ -1,106 +0,0 @@ -# Excalidraw-Complete 改造计划 (BYOC - Bring Your Own Cloud Edition) - -本文档旨在规划和跟踪将 `excalidraw-complete` 升级为一个支持用户认证、多画布管理,并具备前端直连云存储能力的协作平台所需要的开发任务。 - -**核心思想**: 后端负责 **"身份认证"** 与 **"默认存储"**,前端负责 **"存储适配与执行"**。 - ---- - -## ✅ 第一阶段:后端认证与用户体系基础 - -**目标**:为应用引入用户身份。这是所有个性化功能(如按用户存储画布)的基石。 - -### 后端 (Go) -- [x] **1.1.1**: 在 `go.mod` 中添加 `golang.org/x/oauth2` 依赖。 -- [x] **1.1.2**: 创建新的 HTTP 处理器用于处理 OAuth2 流程。 -- [x] **1.1.3**: 在 `main.go` 中添加认证路由: - - [x] `GET /auth/github/login` - - [x] `GET /auth/github/callback` -- [x] **1.1.4**: 实现从 GitHub API 获取用户信息的逻辑。 -- [x] **1.1.5**: 引入 JWT 库 (e.g., `github.com/golang-jwt/jwt/v5`)。 -- [x] **1.1.6**: 实现用户登录成功后生成和颁发 JWT 的逻辑。 -- [x] **1.1.7**: 创建 `core/user.go` 定义 `User` 实体。 -- [x] **1.1.8**: 创建一个可重用的 JWT 中间件,用于解析 Token 并将用户信息注入请求上下文。 - -### 前端 (React) -- [x] **1.2.1**: 在 UI 中AppWelcomeScreen中添加"使用 GitHub 登录"按钮。在excalidraw\excalidraw-app\components\AppMainMenu.tsx中添加"登录"按钮。 -- [x] **1.2.2**: 添加api层,实现点击按钮后跳转到后端 `/auth/github/login` 的逻辑。 -- [x] **1.2.3**: 创建一个用于处理登录回调的组件/页面,能从 URL 中解析出 JWT。 -- [x] **1.2.4**: 将获取到的 JWT 安全地存储在 `localStorage` 或 `sessionStorage` 中。 -- [x] **1.2.5**: 创建一个全局 API 请求封装(如 Axios 拦截器),为所有请求自动附加 `Authorization` 头。 -- [x] **1.2.6**: 建立全局用户状态管理 (e.g., Jotai/Zustand),并在登录后更新 UI(如显示用户头像)。 - ---- - -## ✅ 第二阶段:前端存储抽象层与UI框架 - -**目标**:在前端建立一个灵活的存储适配器架构和相应的UI,为后续接入多种存储后端做好准备。 - -### 前端 (React) -- [x] **2.1.1**: 在 `src/data/` 目录下创建 `storage.ts` 文件。 -- [x] **2.1.2**: 在 `storage.ts` 中定义 `IStorageAdapter` TypeScript 接口,包含 `listCanvases`, `loadCanvas`, `saveCanvas`, `createCanvas`, `deleteCanvas` 等方法。 -- [x] **2.1.3**: 设计并实现一个新的"数据源配置"设置页面或模态框。 -- [x] **2.1.4**: 在设置UI中,创建一个下拉菜单,包含未来的存储选项("默认后端", "Cloudflare KV", "Amazon S3","IndexDB")。 -- [x] **2.1.5**: 根据下拉菜单的选择,动态渲染用于输入凭证的表单。 -- [x] **2.1.6**: 在 UI 上添加明确的安全警告,告知用户密钥仅存储在浏览器会话中。 -- [x] **2.1.7**: 创建全局状态来管理存储配置,将敏感凭证存储在 `sessionStorage`,非敏感配置存储在 `localStorage`。 - ---- - -## ✅ 第三阶段:实现后端作为第一个KV存储适配器 - -**目标**:将项目自身的 Go 后端实现为一个简单的、面向用户的KV存储,作为第一个可用的存储选项。 - -### 后端 (Go) - KV API 设计 -- **API理念**: 放弃复杂的RESTful设计,提供纯粹的KV操作接口,所有权与当前JWT用户绑定。 -- **路由规划**: - - `GET /api/v2/kv`: 列出当前用户所有画布的元信息 (ID, Name, UpdatedAt)。 - - `GET /api/v2/kv/{key}`: 获取单个画布的完整内容。 - - `PUT /api/v2/kv/{key}`: 创建或更新一个画布。 - - `DELETE /api/v2/kv/{key}`: 删除一个画布。 - -### 后端 (Go) - 执行步骤 -- [x] **3.1.1**: 创建新的 `core/canvas.go` 文件,定义 `Canvas` 实体和 `CanvasStore` 接口。此举可避免与用于实时协作的旧 `Document` 模型冲突。 -- [x] **3.1.2**: `Canvas` 实体将包含 `ID`, `UserID`, `Name`, `Data`, `CreatedAt`, `UpdatedAt` 字段。 -- [x] **3.1.3**: `CanvasStore` 接口将定义 `List`, `Get`, `Save`, `Delete` 方法,所有方法都基于 `UserID` 操作以保证数据隔离。 -- [x] **3.1.4**: 更新现有存储实现 (`sqlite`, `filesystem` 等) 以实现新的 `CanvasStore` 接口。 -- [x] **3.1.5**: 创建新的 `handlers/api/kv/` 目录和处理器,实现上述KV API路由,并使用JWT中间件进行保护。 - -### 前端 (React) -- [x] **3.2.1**: 创建 `src/data/BackendStorageAdapter.ts` 文件,并使其实现 `IStorageAdapter` 接口。 -- [x] **3.2.2**: 在该适配器内部,实现所有接口方法,使其通过 `fetch` 调用后端的 `/api/v2/kv` 相关 API。 -- [x] **3.2.3**: 实现多画布管理的侧边栏 UI。 -- [x] **3.2.4**: 将侧边栏 UI 与 `BackendStorageAdapter` 连接,实现一个功能完整的、由后端驱动的多画布管理系统。 - ---- - -## ✅ 第四阶段:实现Cloudflare KV客户端适配器 - -**目标**:实现第一个纯前端的存储选项,数据直接从浏览器发送到用户的Cloudflare KV。 - -### 前端 (React) -- [ ] **4.1.1**: 创建 `src/data/CloudflareKVAdapter.ts` 文件,并使其实现 `IStorageAdapter` 接口。 -- [ ] **4.1.2**: 实现其构造函数,用于接收用户输入的 `accountId`, `namespaceId`, 和 `apiToken`。 -- [ ] **4.1.3**: 在适配器内部,使用 `fetch` 实现对 Cloudflare KV 官方 API 的直接调用。 -- [ ] **4.1.4**: 设计并在适配器中实现 KV 的键名(Key)管理策略。 -- [ ] **4.1.5**: 在主应用逻辑中,当用户在设置中选择并配置了 Cloudflare KV 后,实例化并切换到 `CloudflareKVAdapter`。 -- [ ] **4.1.6**: 验证所有画布操作(增删改查)都能在用户的 CF KV 上正确执行。 - ---- - -## ✅ 第五阶段:实现Amazon S3客户端适配器与最终打磨 - -**目标**:添加对S3的支持,并完善整个用户体验。 - -### 前端 (React) -- [ ] **5.1.1**: 在前端项目中添加 AWS SDK 依赖: `npm install @aws-sdk/client-s3`。 -- [ ] **5.1.2**: 创建 `src/data/S3StorageAdapter.ts` 文件,并使其实现 `IStorageAdapter` 接口。 -- [ ] **5.1.3**: 实现其构造函数,用于接收用户输入的 `accessKeyId`, `secretAccessKey`, `region`, `bucketName`。 -- [ ] **5.1.4**: 在适配器内部,使用 `@aws-sdk/client-s3` 实现对 S3 对象的 `List`, `Get`, `Put`, `Delete` 操作。 -- [ ] **5.1.5**: 设计并在适配器中实现 S3 的对象键(Key)管理策略。 -- [ ] **5.1.6**: 在主应用逻辑中,当用户在设置中选择并配置了 S3 后,实例化并切换到 `S3StorageAdapter`。 - -### UX/UI 打磨 -- [ ] **5.2.1**: 在每个数据源配置界面添加"测试连接"按钮,提供即时反馈。 -- [ ] **5.2.2**: 完善在不同数据源之间切换时的用户体验,如提示保存未保存的更改。 -- [ ] **5.2.3**: 在文档和UI中提供详细的指南,说明如何获取各种云服务的API密钥。 \ No newline at end of file diff --git a/README.md b/README.md index 3c8f8b1..d047c04 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,33 @@ docker compose up -d The server will start, and you can access the application at `http://localhost:3002`. + + +
+Use Simple Password Authentication(Dex OIDC) + +```bash +# Example for Linux +git clone https://github.com/BetterAndBetterII/excalidraw-full.git +cd excalidraw-full +mv .env.example .env +touch ./excalidraw.db # IMPORTANT: Initialize the SQLite DB, OTHERWISE IT WILL NOT START +docker compose -f docker-compose.dex.yml up -d +``` + +Change your password in `.env` file. + +```bash +# apt install apache2-utils +# Generate the password hash +echo YOUR_NEW_PASSWORD | htpasswd -BinC 10 admin | cut -d: -f2 > .htpasswd +# Update your .env file +sed -i "s|ADMIN_PASSWORD_HASH=.*|ADMIN_PASSWORD_HASH='$(cat .htpasswd)'|" .env +``` + +
+ + ## Configuration Configuration is managed via environment variables. For a full template, see the `.env.example` section below. @@ -63,7 +90,7 @@ You must configure GitHub OAuth and a JWT secret for the application to function - `GITHUB_CLIENT_ID`: Your GitHub OAuth App's Client ID. - `GITHUB_CLIENT_SECRET`: Your GitHub OAuth App's Client Secret. -- `GITHUB_REDIRECT_URL`: The callback URL. For local testing, this is `http://localhost:3002/auth/github/callback`. +- `GITHUB_REDIRECT_URL`: The callback URL. For local testing, this is `http://localhost:3002/auth/callback`. - `JWT_SECRET`: A strong, random string for signing session tokens. Generate one with `openssl rand -base64 32`. - `OPENAI_API_KEY`: Your secret key from OpenAI. - `OPENAI_BASE_URL`: (Optional) For using compatible APIs like Azure OpenAI. @@ -97,7 +124,7 @@ Create a `.env` file in the project root and add the following, filling in your # Get from https://github.com/settings/developers GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret -GITHUB_REDIRECT_URL=http://localhost:3002/auth/github/callback +GITHUB_REDIRECT_URL=http://localhost:3002/auth/callback # Generate with: openssl rand -base64 32 JWT_SECRET=your_super_secret_jwt_string @@ -129,7 +156,7 @@ docker build -t excalidraw-complete -f excalidraw-complete.Dockerfile . docker run -p 3002:3002 \ -e GITHUB_CLIENT_ID="your_id" \ -e GITHUB_CLIENT_SECRET="your_secret" \ - -e GITHUB_REDIRECT_URL="http://localhost:3002/auth/github/callback" \ + -e GITHUB_REDIRECT_URL="http://localhost:3002/auth/callback" \ -e JWT_SECRET="your_jwt_secret" \ -e STORAGE_TYPE="sqlite" \ -e DATA_SOURCE_NAME="excalidraw.db" \ diff --git a/README_zh.md b/README_zh.md index ae4c753..f232ceb 100644 --- a/README_zh.md +++ b/README_zh.md @@ -53,6 +53,32 @@ docker compose up -d 服务器将启动,您可以在 `http://localhost:3002` 访问该应用。 + +
+使用简单密码认证(Dex OIDC) + +```bash +# 示例 +git clone https://github.com/BetterAndBetterII/excalidraw-full.git +cd excalidraw-full +mv .env.example .env +touch ./excalidraw.db # 重要:初始化 SQLite 数据库,否则无法启动 +docker compose -f docker-compose.dex.yml up -d +``` + +修改 `.env` 文件中的密码。 + +```bash +# apt install apache2-utils +# 生成密码哈希 +echo YOUR_NEW_PASSWORD | htpasswd -BinC 10 admin | cut -d: -f2 > .htpasswd +# 更新 .env 文件 +sed -i "s|ADMIN_PASSWORD_HASH=.*|ADMIN_PASSWORD_HASH='$(cat .htpasswd)'|" .env +``` + +
+ + ## 配置 配置通过环境变量进行管理。有关完整模板,请参阅下面的 `.env.example` 部分。 @@ -63,7 +89,7 @@ docker compose up -d - `GITHUB_CLIENT_ID`: 您的 GitHub OAuth App 的 Client ID。 - `GITHUB_CLIENT_SECRET`: 您的 GitHub OAuth App 的 Client Secret。 -- `GITHUB_REDIRECT_URL`: 回调 URL。对于本地测试,这是 `http://localhost:3002/auth/github/callback`。 +- `GITHUB_REDIRECT_URL`: 回调 URL。对于本地测试,这是 `http://localhost:3002/auth/callback`。 - `JWT_SECRET`: 用于签署会话令牌的强随机字符串。使用 `openssl rand -base64 32` 生成一个。 - `OPENAI_API_KEY`: 您在 OpenAI 的秘密密钥。 - `OPENAI_BASE_URL`: (可选) 用于使用兼容的 API,如 Azure OpenAI。 @@ -97,7 +123,7 @@ docker compose up -d # 从 https://github.com/settings/developers 获取 GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret -GITHUB_REDIRECT_URL=http://localhost:3002/auth/github/callback +GITHUB_REDIRECT_URL=http://localhost:3002/auth/callback # 使用以下命令生成: openssl rand -base64 32 JWT_SECRET=your_super_secret_jwt_string @@ -129,7 +155,7 @@ docker build -t excalidraw-complete -f excalidraw-complete.Dockerfile . docker run -p 3002:3002 \ -e GITHUB_CLIENT_ID="your_id" \ -e GITHUB_CLIENT_SECRET="your_secret" \ - -e GITHUB_REDIRECT_URL="http://localhost:3002/auth/github/callback" \ + -e GITHUB_REDIRECT_URL="http://localhost:3002/auth/callback" \ -e JWT_SECRET="your_jwt_secret" \ -e STORAGE_TYPE="sqlite" \ -e DATA_SOURCE_NAME="excalidraw.db" \ diff --git a/config/dex.config.yaml b/config/dex.config.yaml new file mode 100644 index 0000000..5dc384d --- /dev/null +++ b/config/dex.config.yaml @@ -0,0 +1,28 @@ +issuer: {{ .Env.OIDC_ISSUER }} + +storage: + type: memory + +web: + http: 0.0.0.0:5556 + allowedOrigins: ["*"] + +logger: + level: debug + format: text + +enablePasswordDB: true + +staticClients: + - id: excalidraw + redirectURIs: + - {{ .Env.OIDC_REDIRECT_URL }} + name: Excalidraw + public: true + secret: excalidraw-secret + +staticPasswords: + - email: {{ .Env.ADMIN_EMAIL }} + hash: {{ .Env.ADMIN_PASSWORD_HASH }} + username: {{ .Env.ADMIN_USERNAME }} + userID: {{ .Env.ADMIN_USER_ID }} diff --git a/core/user.go b/core/user.go index 9a5a4c3..aad8f8e 100644 --- a/core/user.go +++ b/core/user.go @@ -5,8 +5,9 @@ import "time" type ( User struct { ID uint `json:"id" gorm:"primarykey"` - GitHubID int64 `json:"githubId" gorm:"unique"` - Login string `json:"login"` + Subject string `json:"subject" gorm:"uniqueIndex"` + Login string `json:"login" gorm:"uniqueIndex"` + Email string `json:"email"` AvatarURL string `json:"avatarUrl"` Name string `json:"name"` CreatedAt time.Time `json:"createdAt"` diff --git a/docker-compose.dex.yml b/docker-compose.dex.yml new file mode 100644 index 0000000..3fdf446 --- /dev/null +++ b/docker-compose.dex.yml @@ -0,0 +1,50 @@ +version: '3.8' + +services: + netpod: + image: busybox:latest + ports: + - "5556:5556" # Dex + - "3002:3002" # Excalidraw + command: ["sleep", "infinity"] + networks: + - excalidraw-network + + dex: + image: dexidp/dex:v2.38.0 + container_name: excalidraw-dex + restart: unless-stopped + volumes: + - ./config/dex.config.yaml:/etc/dex/config.yaml + environment: + - OIDC_REDIRECT_URL=${OIDC_REDIRECT_URL:-http://localhost:3002/auth/callback} + - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET:-excalidraw-secret} + - OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-excalidraw} + - OIDC_ISSUER=${OIDC_ISSUER:-http://localhost:5556} + - ADMIN_USERNAME=${ADMIN_USERNAME:-admin} + - ADMIN_PASSWORD_HASH=${ADMIN_PASSWORD_HASH:-your_secure_password} + - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} + - ADMIN_USER_ID=${ADMIN_USER_ID:-'admin1234'} + command: ["dex", "serve", "/etc/dex/config.yaml"] + healthcheck: + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:5556/.well-known/openid-configuration"] + interval: 1s + timeout: 1s + retries: 10 + start_period: 10s + network_mode: service:netpod + + excalidraw: + image: ghcr.io/betterandbetterii/excalidraw-full:latest + volumes: + - ./data:/root/data + - ./excalidraw.db:/root/excalidraw.db:Z + - ./.env:/root/.env + depends_on: + dex: + condition: service_healthy + network_mode: service:netpod + +networks: + excalidraw-network: + driver: bridge diff --git a/excalidraw b/excalidraw index b0248d8..b5cca50 160000 --- a/excalidraw +++ b/excalidraw @@ -1 +1 @@ -Subproject commit b0248d8c3067fd3fa807da4c3538486d6fa420f8 +Subproject commit b5cca508d480108457faa62fda06b9071c132d94 diff --git a/go.mod b/go.mod index a51157a..685f320 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.28.5 // indirect github.com/aws/smithy-go v1.20.1 // indirect + github.com/coreos/go-oidc/v3 v3.15.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index 3d8aa03..509f335 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.28.5 h1:J/PpTf/hllOjx8Xu9DMflff3Fajf github.com/aws/aws-sdk-go-v2/service/sts v1.28.5/go.mod h1:0ih0Z83YDH/QeQ6Ori2yGE2XvWYv/Xm+cZc01LC6oK0= github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg= +github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -51,6 +53,8 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= diff --git a/handlers/auth/auth.go b/handlers/auth/auth.go new file mode 100644 index 0000000..c59bda9 --- /dev/null +++ b/handlers/auth/auth.go @@ -0,0 +1,377 @@ +package auth + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "excalidraw-complete/core" + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" + "golang.org/x/oauth2/github" + + "encoding/hex" + + "github.com/coreos/go-oidc/v3/oidc" +) + +var ( + loginHandler http.HandlerFunc + callbackHandler http.HandlerFunc +) + +var ( + githubOauthConfig *oauth2.Config + jwtSecret []byte + + oidcOauthConfig *oauth2.Config + oidcProvider *oidc.Provider + verifier *oidc.IDTokenVerifier +) + +// AppClaims represents the custom claims for the JWT. +type AppClaims struct { + jwt.RegisteredClaims + Login string `json:"login"` + Email string `json:"email,omitempty"` + AvatarURL string `json:"avatarUrl"` + Name string `json:"name"` +} + +// OIDCClaims represents the claims from OIDC token +type OIDCClaims struct { + Email string `json:"email"` + Name string `json:"name"` + PreferredUsername string `json:"preferred_username"` + Picture string `json:"picture"` + Sub string `json:"sub"` +} + +func InitAuth() { + oidcConfigured := os.Getenv("OIDC_ISSUER_URL") != "" && os.Getenv("OIDC_CLIENT_ID") != "" + githubConfigured := os.Getenv("GITHUB_CLIENT_ID") != "" && os.Getenv("GITHUB_CLIENT_SECRET") != "" + + if oidcConfigured { + logrus.Info("Initializing OIDC authentication provider.") + initOIDC() + loginHandler = HandleOIDCLogin + callbackHandler = HandleOIDCCallback + } else if githubConfigured { + logrus.Info("Initializing GitHub authentication provider.") + initGitHub() + loginHandler = HandleGitHubLogin + callbackHandler = HandleGitHubCallback + } else { + logrus.Warn("No authentication provider configured.") + dummyHandler := func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Authentication not configured", http.StatusInternalServerError) + } + loginHandler = dummyHandler + callbackHandler = dummyHandler + } + + jwtSecret = []byte(os.Getenv("JWT_SECRET")) + if len(jwtSecret) == 0 { + logrus.Warn("JWT_SECRET is not set. Authentication will not work.") + } +} + +func HandleLogin(w http.ResponseWriter, r *http.Request) { + if loginHandler != nil { + loginHandler(w, r) + } else { + http.Error(w, "Authentication not configured", http.StatusInternalServerError) + } +} + +func HandleCallback(w http.ResponseWriter, r *http.Request) { + if callbackHandler != nil { + callbackHandler(w, r) + } else { + http.Error(w, "Authentication not configured", http.StatusInternalServerError) + } +} + +func initGitHub() { + githubOauthConfig = &oauth2.Config{ + ClientID: os.Getenv("GITHUB_CLIENT_ID"), + ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"), + RedirectURL: os.Getenv("GITHUB_REDIRECT_URL"), + Scopes: []string{"read:user", "user:email"}, + Endpoint: github.Endpoint, + } + + if githubOauthConfig.ClientID == "" || githubOauthConfig.ClientSecret == "" { + logrus.Warn("GitHub OAuth credentials are not set. Authentication routes will not work.") + } +} + +func initOIDC() { + providerURL := os.Getenv("OIDC_ISSUER_URL") + clientID := os.Getenv("OIDC_CLIENT_ID") + clientSecret := os.Getenv("OIDC_CLIENT_SECRET") + redirectURL := os.Getenv("OIDC_REDIRECT_URL") + + if providerURL == "" || clientID == "" || clientSecret == "" { + logrus.Warn("OIDC credentials are not set. OIDC authentication routes will not work.") + return + } + + var err error + oidcProvider, err = oidc.NewProvider(context.Background(), providerURL) + if err != nil { + logrus.Errorf("Failed to create OIDC provider: %s", err.Error()) + return + } + + oidcOauthConfig = &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURL: redirectURL, + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + Endpoint: oidcProvider.Endpoint(), + } + + logrus.Info("OIDC provider initialized") + + verifier = oidcProvider.Verifier(&oidc.Config{ + ClientID: clientID, + }) +} + +// Init function is deprecated, use InitAuth instead +func Init() { + initGitHub() + + jwtSecret = []byte(os.Getenv("JWT_SECRET")) + + if githubOauthConfig.ClientID == "" || githubOauthConfig.ClientSecret == "" { + logrus.Warn("GitHub OAuth credentials are not set. Authentication routes will not work.") + } + if len(jwtSecret) == 0 { + logrus.Warn("JWT_SECRET is not set. Authentication routes will not work.") + } +} + +func generateStateOauthCookie(w http.ResponseWriter) string { + b := make([]byte, 16) + rand.Read(b) + state := base64.URLEncoding.EncodeToString(b) + cookie := &http.Cookie{ + Name: "oauthstate", + Value: state, + Expires: time.Now().Add(10 * time.Minute), + HttpOnly: true, + } + http.SetCookie(w, cookie) + return state +} + +func HandleGitHubLogin(w http.ResponseWriter, r *http.Request) { + if githubOauthConfig.ClientID == "" { + http.Error(w, "GitHub OAuth is not configured", http.StatusInternalServerError) + return + } + state := generateStateOauthCookie(w) + url := githubOauthConfig.AuthCodeURL(state) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func HandleGitHubCallback(w http.ResponseWriter, r *http.Request) { + if githubOauthConfig.ClientID == "" { + http.Error(w, "GitHub OAuth is not configured", http.StatusInternalServerError) + return + } + + token, err := githubOauthConfig.Exchange(context.Background(), r.FormValue("code")) + if err != nil { + logrus.Errorf("failed to exchange token: %s", err.Error()) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + client := githubOauthConfig.Client(context.Background(), token) + resp, err := client.Get("https://api.github.com/user") + if err != nil { + logrus.Errorf("failed to get user from github: %s", err.Error()) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + logrus.Errorf("failed to read github response body: %s", err.Error()) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + var githubUser struct { + ID int64 `json:"id"` + Login string `json:"login"` + AvatarURL string `json:"avatar_url"` + Name string `json:"name"` + } + + if err := json.Unmarshal(body, &githubUser); err != nil { + logrus.Errorf("failed to unmarshal github user: %s", err.Error()) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + // Create user object using Subject instead of GitHubID + user := &core.User{ + Subject: fmt.Sprintf("github:%d", githubUser.ID), + Login: githubUser.Login, + AvatarURL: githubUser.AvatarURL, + Name: githubUser.Name, + } + + jwtToken, err := createJWT(user) + if err != nil { + logrus.Errorf("failed to create JWT: %s", err.Error()) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + // Redirect to frontend with token + http.Redirect(w, r, fmt.Sprintf("/?token=%s", jwtToken), http.StatusTemporaryRedirect) +} + +func HandleOIDCLogin(w http.ResponseWriter, r *http.Request) { + if oidcOauthConfig == nil { + http.Error(w, "OIDC is not configured", http.StatusInternalServerError) + return + } + + // Generate random state + stateBytes := make([]byte, 16) + _, err := rand.Read(stateBytes) + if err != nil { + http.Error(w, "Failed to generate state for OIDC login", http.StatusInternalServerError) + return + } + state := hex.EncodeToString(stateBytes) + + // Set state in a cookie + http.SetCookie(w, &http.Cookie{ + Name: "oidc_state", + Value: state, + Path: "/", + Expires: time.Now().Add(10 * time.Minute), // 10 minutes expiry + HttpOnly: true, + Secure: r.Header.Get("X-Forwarded-Proto") == "https", + SameSite: http.SameSiteLaxMode, + }) + + url := oidcOauthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func HandleOIDCCallback(w http.ResponseWriter, r *http.Request) { + if oidcOauthConfig == nil { + http.Error(w, "OIDC is not configured", http.StatusInternalServerError) + return + } + + code := r.FormValue("code") + if code == "" { + logrus.Error("no code in callback") + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + token, err := oidcOauthConfig.Exchange(context.Background(), code) + if err != nil { + logrus.Errorf("failed to exchange token: %s", err.Error()) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + logrus.Error("no id_token in token response") + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + idToken, err := verifier.Verify(context.Background(), rawIDToken) + if err != nil { + logrus.Errorf("failed to verify ID token: %s", err.Error()) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + var claims OIDCClaims + if err := idToken.Claims(&claims); err != nil { + logrus.Errorf("failed to extract claims from ID token: %s", err.Error()) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + // Create user from OIDC claims + user := &core.User{ + Subject: claims.Sub, + Login: claims.PreferredUsername, + Email: claims.Email, + AvatarURL: claims.Picture, + Name: claims.Name, + } + + // If preferred_username is not available, use email + if user.Login == "" && user.Email != "" { + user.Login = user.Email + } + + jwtToken, err := createJWT(user) + if err != nil { + logrus.Errorf("failed to create JWT: %s", err.Error()) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + // Redirect to frontend with token + http.Redirect(w, r, fmt.Sprintf("/?token=%s", jwtToken), http.StatusTemporaryRedirect) +} + +func createJWT(user *core.User) (string, error) { + claims := AppClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Subject: user.Subject, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 7)), // 1 week + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Login: user.Login, + AvatarURL: user.AvatarURL, + Name: user.Name, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtSecret) +} + +func ParseJWT(tokenString string) (*AppClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &AppClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return jwtSecret, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*AppClaims); ok && token.Valid { + return claims, nil + } + + return nil, fmt.Errorf("invalid token") +} diff --git a/handlers/auth/github.go b/handlers/auth/github.go deleted file mode 100644 index 55bc65b..0000000 --- a/handlers/auth/github.go +++ /dev/null @@ -1,180 +0,0 @@ -package auth - -import ( - "context" - "crypto/rand" - "encoding/base64" - "encoding/json" - "excalidraw-complete/core" - "fmt" - "io" - "net/http" - "os" - "time" - - "github.com/golang-jwt/jwt/v5" - "github.com/sirupsen/logrus" - "golang.org/x/oauth2" - "golang.org/x/oauth2/github" -) - -var ( - githubOauthConfig *oauth2.Config - jwtSecret []byte -) - -const oauthStateString = "random" - -// AppClaims represents the custom claims for the JWT. -type AppClaims struct { - jwt.RegisteredClaims - Login string `json:"login"` - AvatarURL string `json:"avatarUrl"` - Name string `json:"name"` -} - -func Init() { - githubOauthConfig = &oauth2.Config{ - ClientID: os.Getenv("GITHUB_CLIENT_ID"), - ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"), - RedirectURL: os.Getenv("GITHUB_REDIRECT_URL"), - Scopes: []string{"read:user", "user:email"}, - Endpoint: github.Endpoint, - } - jwtSecret = []byte(os.Getenv("JWT_SECRET")) - - if githubOauthConfig.ClientID == "" || githubOauthConfig.ClientSecret == "" { - logrus.Warn("GitHub OAuth credentials are not set. Authentication routes will not work.") - } - if len(jwtSecret) == 0 { - logrus.Warn("JWT_SECRET is not set. Authentication routes will not work.") - } -} - -func generateStateOauthCookie(w http.ResponseWriter) string { - b := make([]byte, 16) - rand.Read(b) - state := base64.URLEncoding.EncodeToString(b) - cookie := &http.Cookie{ - Name: "oauthstate", - Value: state, - Expires: time.Now().Add(10 * time.Minute), - HttpOnly: true, - } - http.SetCookie(w, cookie) - return state -} - -func HandleGitHubLogin(w http.ResponseWriter, r *http.Request) { - if githubOauthConfig.ClientID == "" { - http.Error(w, "GitHub OAuth is not configured", http.StatusInternalServerError) - return - } - state := generateStateOauthCookie(w) - url := githubOauthConfig.AuthCodeURL(state) - http.Redirect(w, r, url, http.StatusTemporaryRedirect) -} - -func HandleGitHubCallback(w http.ResponseWriter, r *http.Request) { - if githubOauthConfig.ClientID == "" { - http.Error(w, "GitHub OAuth is not configured", http.StatusInternalServerError) - return - } - - oauthState, _ := r.Cookie("oauthstate") - if r.FormValue("state") != oauthState.Value { - logrus.Error("invalid oauth github state") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - token, err := githubOauthConfig.Exchange(context.Background(), r.FormValue("code")) - if err != nil { - logrus.Errorf("failed to exchange token: %s", err.Error()) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - client := githubOauthConfig.Client(context.Background(), token) - resp, err := client.Get("https://api.github.com/user") - if err != nil { - logrus.Errorf("failed to get user from github: %s", err.Error()) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - logrus.Errorf("failed to read github response body: %s", err.Error()) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - var githubUser struct { - ID int64 `json:"id"` - Login string `json:"login"` - AvatarURL string `json:"avatar_url"` - Name string `json:"name"` - } - - if err := json.Unmarshal(body, &githubUser); err != nil { - logrus.Errorf("failed to unmarshal github user: %s", err.Error()) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - // For now we don't have a user database, so we create a user object on the fly. - // In phase 3, we will save/get the user from the database here. - user := &core.User{ - GitHubID: githubUser.ID, - Login: githubUser.Login, - AvatarURL: githubUser.AvatarURL, - Name: githubUser.Name, - } - - jwtToken, err := createJWT(user) - if err != nil { - logrus.Errorf("failed to create JWT: %s", err.Error()) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - // Redirect to frontend with token - http.Redirect(w, r, fmt.Sprintf("/?token=%s", jwtToken), http.StatusTemporaryRedirect) -} - -func createJWT(user *core.User) (string, error) { - claims := AppClaims{ - RegisteredClaims: jwt.RegisteredClaims{ - Subject: fmt.Sprintf("%d", user.GitHubID), - ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 7)), // 1 week - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Login: user.Login, - AvatarURL: user.AvatarURL, - Name: user.Name, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(jwtSecret) -} - -func ParseJWT(tokenString string) (*AppClaims, error) { - token, err := jwt.ParseWithClaims(tokenString, &AppClaims{}, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return jwtSecret, nil - }) - - if err != nil { - return nil, err - } - - if claims, ok := token.Claims.(*AppClaims); ok && token.Valid { - return claims, nil - } - - return nil, fmt.Errorf("invalid token") -} diff --git a/main.go b/main.go index ace7ab3..a1ddd10 100644 --- a/main.go +++ b/main.go @@ -166,9 +166,9 @@ func setupRouter(store stores.Store) *chi.Mux { }) }) - r.Route("/auth/github", func(r chi.Router) { - r.Get("/login", auth.HandleGitHubLogin) - r.Get("/callback", auth.HandleGitHubCallback) + r.Route("/auth", func(r chi.Router) { + r.Get("/login", auth.HandleLogin) + r.Get("/callback", auth.HandleCallback) }) return r @@ -306,7 +306,7 @@ func main() { FullTimestamp: true, }) - auth.Init() + auth.InitAuth() openai.Init() store := stores.GetStore()