A deployable markdown editor that connects with your self hosted files and lets you edit in a beautiful interface
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add SQLite database setup with migrations

- Create database package with connection pooling and WAL mode
- Add comprehensive database schema (users, auth_tokens, branch_states, draft_content)
- Implement migration runner using embedded SQL files
- Add data models for all database tables
- Create query functions for user and auth token operations
- Integrate database initialization into main server startup
- Build test successful

+505 -12
+49
.claude/claude.md
··· 1 + # Claude Development Guidelines for MarkEdit 2 + 3 + ## Package Manager 4 + 5 + **Use Bun for all npm-related tasks:** 6 + - Use `bun install` instead of `npm install` or `yarn install` 7 + - Use `bun run` instead of `npm run` or `yarn run` 8 + - Use `bun add` instead of `npm install <package>` or `yarn add` 9 + - Use `bun remove` instead of `npm uninstall` or `yarn remove` 10 + 11 + Examples: 12 + ```bash 13 + # Install dependencies 14 + bun install 15 + 16 + # Run dev server 17 + bun run dev 18 + 19 + # Add a package 20 + bun add react 21 + 22 + # Add a dev dependency 23 + bun add -d typescript 24 + 25 + # Remove a package 26 + bun remove lodash 27 + ``` 28 + 29 + ## Git Commit Guidelines 30 + 31 + **Create commits for logical code boundaries:** 32 + - A phase can and should have multiple commits 33 + - Don't wait until the end of a phase to commit 34 + - Break work into logical, self-contained chunks 35 + - Each commit should represent a complete, working state 36 + - Commit messages should clearly describe what was accomplished 37 + 38 + Examples of good commit boundaries: 39 + - "Add SQLite database setup and initial migrations" 40 + - "Implement GitHub OAuth flow with goth" 41 + - "Add session management with encrypted cookies" 42 + - "Create health check and auth endpoints" 43 + - "Add repository listing API endpoint" 44 + - "Implement file tree component with recursive rendering" 45 + 46 + **Avoid:** 47 + - Single massive commits per phase 48 + - Commits with mixed concerns (e.g., "Add auth and fix styling") 49 + - Incomplete or broken code in commits
+42
.claude/skills/frontend-design/SKILL.md
··· 1 + --- 2 + name: frontend-design 3 + description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics. 4 + license: Complete terms in LICENSE.txt 5 + --- 6 + 7 + This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. 8 + 9 + The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. 10 + 11 + ## Design Thinking 12 + 13 + Before coding, understand the context and commit to a BOLD aesthetic direction: 14 + - **Purpose**: What problem does this interface solve? Who uses it? 15 + - **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. 16 + - **Constraints**: Technical requirements (framework, performance, accessibility). 17 + - **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? 18 + 19 + **CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. 20 + 21 + Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: 22 + - Production-grade and functional 23 + - Visually striking and memorable 24 + - Cohesive with a clear aesthetic point-of-view 25 + - Meticulously refined in every detail 26 + 27 + ## Frontend Aesthetics Guidelines 28 + 29 + Focus on: 30 + - **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. 31 + - **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. 32 + - **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. 33 + - **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. 34 + - **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. 35 + 36 + NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. 37 + 38 + Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. 39 + 40 + **IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. 41 + 42 + Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
+29 -11
IMPLEMENTATION_PLAN.md
··· 36 36 ### Frontend 37 37 - **Framework**: Astro (client-only, no SSR) 38 38 - **UI Library**: React 18 39 + - **UI Components**: shadcn/ui (with Tailwind CSS) 39 40 - **Editor**: TipTap (React) 40 41 - **Styling**: Tailwind CSS 41 42 - **State Management**: `@tanstack/react-query` for server state ··· 407 408 │ │ │ ├── Header.tsx 408 409 │ │ │ ├── Sidebar.tsx 409 410 │ │ │ └── Layout.tsx 410 - │ │ └── ui/ 411 - │ │ ├── Button.tsx 412 - │ │ ├── Modal.tsx 413 - │ │ └── ... (shadcn/ui components or custom) 411 + │ │ └── ui/ # shadcn/ui components 412 + │ │ ├── button.tsx 413 + │ │ ├── dialog.tsx 414 + │ │ ├── dropdown-menu.tsx 415 + │ │ ├── input.tsx 416 + │ │ ├── label.tsx 417 + │ │ ├── separator.tsx 418 + │ │ ├── spinner.tsx 419 + │ │ └── ... (other shadcn/ui components) 414 420 │ ├── lib/ 415 421 │ │ ├── api/ 416 422 │ │ │ ├── client.ts # Axios instance with interceptors ··· 418 424 │ │ │ ├── files.ts # File API calls 419 425 │ │ │ └── auth.ts # Auth API calls 420 426 │ │ ├── utils/ 427 + │ │ │ ├── cn.ts # Tailwind class merging utility (shadcn) 421 428 │ │ │ ├── markdown.ts # Markdown/frontmatter parsing 422 429 │ │ │ ├── debounce.ts 423 430 │ │ │ └── formatters.ts ··· 437 444 │ └── favicon.svg 438 445 ├── astro.config.mjs 439 446 ├── tailwind.config.mjs 447 + ├── components.json # shadcn/ui configuration 440 448 ├── tsconfig.json 441 449 └── package.json 442 450 ``` ··· 673 681 **Frontend Tasks:** 674 682 - [ ] Astro project setup 675 683 - [ ] Tailwind CSS configuration 684 + - [ ] shadcn/ui setup and initial components (button, dialog, input, etc.) 676 685 - [ ] Landing page with "Login with GitHub" button 677 686 - [ ] OAuth callback handler 678 687 - [ ] Dashboard shell (empty) ··· 916 925 │ │ │ │ ├── Header.tsx 917 926 │ │ │ │ ├── Sidebar.tsx 918 927 │ │ │ │ └── Layout.tsx 919 - │ │ │ ├── ui/ 920 - │ │ │ │ ├── Button.tsx 921 - │ │ │ │ ├── Modal.tsx 922 - │ │ │ │ ├── Spinner.tsx 923 - │ │ │ │ └── Alert.tsx 928 + │ │ │ ├── ui/ # shadcn/ui components 929 + │ │ │ │ ├── button.tsx 930 + │ │ │ │ ├── dialog.tsx 931 + │ │ │ │ ├── dropdown-menu.tsx 932 + │ │ │ │ ├── input.tsx 933 + │ │ │ │ ├── label.tsx 934 + │ │ │ │ ├── separator.tsx 935 + │ │ │ │ └── ... (other shadcn/ui components) 924 936 │ │ │ └── App.tsx # Main React app 925 937 │ │ ├── lib/ 926 938 │ │ │ ├── api/ ··· 934 946 │ │ │ │ ├── useFiles.ts 935 947 │ │ │ │ └── useFileContent.ts 936 948 │ │ │ ├── utils/ 949 + │ │ │ │ ├── cn.ts # Tailwind class merging utility 937 950 │ │ │ │ ├── markdown.ts 938 951 │ │ │ │ ├── debounce.ts 939 952 │ │ │ │ └── formatters.ts ··· 952 965 │ │ └── favicon.svg 953 966 │ ├── astro.config.mjs 954 967 │ ├── tailwind.config.mjs 968 + │ ├── components.json # shadcn/ui configuration 955 969 │ ├── tsconfig.json 956 970 │ └── package.json 957 971 ├── docker/ ··· 991 1005 992 1006 # Frontend setup (new terminal) 993 1007 cd frontend 994 - npm install 995 - npm run dev 1008 + bun install 1009 + # Initialize shadcn/ui (if not already done) 1010 + bunx shadcn-ui@latest init 1011 + bun run dev 996 1012 997 1013 # Visit http://localhost:3000 998 1014 ``` ··· 1151 1167 ## Additional Resources 1152 1168 1153 1169 - **TipTap:** https://tiptap.dev/docs 1170 + - **shadcn/ui:** https://ui.shadcn.com 1171 + - **Astro with React:** https://docs.astro.build/en/guides/integrations-guide/react/ 1154 1172 - **go-git:** https://github.com/go-git/go-git 1155 1173 - **goth:** https://github.com/markbates/goth 1156 1174 - **Astro:** https://docs.astro.build
+19 -1
backend/cmd/server/main.go
··· 11 11 "time" 12 12 13 13 "github.com/joho/godotenv" 14 + "github.com/yourusername/markedit/internal/database" 14 15 ) 15 16 16 17 func main() { ··· 19 20 log.Println("No .env file found, using environment variables") 20 21 } 21 22 22 - // Get port from environment 23 + // Get configuration 23 24 port := os.Getenv("PORT") 24 25 if port == "" { 25 26 port = "8080" 27 + } 28 + 29 + dbPath := os.Getenv("DATABASE_PATH") 30 + if dbPath == "" { 31 + dbPath = "./data/markedit.db" 32 + } 33 + 34 + // Initialize database 35 + db, err := database.New(dbPath) 36 + if err != nil { 37 + log.Fatalf("Failed to initialize database: %v", err) 38 + } 39 + defer db.Close() 40 + 41 + // Run migrations 42 + if err := db.RunMigrations(); err != nil { 43 + log.Fatalf("Failed to run migrations: %v", err) 26 44 } 27 45 28 46 // Create HTTP server
+14
backend/go.mod
··· 3 3 go 1.24.1 4 4 5 5 require github.com/joho/godotenv v1.5.1 6 + 7 + require ( 8 + github.com/dustin/go-humanize v1.0.1 // indirect 9 + github.com/google/uuid v1.6.0 // indirect 10 + github.com/mattn/go-isatty v0.0.20 // indirect 11 + github.com/ncruces/go-strftime v1.0.0 // indirect 12 + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 13 + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect 14 + golang.org/x/sys v0.37.0 // indirect 15 + modernc.org/libc v1.67.6 // indirect 16 + modernc.org/mathutil v1.7.1 // indirect 17 + modernc.org/memory v1.11.0 // indirect 18 + modernc.org/sqlite v1.44.3 // indirect 19 + )
+23
backend/go.sum
··· 1 + github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 2 + github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 3 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 1 5 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 2 6 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 7 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 8 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 9 + github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= 10 + github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 11 + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 12 + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 13 + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= 14 + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= 15 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 + golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 17 + golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 18 + modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= 19 + modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= 20 + modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 21 + modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 22 + modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= 23 + modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= 24 + modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= 25 + modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
+62
backend/internal/database/db.go
··· 1 + package database 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + "log" 7 + "os" 8 + "path/filepath" 9 + 10 + _ "modernc.org/sqlite" 11 + ) 12 + 13 + // DB wraps the database connection 14 + type DB struct { 15 + *sql.DB 16 + } 17 + 18 + // New creates a new database connection 19 + func New(dbPath string) (*DB, error) { 20 + // Ensure the directory exists 21 + dir := filepath.Dir(dbPath) 22 + if err := os.MkdirAll(dir, 0755); err != nil { 23 + return nil, fmt.Errorf("failed to create database directory: %w", err) 24 + } 25 + 26 + // Open database connection 27 + db, err := sql.Open("sqlite", dbPath) 28 + if err != nil { 29 + return nil, fmt.Errorf("failed to open database: %w", err) 30 + } 31 + 32 + // Configure connection pool 33 + db.SetMaxOpenConns(1) // SQLite works best with single connection 34 + db.SetMaxIdleConns(1) 35 + 36 + // Enable WAL mode for better concurrency 37 + if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { 38 + db.Close() 39 + return nil, fmt.Errorf("failed to enable WAL mode: %w", err) 40 + } 41 + 42 + // Enable foreign keys 43 + if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil { 44 + db.Close() 45 + return nil, fmt.Errorf("failed to enable foreign keys: %w", err) 46 + } 47 + 48 + // Test connection 49 + if err := db.Ping(); err != nil { 50 + db.Close() 51 + return nil, fmt.Errorf("failed to ping database: %w", err) 52 + } 53 + 54 + log.Printf("Connected to database: %s", dbPath) 55 + 56 + return &DB{db}, nil 57 + } 58 + 59 + // Close closes the database connection 60 + func (db *DB) Close() error { 61 + return db.DB.Close() 62 + }
+29
backend/internal/database/migrations.go
··· 1 + package database 2 + 3 + import ( 4 + "embed" 5 + "fmt" 6 + "log" 7 + ) 8 + 9 + //go:embed migrations/*.sql 10 + var migrationFiles embed.FS 11 + 12 + // RunMigrations executes all SQL migration files 13 + func (db *DB) RunMigrations() error { 14 + log.Println("Running database migrations...") 15 + 16 + // Read and execute the initial migration 17 + sqlBytes, err := migrationFiles.ReadFile("migrations/001_initial.sql") 18 + if err != nil { 19 + return fmt.Errorf("failed to read migration file: %w", err) 20 + } 21 + 22 + // Execute the migration 23 + if _, err := db.Exec(string(sqlBytes)); err != nil { 24 + return fmt.Errorf("failed to execute migration: %w", err) 25 + } 26 + 27 + log.Println("Database migrations completed successfully") 28 + return nil 29 + }
+55
backend/internal/database/migrations/001_initial.sql
··· 1 + -- Users table 2 + CREATE TABLE IF NOT EXISTS users ( 3 + id INTEGER PRIMARY KEY AUTOINCREMENT, 4 + github_id INTEGER UNIQUE, 5 + username TEXT NOT NULL, 6 + email TEXT, 7 + avatar_url TEXT, 8 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 9 + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 10 + ); 11 + 12 + -- Sessions/Tokens (encrypted access tokens) 13 + CREATE TABLE IF NOT EXISTS auth_tokens ( 14 + id INTEGER PRIMARY KEY AUTOINCREMENT, 15 + user_id INTEGER NOT NULL, 16 + provider TEXT NOT NULL, 17 + access_token TEXT NOT NULL, 18 + refresh_token TEXT, 19 + expires_at DATETIME, 20 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 21 + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 22 + ); 23 + 24 + -- Branch state tracking 25 + CREATE TABLE IF NOT EXISTS branch_states ( 26 + id INTEGER PRIMARY KEY AUTOINCREMENT, 27 + user_id INTEGER NOT NULL, 28 + repo_full_name TEXT NOT NULL, 29 + branch_name TEXT NOT NULL, 30 + base_branch TEXT DEFAULT 'main', 31 + last_push_at DATETIME NOT NULL, 32 + has_uncommitted_changes BOOLEAN DEFAULT FALSE, 33 + file_paths TEXT, 34 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 35 + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, 36 + UNIQUE(user_id, repo_full_name), 37 + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 38 + ); 39 + 40 + -- Work-in-progress content (auto-save buffer) 41 + CREATE TABLE IF NOT EXISTS draft_content ( 42 + id INTEGER PRIMARY KEY AUTOINCREMENT, 43 + user_id INTEGER NOT NULL, 44 + repo_full_name TEXT NOT NULL, 45 + file_path TEXT NOT NULL, 46 + content TEXT NOT NULL, 47 + last_saved_at DATETIME DEFAULT CURRENT_TIMESTAMP, 48 + UNIQUE(user_id, repo_full_name, file_path), 49 + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 50 + ); 51 + 52 + -- Indexes for performance 53 + CREATE INDEX IF NOT EXISTS idx_auth_tokens_user_provider ON auth_tokens(user_id, provider); 54 + CREATE INDEX IF NOT EXISTS idx_branch_states_user_repo ON branch_states(user_id, repo_full_name); 55 + CREATE INDEX IF NOT EXISTS idx_draft_content_user_repo_file ON draft_content(user_id, repo_full_name, file_path);
+49
backend/internal/database/models.go
··· 1 + package database 2 + 3 + import "time" 4 + 5 + // User represents a user in the system 6 + type User struct { 7 + ID int `json:"id"` 8 + GithubID int `json:"github_id"` 9 + Username string `json:"username"` 10 + Email string `json:"email"` 11 + AvatarURL string `json:"avatar_url"` 12 + CreatedAt time.Time `json:"created_at"` 13 + UpdatedAt time.Time `json:"updated_at"` 14 + } 15 + 16 + // AuthToken represents an OAuth token for a provider 17 + type AuthToken struct { 18 + ID int `json:"id"` 19 + UserID int `json:"user_id"` 20 + Provider string `json:"provider"` 21 + AccessToken string `json:"-"` // Never expose in JSON 22 + RefreshToken string `json:"-"` // Never expose in JSON 23 + ExpiresAt time.Time `json:"expires_at"` 24 + CreatedAt time.Time `json:"created_at"` 25 + } 26 + 27 + // BranchState tracks the state of a user's branch for a repository 28 + type BranchState struct { 29 + ID int `json:"id"` 30 + UserID int `json:"user_id"` 31 + RepoFullName string `json:"repo_full_name"` 32 + BranchName string `json:"branch_name"` 33 + BaseBranch string `json:"base_branch"` 34 + LastPushAt time.Time `json:"last_push_at"` 35 + HasUncommittedChanges bool `json:"has_uncommitted_changes"` 36 + FilePaths string `json:"file_paths"` // JSON array 37 + CreatedAt time.Time `json:"created_at"` 38 + UpdatedAt time.Time `json:"updated_at"` 39 + } 40 + 41 + // DraftContent represents auto-saved content 42 + type DraftContent struct { 43 + ID int `json:"id"` 44 + UserID int `json:"user_id"` 45 + RepoFullName string `json:"repo_full_name"` 46 + FilePath string `json:"file_path"` 47 + Content string `json:"content"` 48 + LastSavedAt time.Time `json:"last_saved_at"` 49 + }
+134
backend/internal/database/queries.go
··· 1 + package database 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + ) 7 + 8 + // CreateUser creates a new user or updates if exists 9 + func (db *DB) CreateUser(user *User) error { 10 + query := ` 11 + INSERT INTO users (github_id, username, email, avatar_url, updated_at) 12 + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) 13 + ON CONFLICT(github_id) DO UPDATE SET 14 + username = excluded.username, 15 + email = excluded.email, 16 + avatar_url = excluded.avatar_url, 17 + updated_at = CURRENT_TIMESTAMP 18 + RETURNING id, created_at, updated_at 19 + ` 20 + 21 + return db.QueryRow(query, user.GithubID, user.Username, user.Email, user.AvatarURL). 22 + Scan(&user.ID, &user.CreatedAt, &user.UpdatedAt) 23 + } 24 + 25 + // GetUserByGithubID retrieves a user by their GitHub ID 26 + func (db *DB) GetUserByGithubID(githubID int) (*User, error) { 27 + user := &User{} 28 + query := ` 29 + SELECT id, github_id, username, email, avatar_url, created_at, updated_at 30 + FROM users 31 + WHERE github_id = ? 32 + ` 33 + 34 + err := db.QueryRow(query, githubID).Scan( 35 + &user.ID, 36 + &user.GithubID, 37 + &user.Username, 38 + &user.Email, 39 + &user.AvatarURL, 40 + &user.CreatedAt, 41 + &user.UpdatedAt, 42 + ) 43 + 44 + if err == sql.ErrNoRows { 45 + return nil, fmt.Errorf("user not found") 46 + } 47 + if err != nil { 48 + return nil, fmt.Errorf("failed to get user: %w", err) 49 + } 50 + 51 + return user, nil 52 + } 53 + 54 + // GetUserByID retrieves a user by their ID 55 + func (db *DB) GetUserByID(id int) (*User, error) { 56 + user := &User{} 57 + query := ` 58 + SELECT id, github_id, username, email, avatar_url, created_at, updated_at 59 + FROM users 60 + WHERE id = ? 61 + ` 62 + 63 + err := db.QueryRow(query, id).Scan( 64 + &user.ID, 65 + &user.GithubID, 66 + &user.Username, 67 + &user.Email, 68 + &user.AvatarURL, 69 + &user.CreatedAt, 70 + &user.UpdatedAt, 71 + ) 72 + 73 + if err == sql.ErrNoRows { 74 + return nil, fmt.Errorf("user not found") 75 + } 76 + if err != nil { 77 + return nil, fmt.Errorf("failed to get user: %w", err) 78 + } 79 + 80 + return user, nil 81 + } 82 + 83 + // SaveAuthToken saves or updates an auth token 84 + func (db *DB) SaveAuthToken(token *AuthToken) error { 85 + query := ` 86 + INSERT INTO auth_tokens (user_id, provider, access_token, refresh_token, expires_at) 87 + VALUES (?, ?, ?, ?, ?) 88 + ON CONFLICT(user_id, provider) DO UPDATE SET 89 + access_token = excluded.access_token, 90 + refresh_token = excluded.refresh_token, 91 + expires_at = excluded.expires_at, 92 + created_at = CURRENT_TIMESTAMP 93 + RETURNING id, created_at 94 + ` 95 + 96 + // Note: In production, these tokens should be encrypted before storage 97 + return db.QueryRow(query, 98 + token.UserID, 99 + token.Provider, 100 + token.AccessToken, 101 + token.RefreshToken, 102 + token.ExpiresAt, 103 + ).Scan(&token.ID, &token.CreatedAt) 104 + } 105 + 106 + // GetAuthToken retrieves an auth token for a user and provider 107 + func (db *DB) GetAuthToken(userID int, provider string) (*AuthToken, error) { 108 + token := &AuthToken{} 109 + query := ` 110 + SELECT id, user_id, provider, access_token, refresh_token, expires_at, created_at 111 + FROM auth_tokens 112 + WHERE user_id = ? AND provider = ? 113 + ` 114 + 115 + err := db.QueryRow(query, userID, provider).Scan( 116 + &token.ID, 117 + &token.UserID, 118 + &token.Provider, 119 + &token.AccessToken, 120 + &token.RefreshToken, 121 + &token.ExpiresAt, 122 + &token.CreatedAt, 123 + ) 124 + 125 + if err == sql.ErrNoRows { 126 + return nil, fmt.Errorf("token not found") 127 + } 128 + if err != nil { 129 + return nil, fmt.Errorf("failed to get token: %w", err) 130 + } 131 + 132 + // Note: In production, these tokens should be decrypted after retrieval 133 + return token, nil 134 + }