328 lines
9.9 KiB
Markdown
328 lines
9.9 KiB
Markdown
# AGENTS.md - Developer Guide for oslstats
|
|
|
|
This document provides guidelines for AI coding agents and developers working on the oslstats codebase.
|
|
|
|
## Project Overview
|
|
|
|
**Module**: `git.haelnorr.com/h/oslstats`
|
|
**Language**: Go 1.25.5
|
|
**Architecture**: Web application with Discord OAuth, PostgreSQL database, templ templates
|
|
**Key Technologies**: Bun ORM, templ, TailwindCSS, custom golib libraries
|
|
|
|
## Build, Test, and Development Commands
|
|
|
|
### Building
|
|
```bash
|
|
# Full production build (tailwind → templ → go generate → go build)
|
|
make build
|
|
|
|
# Build and run
|
|
make run
|
|
|
|
# Clean build artifacts
|
|
make clean
|
|
```
|
|
|
|
### Development Mode
|
|
```bash
|
|
# Watch mode with hot reload (templ, air, tailwindcss in parallel)
|
|
make dev
|
|
|
|
# Development server runs on:
|
|
# - Proxy: http://localhost:3000 (use this)
|
|
# - App: http://localhost:3333 (internal)
|
|
```
|
|
|
|
### Testing
|
|
```bash
|
|
# Run all tests
|
|
go test ./...
|
|
|
|
# Run tests for a specific package
|
|
go test ./pkg/oauth
|
|
|
|
# Run a single test function
|
|
go test ./pkg/oauth -run TestGenerateState_Success
|
|
|
|
# Run tests with verbose output
|
|
go test -v ./pkg/oauth
|
|
|
|
# Run tests with coverage
|
|
go test -cover ./...
|
|
go test -coverprofile=coverage.out ./...
|
|
go tool cover -html=coverage.out
|
|
```
|
|
|
|
### Database
|
|
```bash
|
|
# Run migrations
|
|
make migrate
|
|
# OR
|
|
./bin/oslstats --migrate
|
|
```
|
|
|
|
### Configuration Management
|
|
```bash
|
|
# Generate .env template file
|
|
make genenv
|
|
# OR with custom output: make genenv OUT=.env.example
|
|
|
|
# Show environment variable documentation
|
|
make envdoc
|
|
|
|
# Show current environment values
|
|
make showenv
|
|
```
|
|
|
|
## Code Style Guidelines
|
|
|
|
### Import Organization
|
|
Organize imports in **3 groups** separated by blank lines:
|
|
|
|
```go
|
|
import (
|
|
// 1. Standard library
|
|
"context"
|
|
"net/http"
|
|
"fmt"
|
|
|
|
// 2. External dependencies
|
|
"git.haelnorr.com/h/golib/hws"
|
|
"github.com/pkg/errors"
|
|
"github.com/uptrace/bun"
|
|
|
|
// 3. Internal packages
|
|
"git.haelnorr.com/h/oslstats/internal/config"
|
|
"git.haelnorr.com/h/oslstats/pkg/oauth"
|
|
)
|
|
```
|
|
|
|
### Naming Conventions
|
|
|
|
**Variables**:
|
|
- Local: `camelCase` (userAgentKey, httpServer, dbConn)
|
|
- Exported: `PascalCase` (Config, User, Token)
|
|
- Common abbreviations: `cfg`, `ctx`, `tx`, `db`, `err`, `w`, `r`
|
|
|
|
**Functions**:
|
|
- Exported: `PascalCase` (GetConfig, NewStore, GenerateState)
|
|
- Private: `camelCase` (throwError, shouldShowDetails, loadModels)
|
|
- HTTP handlers: Return `http.Handler`, use dependency injection pattern
|
|
- Database functions: Use `bun.Tx` as parameter for transactions
|
|
|
|
**Types**:
|
|
- Structs/Interfaces: `PascalCase` (Config, User, OAuthSession)
|
|
- Use `-er` suffix for interfaces (implied from usage)
|
|
|
|
**Files**:
|
|
- Prefer single word: `config.go`, `oauth.go`, `errors.go`
|
|
- Don't use snake_case except for tests: `state_test.go`
|
|
- Test files: `*_test.go` alongside source files
|
|
|
|
### Error Handling
|
|
|
|
**Always wrap errors** with context using `github.com/pkg/errors`:
|
|
|
|
```go
|
|
if err != nil {
|
|
return errors.Wrap(err, "operation_name")
|
|
}
|
|
```
|
|
|
|
**Validate inputs at function start**:
|
|
```go
|
|
func DoSomething(cfg *Config, data string) error {
|
|
if cfg == nil {
|
|
return errors.New("cfg cannot be nil")
|
|
}
|
|
if data == "" {
|
|
return errors.New("data cannot be empty")
|
|
}
|
|
// ... rest of function
|
|
}
|
|
```
|
|
|
|
**HTTP error helpers** (in handlers package):
|
|
*Note: `msg` should be a frontend presentable message*
|
|
- `throwInternalServiceError(s, w, r, msg, err)` - 500 errors
|
|
- `throwBadRequest(s, w, r, msg, err)` - 400 errors
|
|
- `throwForbidden(s, w, r, msg, err)` - 403 errors (normal)
|
|
- `throwForbiddenSecurity(s, w, r, msg, err)` - 403 security violations (WARN level)
|
|
- `throwUnauthorized(s, w, r, msg, err)` - 401 errors (normal)
|
|
- `throwUnauthorizedSecurity(s, w, r, msg, err)` - 401 security violations (WARN level)
|
|
- `throwNotFound(s, w, r, path)` - 404 errors
|
|
|
|
### Common Patterns
|
|
|
|
**HTTP Handler Pattern**:
|
|
```go
|
|
func HandlerName(server *hws.Server, deps ...) http.Handler {
|
|
return http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
// Handler logic here
|
|
},
|
|
)
|
|
}
|
|
```
|
|
|
|
**Database Operation Pattern**:
|
|
```go
|
|
func GetSomething(ctx context.Context, tx bun.Tx, id int) (*Result, error) {
|
|
result := new(Result)
|
|
err := tx.NewSelect().
|
|
Model(result).
|
|
Where("id = ?", id).
|
|
Scan(ctx)
|
|
if err != nil {
|
|
if err.Error() == "sql: no rows in result set" {
|
|
return nil, nil // Return nil, nil for not found
|
|
}
|
|
return nil, errors.Wrap(err, "tx.Select")
|
|
}
|
|
return result, nil
|
|
}
|
|
```
|
|
|
|
**Setup Function Pattern** (returns instance, cleanup func, error):
|
|
```go
|
|
func setupSomething(ctx context.Context, cfg *Config) (*Type, func() error, error) {
|
|
instance := newInstance()
|
|
|
|
err := configure(instance)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "configure")
|
|
}
|
|
|
|
return instance, instance.Close, nil
|
|
}
|
|
```
|
|
|
|
**Configuration Pattern** (using ezconf):
|
|
```go
|
|
type Config struct {
|
|
Field string // ENV FIELD_NAME: Description (required/default: value)
|
|
}
|
|
|
|
func ConfigFromEnv() (any, error) {
|
|
cfg := &Config{
|
|
Field: env.String("FIELD_NAME", "default"),
|
|
}
|
|
// Validation here
|
|
return cfg, nil
|
|
}
|
|
```
|
|
|
|
### Formatting & Types
|
|
|
|
**Formatting**:
|
|
- Use `gofmt` (standard Go formatting)
|
|
|
|
**Types**:
|
|
- Prefer explicit types over inference when it improves clarity
|
|
- Use struct tags for ORM and JSON marshaling:
|
|
```go
|
|
type User struct {
|
|
bun.BaseModel `bun:"table:users,alias:u"`
|
|
ID int `bun:"id,pk,autoincrement"`
|
|
Username string `bun:"username,unique"`
|
|
AccessToken string `json:"access_token"`
|
|
}
|
|
```
|
|
|
|
**Comments**:
|
|
- Document exported functions and types
|
|
- Use inline comments for ENV var documentation in Config structs
|
|
- Explain security-critical code flows
|
|
|
|
### Testing
|
|
|
|
**Test File Location**: Place `*_test.go` files alongside source files
|
|
|
|
**Test Naming**:
|
|
```go
|
|
func TestFunctionName_Scenario(t *testing.T)
|
|
func TestGenerateState_Success(t *testing.T)
|
|
func TestVerifyState_WrongUserAgentKey(t *testing.T)
|
|
```
|
|
|
|
**Test Structure**:
|
|
- Use subtests with `t.Run()` for related scenarios
|
|
- Use table-driven tests for multiple similar cases
|
|
- Create helper functions for common setup (e.g., `testConfig()`)
|
|
- Test happy paths, error cases, edge cases, and security properties
|
|
|
|
**Test Categories** (from pkg/oauth/state_test.go example):
|
|
1. Happy path tests
|
|
2. Error handling (nil params, empty fields, malformed input)
|
|
3. Security tests (MITM, CSRF, replay attacks, tampering)
|
|
4. Edge cases (concurrency, constant-time comparison)
|
|
5. Integration tests (round-trip verification)
|
|
|
|
### Security
|
|
|
|
**Critical Practices**:
|
|
- Use `crypto/subtle.ConstantTimeCompare` for cryptographic comparisons
|
|
- Implement CSRF protection via state tokens
|
|
- Store sensitive cookies as HttpOnly
|
|
- Use separate logging levels for security violations (WARN)
|
|
- Validate all inputs at function boundaries
|
|
- Use parameterized queries (Bun ORM handles this)
|
|
- Never commit secrets (.env, keys/ are gitignored)
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
oslstats/
|
|
├── cmd/oslstats/ # Application entry point
|
|
│ ├── main.go # Entry point with flag parsing
|
|
│ ├── run.go # Server initialization & graceful shutdown
|
|
│ ├── httpserver.go # HTTP server setup
|
|
│ ├── routes.go # Route registration
|
|
│ ├── middleware.go # Middleware registration
|
|
│ ├── auth.go # Authentication setup
|
|
│ └── db.go # Database connection & migrations
|
|
├── internal/ # Private application code
|
|
│ ├── config/ # Configuration aggregation
|
|
│ ├── db/ # Database models & queries (Bun ORM)
|
|
│ ├── discord/ # Discord OAuth integration
|
|
│ ├── handlers/ # HTTP request handlers
|
|
│ ├── session/ # Session store (in-memory)
|
|
│ └── view/ # Templ templates
|
|
│ ├── component/ # Reusable UI components
|
|
│ ├── layout/ # Page layouts
|
|
│ └── page/ # Full pages
|
|
├── pkg/ # Reusable packages
|
|
│ ├── contexts/ # Context key definitions
|
|
│ ├── embedfs/ # Embedded static files
|
|
│ └── oauth/ # OAuth state management
|
|
├── bin/ # Compiled binaries (gitignored)
|
|
├── keys/ # Private keys (gitignored)
|
|
├── tmp/ # Air hot reload temp files (gitignored)
|
|
├── Makefile # Build automation
|
|
├── .air.toml # Hot reload configuration
|
|
└── go.mod # Go module definition
|
|
```
|
|
|
|
## Key Dependencies
|
|
|
|
- **git.haelnorr.com/h/golib/*** - Custom libraries (env, ezconf, hlog, hws, hwsauth, cookies, jwt)
|
|
- **github.com/a-h/templ** - Type-safe HTML templating
|
|
- **github.com/uptrace/bun** - PostgreSQL ORM
|
|
- **github.com/bwmarrin/discordgo** - Discord API client
|
|
- **github.com/pkg/errors** - Error wrapping (use this, not fmt.Errorf)
|
|
- **github.com/joho/godotenv** - .env file loading
|
|
|
|
## Notes for AI Agents
|
|
|
|
1. **Never commit** .env files, keys/, or generated files (*_templ.go, output.css)
|
|
2. **Database operations** should use `bun.Tx` for transaction safety
|
|
3. **Templates** are written in templ, not Go html/template - run `templ generate` after changes
|
|
4. **Static files** are embedded via `//go:embed` - check pkg/embedfs/
|
|
5. **Error messages** should be descriptive and use errors.Wrap for context
|
|
6. **Security is critical** - especially in OAuth flows (see pkg/oauth/state_test.go for examples)
|
|
7. **Air proxy** runs on port 3000 during development; app runs on 3333
|
|
8. **Test coverage** is currently limited - prioritize testing security-critical code
|
|
9. **Configuration** uses ezconf pattern - see internal/*/ezconf.go files for examples
|
|
10. **Graceful shutdown** is implemented in cmd/oslstats/run.go - follow this pattern
|
|
11. When in plan mode, always use the interactive question tool if available
|