go first commit
This commit is contained in:
14
go_cloud/.env.example
Normal file
14
go_cloud/.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
# Server Configuration
|
||||
SERVER_ADDR=:8080
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgres://user:password@localhost/dbname?sslmode=disable
|
||||
|
||||
# OIDC Configuration
|
||||
OIDC_ISSUER_URL=https://storage.b0esche.cloud/index.php/apps/oidc_login
|
||||
OIDC_REDIRECT_URL=http://localhost:8080/auth/callback
|
||||
OIDC_CLIENT_ID=your_client_id
|
||||
OIDC_CLIENT_SECRET=your_client_secret
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your_jwt_secret_key
|
||||
16
go_cloud/Makefile
Normal file
16
go_cloud/Makefile
Normal file
@@ -0,0 +1,16 @@
|
||||
.PHONY: build run test clean lint
|
||||
|
||||
build:
|
||||
go build -o bin/api ./cmd/api
|
||||
|
||||
run:
|
||||
go run ./cmd/api
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
clean:
|
||||
rm -rf bin/
|
||||
|
||||
lint:
|
||||
golangci-lint run
|
||||
113
go_cloud/README.md
Normal file
113
go_cloud/README.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Go Backend Control Plane
|
||||
|
||||
This is the Go backend for the enterprise document collaboration platform, serving as the single source of truth for authentication, organizations, permissions, sessions, and orchestration.
|
||||
|
||||
## Architecture
|
||||
|
||||
The backend is designed for security, scale, auditability, and SaaS readiness. It uses OIDC authentication via Nextcloud and enforces organization-scoped permissions.
|
||||
|
||||
### Core Principles
|
||||
- **Single Source of Truth**: All auth, orgs, permissions, and sessions are managed here.
|
||||
- **Untrusted Frontend**: The backend does not trust client-side permissions.
|
||||
- **Organization Scoping**: Every API request is org-scoped.
|
||||
- **Audit Logging**: All sensitive actions are logged.
|
||||
|
||||
### Key Components
|
||||
- **Authentication**: OIDC via Nextcloud
|
||||
- **Sessions**: Short-lived JWTs (5-15 minutes)
|
||||
- **Organizations**: Multi-org support with role-based permissions
|
||||
- **File Mediation**: Talks to Nextcloud via WebDAV/APIs, no direct client access
|
||||
- **Document Sessions**: Generates signed URLs for viewers and editors (Collabora)
|
||||
|
||||
### Tech Stack
|
||||
- **Language**: Go 1.22+
|
||||
- **Framework**: Chi router
|
||||
- **Database**: PostgreSQL 16
|
||||
- **JWT**: golang-jwt/jwt/v4
|
||||
- **Deployment**: Ready for containerization
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
.
|
||||
├── cmd/api/main.go # Application entry point
|
||||
├── internal/
|
||||
│ ├── auth/ # OIDC authentication logic
|
||||
│ ├── session/ # Session management
|
||||
│ ├── org/ # Organization resolution
|
||||
│ ├── permission/ # Permission checking
|
||||
│ ├── files/ # File access mediation
|
||||
│ ├── documents/ # Document session generation
|
||||
│ ├── collabora/ # Collabora editor integration
|
||||
│ ├── audit/ # Audit logging
|
||||
│ ├── middleware/ # HTTP middleware (auth, org, rate limit)
|
||||
│ ├── config/ # Configuration loading
|
||||
│ ├── database/ # DB connections and migrations
|
||||
│ └── http/ # HTTP server and routes
|
||||
├── pkg/jwt/ # JWT utilities
|
||||
├── migrations/ # Database migrations
|
||||
├── scripts/ # Utility scripts
|
||||
├── .env.example # Environment variables template
|
||||
├── Makefile # Build and deployment tasks
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Initialize**:
|
||||
```bash
|
||||
go mod init go.b0esche.cloud/backend
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
2. **Set up PostgreSQL**:
|
||||
- Install PostgreSQL 16
|
||||
- Create a database
|
||||
- Run migrations:
|
||||
```bash
|
||||
psql -d your_database < migrations/0001_initial.sql
|
||||
```
|
||||
|
||||
3. **Configure Environment**:
|
||||
Copy `.env.example` to `.env` and fill in values, especially database URL and OIDC settings.
|
||||
|
||||
4. **Run**:
|
||||
```bash
|
||||
go run cmd/api/main.go
|
||||
```
|
||||
|
||||
5. **Health Check**:
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /health` - Health check
|
||||
- `GET /auth/login` - Initiate OIDC login (redirects to Nextcloud)
|
||||
- `GET /auth/callback` - OIDC callback (returns JWT token)
|
||||
|
||||
## Development
|
||||
|
||||
- Use Go 1.22+
|
||||
- Follow Go conventions
|
||||
- Add tests for critical components
|
||||
- Use structured logging
|
||||
- No global mutable state
|
||||
- No business logic in handlers
|
||||
|
||||
## Security
|
||||
|
||||
- All handlers must go through middleware chain
|
||||
- JWTs are short-lived
|
||||
- Permissions resolved server-side only
|
||||
- Audit all sensitive actions
|
||||
- Rate limiting implemented
|
||||
- No direct DB access from handlers
|
||||
|
||||
## Roadmap
|
||||
|
||||
1. Complete OIDC implementation
|
||||
2. Add database integration
|
||||
3. Implement org and permission resolution
|
||||
4. Add file access APIs
|
||||
5. Integrate with Nextcloud and Collabora
|
||||
BIN
go_cloud/api
Executable file
BIN
go_cloud/api
Executable file
Binary file not shown.
BIN
go_cloud/bin/api
Executable file
BIN
go_cloud/bin/api
Executable file
Binary file not shown.
43
go_cloud/cmd/api/main.go
Normal file
43
go_cloud/cmd/api/main.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/audit"
|
||||
"go.b0esche.cloud/backend/internal/auth"
|
||||
"go.b0esche.cloud/backend/internal/config"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
httpsrv "go.b0esche.cloud/backend/internal/http"
|
||||
"go.b0esche.cloud/backend/pkg/jwt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
|
||||
dbConn, err := database.Connect(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Database connection error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
db := database.New(dbConn)
|
||||
|
||||
jwtManager := jwt.NewManager(cfg.JWTSecret)
|
||||
|
||||
authService, err := auth.NewService(cfg, db)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Auth service error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
auditLogger := audit.NewLogger(db)
|
||||
|
||||
srv := httpsrv.New(cfg, db, jwtManager, authService, auditLogger)
|
||||
|
||||
fmt.Printf("Starting server on %s\n", cfg.ServerAddr)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
23
go_cloud/go.mod
Normal file
23
go_cloud/go.mod
Normal file
@@ -0,0 +1,23 @@
|
||||
module go.b0esche.cloud/backend
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/oauth2 v0.28.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
)
|
||||
35
go_cloud/go.sum
Normal file
35
go_cloud/go.sum
Normal file
@@ -0,0 +1,35 @@
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
44
go_cloud/internal/audit/audit.go
Normal file
44
go_cloud/internal/audit/audit.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
func NewLogger(db *database.DB) *Logger {
|
||||
return &Logger{db: db}
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
UserID *uuid.UUID
|
||||
OrgID *uuid.UUID
|
||||
Action string
|
||||
Resource *string
|
||||
Success bool
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
func (l *Logger) Log(ctx context.Context, entry Entry) {
|
||||
metadataJSON, err := json.Marshal(entry.Metadata)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal audit metadata: %v", err)
|
||||
metadataJSON = []byte("{}")
|
||||
}
|
||||
|
||||
_, err = l.db.ExecContext(ctx, `
|
||||
INSERT INTO audit_logs (user_id, org_id, action, resource, success, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`, entry.UserID, entry.OrgID, entry.Action, entry.Resource, entry.Success, metadataJSON)
|
||||
if err != nil {
|
||||
log.Printf("Failed to log audit entry: %v", err)
|
||||
}
|
||||
}
|
||||
96
go_cloud/internal/auth/auth.go
Normal file
96
go_cloud/internal/auth/auth.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/config"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
provider *oidc.Provider
|
||||
oauth2Config oauth2.Config
|
||||
db *database.DB // Assume we have a DB wrapper
|
||||
}
|
||||
|
||||
func NewService(cfg *config.Config, db *database.DB) (*Service, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
provider, err := oidc.NewProvider(ctx, cfg.OIDCIssuerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get OIDC provider: %w", err)
|
||||
}
|
||||
|
||||
oauth2Config := oauth2.Config{
|
||||
ClientID: cfg.OIDCClientID,
|
||||
ClientSecret: cfg.OIDCClientSecret,
|
||||
RedirectURL: cfg.OIDCRedirectURL, // Add to config
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
}
|
||||
|
||||
return &Service{
|
||||
provider: provider,
|
||||
oauth2Config: oauth2Config,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) LoginURL(state string) string {
|
||||
return s.oauth2Config.AuthCodeURL(state)
|
||||
}
|
||||
|
||||
func (s *Service) HandleCallback(ctx context.Context, code, state string) (*database.User, *database.Session, error) {
|
||||
oauth2Token, err := s.oauth2Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to exchange code: %w", err)
|
||||
}
|
||||
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("no id_token in token response")
|
||||
}
|
||||
|
||||
idToken, err := s.provider.Verifier(&oidc.Config{ClientID: s.oauth2Config.ClientID}).Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to verify ID token: %w", err)
|
||||
}
|
||||
|
||||
var claims struct {
|
||||
Sub string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse claims: %w", err)
|
||||
}
|
||||
|
||||
user, err := s.db.GetOrCreateUser(ctx, claims.Sub, claims.Email, claims.Name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
session, err := s.db.CreateSession(ctx, user.ID, time.Now().Add(15*time.Minute))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return user, session, nil
|
||||
}
|
||||
|
||||
// GenerateState generates a secure random state string
|
||||
func GenerateState() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
29
go_cloud/internal/auth/auth_test.go
Normal file
29
go_cloud/internal/auth/auth_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateState(t *testing.T) {
|
||||
state1, err := GenerateState()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state2, err := GenerateState()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if state1 == state2 {
|
||||
t.Error("States should be unique")
|
||||
}
|
||||
if len(state1) == 0 {
|
||||
t.Error("State should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
// Mock db
|
||||
// service, err := NewService(cfg, db)
|
||||
// TODO: Mock database for full test
|
||||
t.Skip("Requires database mock")
|
||||
}
|
||||
34
go_cloud/internal/config/config.go
Normal file
34
go_cloud/internal/config/config.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ServerAddr string
|
||||
DatabaseURL string
|
||||
OIDCIssuerURL string
|
||||
OIDCRedirectURL string
|
||||
OIDCClientID string
|
||||
OIDCClientSecret string
|
||||
JWTSecret string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
ServerAddr: getEnv("SERVER_ADDR", ":8080"),
|
||||
DatabaseURL: os.Getenv("DATABASE_URL"),
|
||||
OIDCIssuerURL: os.Getenv("OIDC_ISSUER_URL"),
|
||||
OIDCRedirectURL: os.Getenv("OIDC_REDIRECT_URL"),
|
||||
OIDCClientID: os.Getenv("OIDC_CLIENT_ID"),
|
||||
OIDCClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
|
||||
JWTSecret: os.Getenv("JWT_SECRET"),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultVal string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
29
go_cloud/internal/database/database.go
Normal file
29
go_cloud/internal/database/database.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/config"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
func Connect(cfg *config.Config) (*sql.DB, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
pool, err := pgxpool.New(ctx, cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create pool: %w", err)
|
||||
}
|
||||
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
db := stdlib.OpenDBFromPool(pool)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
124
go_cloud/internal/database/db.go
Normal file
124
go_cloud/internal/database/db.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func New(db *sql.DB) *DB {
|
||||
return &DB{DB: db}
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uuid.UUID
|
||||
Email string
|
||||
DisplayName string
|
||||
CreatedAt time.Time
|
||||
LastLoginAt *time.Time
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
ID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
ExpiresAt time.Time
|
||||
RevokedAt *time.Time
|
||||
}
|
||||
|
||||
type Organization struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
Slug string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Membership struct {
|
||||
UserID uuid.UUID
|
||||
OrgID uuid.UUID
|
||||
Role string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (db *DB) GetOrCreateUser(ctx context.Context, sub, email, name string) (*User, error) {
|
||||
var user User
|
||||
err := db.QueryRowContext(ctx, `
|
||||
INSERT INTO users (id, email, display_name)
|
||||
VALUES (gen_random_uuid(), $1, $2)
|
||||
ON CONFLICT (email) DO UPDATE SET
|
||||
display_name = EXCLUDED.display_name,
|
||||
last_login_at = NOW()
|
||||
RETURNING id, email, display_name, created_at, last_login_at
|
||||
`, email, name).Scan(&user.ID, &user.Email, &user.DisplayName, &user.CreatedAt, &user.LastLoginAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (db *DB) CreateSession(ctx context.Context, userID uuid.UUID, expiresAt time.Time) (*Session, error) {
|
||||
var session Session
|
||||
err := db.QueryRowContext(ctx, `
|
||||
INSERT INTO sessions (user_id, expires_at)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, user_id, expires_at, revoked_at
|
||||
`, userID, expiresAt).Scan(&session.ID, &session.UserID, &session.ExpiresAt, &session.RevokedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetSession(ctx context.Context, sessionID uuid.UUID) (*Session, error) {
|
||||
var session Session
|
||||
err := db.QueryRowContext(ctx, `
|
||||
SELECT id, user_id, expires_at, revoked_at
|
||||
FROM sessions
|
||||
WHERE id = $1
|
||||
`, sessionID).Scan(&session.ID, &session.UserID, &session.ExpiresAt, &session.RevokedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetUserOrganizations(ctx context.Context, userID uuid.UUID) ([]Organization, error) {
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT o.id, o.name, o.slug, o.created_at
|
||||
FROM organizations o
|
||||
JOIN memberships m ON o.id = m.org_id
|
||||
WHERE m.user_id = $1
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var orgs []Organization
|
||||
for rows.Next() {
|
||||
var org Organization
|
||||
if err := rows.Scan(&org.ID, &org.Name, &org.Slug, &org.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orgs = append(orgs, org)
|
||||
}
|
||||
return orgs, rows.Err()
|
||||
}
|
||||
|
||||
func (db *DB) GetUserMembership(ctx context.Context, userID, orgID uuid.UUID) (*Membership, error) {
|
||||
var membership Membership
|
||||
err := db.QueryRowContext(ctx, `
|
||||
SELECT user_id, org_id, role, created_at
|
||||
FROM memberships
|
||||
WHERE user_id = $1 AND org_id = $2
|
||||
`, userID, orgID).Scan(&membership.UserID, &membership.OrgID, &membership.Role, &membership.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &membership, nil
|
||||
}
|
||||
93
go_cloud/internal/http/routes.go
Normal file
93
go_cloud/internal/http/routes.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/audit"
|
||||
"go.b0esche.cloud/backend/internal/auth"
|
||||
"go.b0esche.cloud/backend/internal/config"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
"go.b0esche.cloud/backend/internal/middleware"
|
||||
"go.b0esche.cloud/backend/pkg/jwt"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, authService *auth.Service, auditLogger *audit.Logger) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Global middleware
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.RateLimit)
|
||||
|
||||
// Health check
|
||||
r.Get("/health", healthHandler)
|
||||
|
||||
// Auth routes (no auth required)
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Get("/login", func(w http.ResponseWriter, req *http.Request) {
|
||||
authLoginHandler(w, req, authService)
|
||||
})
|
||||
r.Get("/callback", func(w http.ResponseWriter, req *http.Request) {
|
||||
authCallbackHandler(w, req, cfg, authService, jwtManager, auditLogger)
|
||||
})
|
||||
})
|
||||
|
||||
// Auth middleware for protected routes
|
||||
r.Use(middleware.Auth(jwtManager, db))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
func authLoginHandler(w http.ResponseWriter, r *http.Request, authService *auth.Service) {
|
||||
state, err := auth.GenerateState()
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Store state securely (e.g., in session or cache)
|
||||
|
||||
url := authService.LoginURL(state)
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
|
||||
func authCallbackHandler(w http.ResponseWriter, r *http.Request, cfg *config.Config, authService *auth.Service, jwtManager *jwt.Manager, auditLogger *audit.Logger) {
|
||||
code := r.URL.Query().Get("code")
|
||||
state := r.URL.Query().Get("state")
|
||||
|
||||
// TODO: Validate state
|
||||
|
||||
user, session, err := authService.HandleCallback(r.Context(), code, state)
|
||||
if err != nil {
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
Action: "login",
|
||||
Success: false,
|
||||
Metadata: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
http.Error(w, "Authentication failed", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwtManager.Generate(user.Email, []string{}, session.ID.String()) // Orgs not yet
|
||||
if err != nil {
|
||||
http.Error(w, "Token generation failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &user.ID,
|
||||
Action: "login",
|
||||
Success: true,
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"token": "` + token + `"}`))
|
||||
}
|
||||
26
go_cloud/internal/http/server.go
Normal file
26
go_cloud/internal/http/server.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/audit"
|
||||
"go.b0esche.cloud/backend/internal/auth"
|
||||
"go.b0esche.cloud/backend/internal/config"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
"go.b0esche.cloud/backend/pkg/jwt"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*http.Server
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, authService *auth.Service, auditLogger *audit.Logger) *Server {
|
||||
r := NewRouter(cfg, db, jwtManager, authService, auditLogger)
|
||||
|
||||
return &Server{
|
||||
Server: &http.Server{
|
||||
Addr: cfg.ServerAddr,
|
||||
Handler: r,
|
||||
},
|
||||
}
|
||||
}
|
||||
123
go_cloud/internal/middleware/middleware.go
Normal file
123
go_cloud/internal/middleware/middleware.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/audit"
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
"go.b0esche.cloud/backend/internal/org"
|
||||
"go.b0esche.cloud/backend/internal/permission"
|
||||
"go.b0esche.cloud/backend/pkg/jwt"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var RequestID = middleware.RequestID
|
||||
var Logger = middleware.Logger
|
||||
var Recoverer = middleware.Recoverer
|
||||
|
||||
// TODO: Implement rate limiter
|
||||
var RateLimit = func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Basic rate limiting logic here
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
userKey contextKey = "user"
|
||||
sessionKey contextKey = "session"
|
||||
orgKey contextKey = "org"
|
||||
)
|
||||
|
||||
// Auth middleware
|
||||
func Auth(jwtManager *jwt.Manager, db *database.DB) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
claims, session, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db)
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), userKey, claims.UserID)
|
||||
ctx = context.WithValue(ctx, sessionKey, session)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Org middleware
|
||||
func Org(db *database.DB, auditLogger *audit.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userIDStr := r.Context().Value(userKey).(string)
|
||||
userID, _ := uuid.Parse(userIDStr)
|
||||
|
||||
orgIDStr := r.Header.Get("X-Org-ID")
|
||||
if orgIDStr == "" {
|
||||
orgIDStr = chi.URLParam(r, "orgId")
|
||||
}
|
||||
orgID, err := uuid.Parse(orgIDStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid org ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = org.CheckMembership(r.Context(), db, userID, orgID)
|
||||
if err != nil {
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
Action: "org_access",
|
||||
Success: false,
|
||||
Metadata: map[string]interface{}{"org_id": orgID, "error": err.Error()},
|
||||
})
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), orgKey, orgID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Permission middleware
|
||||
func Permission(db *database.DB, auditLogger *audit.Logger, perm permission.Permission) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userIDStr := r.Context().Value(userKey).(string)
|
||||
userID, _ := uuid.Parse(userIDStr)
|
||||
orgID := r.Context().Value(orgKey).(uuid.UUID)
|
||||
|
||||
hasPerm, err := permission.HasPermission(r.Context(), db, userID, orgID, perm)
|
||||
if err != nil || !hasPerm {
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
OrgID: &orgID,
|
||||
Action: "permission_check",
|
||||
Resource: &[]string{string(perm)}[0],
|
||||
Success: false,
|
||||
Metadata: map[string]interface{}{"permission": perm},
|
||||
})
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
23
go_cloud/internal/org/org.go
Normal file
23
go_cloud/internal/org/org.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ResolveUserOrgs returns the organizations a user belongs to
|
||||
func ResolveUserOrgs(ctx context.Context, db *database.DB, userID uuid.UUID) ([]database.Organization, error) {
|
||||
return db.GetUserOrganizations(ctx, userID)
|
||||
}
|
||||
|
||||
// CheckMembership checks if user is member of org and returns role
|
||||
func CheckMembership(ctx context.Context, db *database.DB, userID, orgID uuid.UUID) (string, error) {
|
||||
membership, err := db.GetUserMembership(ctx, userID, orgID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return membership.Role, nil
|
||||
}
|
||||
48
go_cloud/internal/permission/permission.go
Normal file
48
go_cloud/internal/permission/permission.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package permission
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Permission string
|
||||
|
||||
const (
|
||||
FileRead Permission = "file.read"
|
||||
FileWrite Permission = "file.write"
|
||||
FileDelete Permission = "file.delete"
|
||||
DocumentView Permission = "document.view"
|
||||
DocumentEdit Permission = "document.edit"
|
||||
OrgManage Permission = "org.manage"
|
||||
)
|
||||
|
||||
var rolePermissions = map[string][]Permission{
|
||||
"owner": {FileRead, FileWrite, FileDelete, DocumentView, DocumentEdit, OrgManage},
|
||||
"admin": {FileRead, FileWrite, FileDelete, DocumentView, DocumentEdit},
|
||||
"editor": {FileRead, FileWrite, DocumentView, DocumentEdit},
|
||||
"viewer": {FileRead, DocumentView},
|
||||
}
|
||||
|
||||
// HasPermission checks if user has permission in org
|
||||
func HasPermission(ctx context.Context, db *database.DB, userID, orgID uuid.UUID, perm Permission) (bool, error) {
|
||||
membership, err := db.GetUserMembership(ctx, userID, orgID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
perms, ok := rolePermissions[membership.Role]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unknown role: %s", membership.Role)
|
||||
}
|
||||
|
||||
for _, p := range perms {
|
||||
if p == perm {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
42
go_cloud/migrations/0001_initial.sql
Normal file
42
go_cloud/migrations/0001_initial.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
display_name TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
last_login_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
CREATE TABLE organizations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE memberships (
|
||||
user_id UUID REFERENCES users(id),
|
||||
org_id UUID REFERENCES organizations(id),
|
||||
role TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
PRIMARY KEY (user_id, org_id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id),
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
revoked_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
CREATE TABLE audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id),
|
||||
org_id UUID REFERENCES organizations(id),
|
||||
action TEXT NOT NULL,
|
||||
resource TEXT,
|
||||
success BOOLEAN NOT NULL,
|
||||
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
metadata JSONB
|
||||
);
|
||||
5
go_cloud/migrations/0001_initial_down.sql
Normal file
5
go_cloud/migrations/0001_initial_down.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
DROP TABLE IF EXISTS audit_logs;
|
||||
DROP TABLE IF EXISTS sessions;
|
||||
DROP TABLE IF EXISTS memberships;
|
||||
DROP TABLE IF EXISTS organizations;
|
||||
DROP TABLE IF EXISTS users;
|
||||
84
go_cloud/pkg/jwt/jwt.go
Normal file
84
go_cloud/pkg/jwt/jwt.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"go.b0esche.cloud/backend/internal/database"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
OrgIDs []string `json:"org_ids"`
|
||||
SessionID string `json:"session_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
secret []byte
|
||||
}
|
||||
|
||||
func NewManager(secret string) *Manager {
|
||||
return &Manager{secret: []byte(secret)}
|
||||
}
|
||||
|
||||
func (m *Manager) Generate(userID string, orgIDs []string, sessionID string) (string, error) {
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
OrgIDs: orgIDs,
|
||||
SessionID: sessionID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(m.secret)
|
||||
}
|
||||
|
||||
func (m *Manager) Validate(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("unexpected signing method")
|
||||
}
|
||||
return m.secret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
func (m *Manager) ValidateWithSession(ctx context.Context, tokenString string, db *database.DB) (*Claims, *database.Session, error) {
|
||||
claims, err := m.Validate(tokenString)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sessionID, err := uuid.Parse(claims.SessionID)
|
||||
if err != nil {
|
||||
return nil, nil, errors.New("invalid session ID in token")
|
||||
}
|
||||
|
||||
session, err := db.GetSession(ctx, sessionID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if session.RevokedAt != nil || time.Now().After(session.ExpiresAt) {
|
||||
return nil, nil, errors.New("session invalid")
|
||||
}
|
||||
|
||||
return claims, session, nil
|
||||
}
|
||||
Reference in New Issue
Block a user