package api import ( "encoding/json" "fmt" "net/http" "strconv" "time" "archivmail/internal/audit" "archivmail/internal/auth" ) // apiKeyCreateRequest is the JSON body for POST /api/admin/apikeys. type apiKeyCreateRequest struct { Name string `json:"name"` Role string `json:"role"` RateLimit int `json:"rate_limit"` } // handleCreateAPIKey generates a new API key for the current tenant. // POST /api/admin/apikeys func (s *Server) handleCreateAPIKey(w http.ResponseWriter, r *http.Request) { sess := sessionFromCtx(r.Context()) tenantID := tenantFromCtx(r.Context()) var req apiKeyCreateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Name == "" { writeError(w, http.StatusBadRequest, "name is required") return } // Validate role. if req.Role == "" { req.Role = "user" } if req.Role != "user" && req.Role != "auditor" { writeError(w, http.StatusBadRequest, "role must be 'user' or 'auditor'") return } if req.RateLimit <= 0 { req.RateLimit = 60 } if req.RateLimit > 1000 { req.RateLimit = 1000 } // Generate key. rawToken, tokenHash, err := auth.GenerateAPIKey() if err != nil { writeError(w, http.StatusInternalServerError, "failed to generate API key") return } // Determine tenant_id. var tid int64 if tenantID != nil { tid = *tenantID } if tid == 0 { writeError(w, http.StatusBadRequest, "API keys require a tenant context") return } // Insert into DB. var keyID int64 row := s.store.DBQueryRow(r.Context(), `INSERT INTO api_keys (tenant_id, name, token_hash, role, rate_limit) VALUES ($1, $2, $3, $4, $5) RETURNING id`, tid, req.Name, tokenHash, req.Role, req.RateLimit, ) if err := row.Scan(&keyID); err != nil { s.logger.Error("create api key failed", "err", err) writeError(w, http.StatusInternalServerError, "failed to create API key") return } // Audit log. s.audlog.Log(audit.Entry{ EventType: audit.EventUserMgmt, Username: sess.Username, Detail: fmt.Sprintf("created api key %q (id=%d, role=%s)", req.Name, keyID, req.Role), Success: true, }) // Return the raw token ONCE. writeJSON(w, http.StatusCreated, map[string]interface{}{ "id": keyID, "name": req.Name, "role": req.Role, "rate_limit": req.RateLimit, "token": rawToken, "message": "Save this token now. It will not be shown again.", }) } // handleListAPIKeys lists API keys for the current tenant. // GET /api/admin/apikeys func (s *Server) handleListAPIKeys(w http.ResponseWriter, r *http.Request) { tenantID := tenantFromCtx(r.Context()) var tid int64 if tenantID != nil { tid = *tenantID } rows, err := s.store.DBQuery(r.Context(), `SELECT id, name, role, active, rate_limit, created_at, last_used_at FROM api_keys WHERE tenant_id = $1 ORDER BY created_at DESC`, tid, ) if err != nil { writeError(w, http.StatusInternalServerError, "failed to list API keys") return } defer rows.Close() type apiKeyResponse struct { ID int64 `json:"id"` Name string `json:"name"` Role string `json:"role"` Active bool `json:"active"` RateLimit int `json:"rate_limit"` CreatedAt string `json:"created_at"` LastUsedAt *string `json:"last_used_at"` } keys := make([]apiKeyResponse, 0) for rows.Next() { var k apiKeyResponse var createdAt time.Time var lastUsedAt *time.Time if err := rows.Scan(&k.ID, &k.Name, &k.Role, &k.Active, &k.RateLimit, &createdAt, &lastUsedAt); err != nil { continue } k.CreatedAt = createdAt.UTC().Format(time.RFC3339) if lastUsedAt != nil { s := lastUsedAt.UTC().Format(time.RFC3339) k.LastUsedAt = &s } keys = append(keys, k) } writeJSON(w, http.StatusOK, map[string]interface{}{ "api_keys": keys, }) } // handleDeleteAPIKey deletes an API key belonging to the current tenant. // DELETE /api/admin/apikeys/{id} func (s *Server) handleDeleteAPIKey(w http.ResponseWriter, r *http.Request) { sess := sessionFromCtx(r.Context()) tenantID := tenantFromCtx(r.Context()) idStr := r.PathValue("id") keyID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid key id") return } var tid int64 if tenantID != nil { tid = *tenantID } // Delete only if it belongs to this tenant. tag, err := s.store.DBExec(r.Context(), `DELETE FROM api_keys WHERE id = $1 AND tenant_id = $2`, keyID, tid, ) if err != nil { writeError(w, http.StatusInternalServerError, "failed to delete API key") return } if tag == 0 { writeError(w, http.StatusNotFound, "API key not found") return } // Audit log. s.audlog.Log(audit.Entry{ EventType: audit.EventUserMgmt, Username: sess.Username, Detail: fmt.Sprintf("deleted api key id=%d", keyID), Success: true, }) writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) }