Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f0a6a5e
update .env.example
TheJolman Nov 23, 2025
f05f0c3
add ENV var
TheJolman Nov 23, 2025
ee1ecdb
init stuff
TheJolman Nov 23, 2025
40c93cb
wip
TheJolman Nov 24, 2025
f8d8950
move logic to routes package
TheJolman Nov 24, 2025
81896fe
wip
TheJolman Nov 25, 2025
cb3aeeb
fleshed out at this point
TheJolman Nov 25, 2025
80229d6
update go version
TheJolman Nov 25, 2025
1284190
go get ./...
TheJolman Nov 25, 2025
27f9095
go mod tidy
TheJolman Nov 25, 2025
e72c7f9
rm problematic `layout go` line in .envrc
TheJolman Nov 25, 2025
fd536fb
fix variable shadowing bug
TheJolman Dec 3, 2025
a857243
add missing return
TheJolman Dec 3, 2025
9ab90a0
scaffolding for wrapper func
TheJolman Nov 26, 2025
633f226
construct auth token
TheJolman Nov 26, 2025
afb9714
flesh out func
TheJolman Nov 26, 2025
93ef12f
fix shit up
TheJolman Nov 26, 2025
87cbfdb
nix flake update
TheJolman Nov 26, 2025
e779353
use correct method name
TheJolman Nov 26, 2025
3390164
fix odd error
TheJolman Nov 26, 2025
cdccc71
fix staticcheck warning
TheJolman Nov 26, 2025
8bba91a
works in dev mode
TheJolman Nov 26, 2025
b921679
simplify events get
TheJolman Nov 26, 2025
684e532
use colors in json output
TheJolman Nov 26, 2025
0d77960
uncomment clientID
TheJolman Nov 26, 2025
56c41d2
fix url encoding
TheJolman Nov 29, 2025
b28ba7b
add GIN_MODE option to .env.example
TheJolman Nov 29, 2025
6c6ed9a
add docs for env var configuration
TheJolman Nov 29, 2025
4a1c36d
hardcoded test roles
TheJolman Dec 5, 2025
9b123a4
add note
TheJolman Dec 5, 2025
21d90f7
add persistence helper functions
TheJolman Dec 5, 2025
0fed4ff
add logic to save and load token
TheJolman Dec 5, 2025
b2f8a49
use more obscure port for redirect uri
TheJolman Dec 5, 2025
9e30b8a
add documentation!!
TheJolman Dec 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
ENV="development"
DATABASE_URL="file:dev.db?cache=shared&mode=rwc"
PORT=""
TRUSTED_PROXIES=""
ALLOWED_ORIGINS=""
DISCORD_BOT_TOKEN=""
GUILD_ID=""
DISCORD_CLIENT_ID=""
DISCORD_CLIENT_SECRET=""
GIN_MODE="debug"
1 change: 0 additions & 1 deletion .envrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@ if has nix; then
fi
fi
use flake
layout go
PATH_add bin
dotenv ./.env
38 changes: 38 additions & 0 deletions developer-docs/env-vars.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Configuration via Environment Variables

See [config.go](../internal/api/config/config.go) for more information/default
values.

- `GIN_MODE`: One of `debug`, `release`, or `test`. This affects what and how
many HTTP middleware logs appear. Separate from manual logging.
- Default: `debug` (change in production)
- `ENV`: One of `production` or `development`. While in development mode,
authentication with Discord OAuth2 is bypassed. You may need to change this value to
`production` if testing Auth.
- Default: `development` (change in production)
- `PORT`: Port to run on.
- Default: `8080`
- `DATABASE_URL`: The path to the SQLite database file. Takes the format
`file:path/to/database.db`. Can further configure using query params. Example:
`mode=rwc` means read-write and create if doesn't exist. `cache=shared` is more
memory efficient than the default of `cache=private`.
- Default: `file:dev.db?cache=shared&mode=rwc`
- `TRUSTED_PROXIES`: Used in [server.go](../internal/api/server.go) in Gin's
`SetTrustedProxies` setting. Security mechanism to prevent IP spoofing when the
API sits behind a reverse proxy in production.
- Default: `127.0.0.1/32` (change in production)
- `ALLOWED_ORIGINS`: To be used with CORS middleware. Controls which web origins
are allowed to make cross-origin requests to the API.
- Default: `*` (change in production)
- `DISCORD_BOT_TOKEN`: Discord bot token used by the API server to validate user
tokens and check server membership/roles. Required in production.
- Default: (none, required in production)
- `GUILD_ID`: The Discord server/guild ID where user membership and roles are
checked.
- Default: (none, required in production)
- `DISCORD_CLIENT_ID`: OAuth2 application client ID used by the CLI for Discord
authentication.
- Default: (none, required for CLI OAuth)
- `DISCORD_CLIENT_SECRET`: OAuth2 application client secret used by the CLI for
token exchange.
- Default: (none, required for CLI OAuth)
188 changes: 188 additions & 0 deletions developer-docs/oauth-authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# OAuth Authentication

The ACM CSUF API uses Discord OAuth2 for authentication and authorization. This ensures that only authorized Discord server members with appropriate roles can access protected API endpoints.

## Overview

The API implements a role-based access control (RBAC) system that:
1. Authenticates users via Discord OAuth2
2. Verifies Discord server membership
3. Checks user roles against required permissions
4. Caches role information to avoid hitting Discord's rate limits

## Architecture

### Server-Side: API Middleware

The API uses Discord OAuth2 middleware defined in [`internal/api/middleware/oauth.go`](../internal/api/middleware/oauth.go) to protect all `/v1` routes.

**How it works:**

1. **Authorization Header**: The middleware expects requests to include:
```
Authorization: Bearer <discord_access_token>
```

2. **Token Validation**: The middleware validates the token by:
- Making a request to Discord's API to fetch user information
- Verifying the user is a member of the configured Discord guild/server
- Checking if the user has the required role

3. **Role-Based Access Control**:
- Roles are mapped in `RoleMap` (e.g., Discord role ID `1445971950584205554` → `"Board"`)
- The middleware checks if the user has the required role for the endpoint
- Role hierarchy: `President` role also grants `Board` access

4. **Caching**:
- Role information is cached for 5 minutes to prevent rate limit issues
- Cache key is the Authorization header value
- Expired cache entries are automatically removed

5. **Development Mode**:
- When `ENV=development`, the special token `Bearer dev-token` bypasses authentication
- This allows local testing without setting up OAuth

### Client-Side: CLI OAuth Flow

The CLI client (defined in [`utils/requests/request_with_auth.go`](../utils/requests/request_with_auth.go)) implements the OAuth2 authorization code flow with a local callback server.

**How it works:**

1. **Token Persistence**:
- Tokens are stored in `~/.config/acmcsuf-cli/token.json` on Unix systems
- The file contains: `access_token`, `refresh_token`, and `expiry` timestamp
- Tokens are automatically loaded on subsequent CLI runs

2. **OAuth Flow** (when no valid token exists):
```
┌─────────┐ ┌─────────────┐
│ CLI │ │ Discord │
└────┬────┘ └──────┬──────┘
│ │
│ 1. Start local callback server │
│ on random port (e.g., :61234) │
│ │
│ 2. Open browser with OAuth URL │
├──────────────────────────────────────────> │
│ https://discord.com/oauth2/authorize │
│ ?client_id=... │
│ &redirect_uri=http://localhost:54321 │
│ &scope=identify │
│ &response_type=code │
│ │
│ User approves in browser │
│ │
│ 3. Discord redirects to callback │
│ <────────────────────────────────────────── │
│ http://localhost:54321/?code=... │
│ │
│ 4. Exchange code for token │
├──────────────────────────────────────────> │
│ POST /oauth2/token │
│ │
│ 5. Receive access token │
│ <────────────────────────────────────────── │
│ { access_token, refresh_token, ... } │
│ │
│ 6. Save token to ~/.config/acmcsuf-cli/ │
│ │
│ 7. Make authenticated API request │
│ Authorization: Bearer <access_token> │
└──────────────────────────────────────────────┘
```

4. **Token Exchange**:
- The callback server receives the authorization code from Discord
- Exchanges the code for an access token via `POST https://discord.com/api/oauth2/token`
- Stores the token with expiry information for future use

## Environment Variables

See [`developer-docs/env-vars.md`](./env-vars.md) for the complete list, but OAuth-specific variables include:

- `ENV`: Set to `production` to enable OAuth (default: `development`)
- `DISCORD_BOT_TOKEN`: Bot token for server-side API authentication
- `GUILD_ID`: Discord server/guild ID to verify membership
- `DISCORD_CLIENT_ID`: OAuth2 application client ID (for CLI)
- `DISCORD_CLIENT_SECRET`: OAuth2 application client secret (for CLI)

## Development Mode

During development (`ENV=development`), authentication is bypassed:

**API Server:**
```go
// Any request with this header will bypass OAuth
Authorization: Bearer dev-token
```

**CLI Client:**
```go
// Automatically uses dev-token when ENV=development
// No OAuth flow occurs, no tokens are exchanged
```

To test the actual OAuth flow during development, temporarily set `ENV=production` in your `.env` file.

## Testing with OAuth

### Using the CLI

The CLI handles OAuth automatically:

```bash
# First run will trigger OAuth flow
./api.acmcsuf.com events get event-id

# Browser opens for authentication
# Token is saved to ~/.config/acmcsuf-cli/token.json
# Subsequent runs use the cached token
```

### Using curl/xh with OAuth

If testing manually with `curl` or `xh`, you need a valid Discord access token:

```bash
# Development mode (no real auth needed)
xh :8080/v1/events Authorization:"Bearer dev-token"

# Production mode (need real Discord token)
xh :8080/v1/events Authorization:"Bearer <discord_access_token>"
```

To get a real Discord access token for testing:
1. Run the CLI once to complete the OAuth flow
2. Extract the token from `~/.config/acmcsuf-cli/token.json`
3. Use that token in your curl/xh commands

### Token Expiry

Discord access tokens expire after a period (typically 1 week). When a token expires:

**CLI**: The OAuth flow automatically re-runs on the next command
**Manual testing**: You'll receive a `401 Unauthorized` response and need a new token

## Security Considerations

1. **Never commit tokens**: Token files (`~/.config/acmcsuf-cli/token.json`) contain sensitive credentials
2. **HTTPS in production**: The API should only run behind HTTPS in production to protect tokens in transit
3. **Client secrets**: Keep `DISCORD_CLIENT_SECRET` secure and never commit to git
4. **Rate limiting**: The middleware caches role information for 5 minutes to respect Discord's rate limits
5. **Token storage**: CLI tokens are stored with `0600` permissions (read/write for owner only)

## Role Configuration

To add or modify roles, edit the `RoleMap` in [`internal/api/middleware/oauth.go`](../internal/api/middleware/oauth.go):

```go
var RoleMap = map[string]string{
"1445971950584205554": "Board", // Discord role ID -> Role name
"another-role-id": "Developer",
}
```

To find Discord role IDs:
1. Enable Developer Mode in Discord settings
2. Right-click a role in Server Settings → Roles
3. Click "Copy ID"
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 47 additions & 34 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,60 +1,73 @@
module github.com/acmcsufoss/api.acmcsuf.com

go 1.23.0

toolchain go1.23.4
go 1.24.0

require (
github.com/gin-gonic/gin v1.10.0
github.com/spf13/cobra v1.9.1
github.com/bwmarrin/discordgo v0.29.0
github.com/cli/browser v1.3.0
github.com/gin-gonic/gin v1.11.0
github.com/spf13/cobra v1.10.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
modernc.org/sqlite v1.36.0
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
github.com/tidwall/pretty v1.2.1
modernc.org/sqlite v1.40.1
)

require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/jsonpointer v0.22.3 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/spec v0.22.1 // indirect
github.com/go-openapi/swag/conv v0.25.3 // indirect
github.com/go-openapi/swag/jsonname v0.25.3 // indirect
github.com/go-openapi/swag/jsonutils v0.25.3 // indirect
github.com/go-openapi/swag/loading v0.25.3 // indirect
github.com/go-openapi/swag/stringutils v0.25.3 // indirect
github.com/go-openapi/swag/typeutils v0.25.3 // indirect
github.com/go-openapi/swag/yamlutils v0.25.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.61.13 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
modernc.org/libc v1.67.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
modernc.org/memory v1.11.0 // indirect
)
Loading