Fix auth for 1.0.0: add logout endpoint, fix JWT claims consistency, add session revocation
This commit is contained in:
@@ -132,6 +132,15 @@ func (db *DB) GetSession(ctx context.Context, sessionID uuid.UUID) (*Session, er
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
func (db *DB) RevokeSession(ctx context.Context, sessionID uuid.UUID) error {
|
||||
_, err := db.ExecContext(ctx, `
|
||||
UPDATE sessions
|
||||
SET revoked_at = NOW()
|
||||
WHERE id = $1 AND revoked_at IS NULL
|
||||
`, sessionID)
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -48,6 +48,9 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
|
||||
r.Post("/refresh", func(w http.ResponseWriter, req *http.Request) {
|
||||
refreshHandler(w, req, jwtManager, db)
|
||||
})
|
||||
r.Post("/logout", func(w http.ResponseWriter, req *http.Request) {
|
||||
logoutHandler(w, req, jwtManager, db, auditLogger)
|
||||
})
|
||||
// Passkey routes
|
||||
r.Post("/signup", func(w http.ResponseWriter, req *http.Request) {
|
||||
signupHandler(w, req, db, auditLogger)
|
||||
@@ -200,6 +203,41 @@ func refreshHandler(w http.ResponseWriter, r *http.Request, jwtManager *jwt.Mana
|
||||
w.Write([]byte(`{"token": "` + newToken + `"}`))
|
||||
}
|
||||
|
||||
func logoutHandler(w http.ResponseWriter, r *http.Request, jwtManager *jwt.Manager, db *database.DB, auditLogger *audit.Logger) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
claims, session, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db)
|
||||
if err != nil {
|
||||
// Token invalid or session already revoked/expired — still return success
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status": "ok"}`))
|
||||
return
|
||||
}
|
||||
|
||||
userID, _ := uuid.Parse(claims.UserID)
|
||||
|
||||
// Revoke session
|
||||
if err := db.RevokeSession(r.Context(), session.ID); err != nil {
|
||||
errors.LogError(r, err, "Failed to revoke session")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditLogger.Log(r.Context(), audit.Entry{
|
||||
UserID: &userID,
|
||||
Action: "logout",
|
||||
Success: true,
|
||||
})
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status": "ok"}`))
|
||||
}
|
||||
|
||||
func listOrgsHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
@@ -616,7 +654,7 @@ func registrationVerifyHandler(w http.ResponseWriter, r *http.Request, db *datab
|
||||
|
||||
// Generate JWT
|
||||
orgIDs := []string{}
|
||||
token, err := jwtManager.Generate(user.Email, orgIDs, session.ID.String())
|
||||
token, err := jwtManager.Generate(user.ID.String(), orgIDs, session.ID.String())
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Token generation failed")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
@@ -718,7 +756,7 @@ func authenticationVerifyHandler(w http.ResponseWriter, r *http.Request, db *dat
|
||||
}
|
||||
|
||||
// Generate JWT
|
||||
token, err := jwtManager.Generate(user.Email, orgIDs, session.ID.String())
|
||||
token, err := jwtManager.Generate(user.ID.String(), orgIDs, session.ID.String())
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Token generation failed")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
@@ -788,7 +826,7 @@ func passwordLoginHandler(w http.ResponseWriter, r *http.Request, db *database.D
|
||||
}
|
||||
|
||||
// Generate JWT
|
||||
token, err := jwtManager.Generate(user.Email, orgIDs, session.ID.String())
|
||||
token, err := jwtManager.Generate(user.ID.String(), orgIDs, session.ID.String())
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Token generation failed")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
|
||||
46
go_cloud/migrations/run-migrations.sh
Normal file
46
go_cloud/migrations/run-migrations.sh
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# Database Migration Runner for b0esche.cloud
|
||||
# Runs all SQL migrations in order
|
||||
|
||||
set -e
|
||||
|
||||
# Check for required environment variable
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "ERROR: DATABASE_URL environment variable not set"
|
||||
echo "Example: DATABASE_URL=postgres://user:pass@localhost:5432/dbname"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "=== b0esche.cloud Database Migrations ==="
|
||||
echo "Database: $DATABASE_URL"
|
||||
echo
|
||||
|
||||
# Function to run a single migration
|
||||
run_migration() {
|
||||
local file=$1
|
||||
echo "Running: $(basename $file)"
|
||||
psql "$DATABASE_URL" -f "$file" -v ON_ERROR_STOP=1
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✓ Success"
|
||||
else
|
||||
echo "✗ Failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run migrations in order
|
||||
echo "Step 1/3: Initial schema..."
|
||||
run_migration "$SCRIPT_DIR/0001_initial.sql"
|
||||
|
||||
echo
|
||||
echo "Step 2/3: Passkeys and authentication..."
|
||||
run_migration "$SCRIPT_DIR/0002_passkeys.sql"
|
||||
|
||||
echo
|
||||
echo "Step 3/3: Files and storage..."
|
||||
run_migration "$SCRIPT_DIR/0003_files.sql"
|
||||
|
||||
echo
|
||||
echo "=== All migrations completed successfully! ==="
|
||||
Reference in New Issue
Block a user