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
|
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) {
|
func (db *DB) GetUserOrganizations(ctx context.Context, userID uuid.UUID) ([]Organization, error) {
|
||||||
rows, err := db.QueryContext(ctx, `
|
rows, err := db.QueryContext(ctx, `
|
||||||
SELECT o.id, o.name, o.slug, o.created_at
|
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) {
|
r.Post("/refresh", func(w http.ResponseWriter, req *http.Request) {
|
||||||
refreshHandler(w, req, jwtManager, db)
|
refreshHandler(w, req, jwtManager, db)
|
||||||
})
|
})
|
||||||
|
r.Post("/logout", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
logoutHandler(w, req, jwtManager, db, auditLogger)
|
||||||
|
})
|
||||||
// Passkey routes
|
// Passkey routes
|
||||||
r.Post("/signup", func(w http.ResponseWriter, req *http.Request) {
|
r.Post("/signup", func(w http.ResponseWriter, req *http.Request) {
|
||||||
signupHandler(w, req, db, auditLogger)
|
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 + `"}`))
|
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) {
|
func listOrgsHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager) {
|
||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
@@ -616,7 +654,7 @@ func registrationVerifyHandler(w http.ResponseWriter, r *http.Request, db *datab
|
|||||||
|
|
||||||
// Generate JWT
|
// Generate JWT
|
||||||
orgIDs := []string{}
|
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 {
|
if err != nil {
|
||||||
errors.LogError(r, err, "Token generation failed")
|
errors.LogError(r, err, "Token generation failed")
|
||||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
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
|
// 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 {
|
if err != nil {
|
||||||
errors.LogError(r, err, "Token generation failed")
|
errors.LogError(r, err, "Token generation failed")
|
||||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
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
|
// 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 {
|
if err != nil {
|
||||||
errors.LogError(r, err, "Token generation failed")
|
errors.LogError(r, err, "Token generation failed")
|
||||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
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