initial commit on Federted servers
release gate / check (push) Failing after 8m35s

This commit is contained in:
Andrew S Erwin
2026-10-19 16:23:55 -05:00
commit b9721ff13f
217 changed files with 31534 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
package authguard
import (
"encoding/json"
"github.com/andrew/go-computer/internal/auth"
"github.com/andrew/go-computer/internal/config"
"github.com/andrew/go-computer/internal/db"
"github.com/andrew/go-computer/internal/handlers"
"log"
"net/http"
"time"
)
func HandleAdminListUsers(w http.ResponseWriter, r *http.Request) {
// COALESCE(email,'') (review COR-8): NOT LIKE evaluates to NULL for the
// NULL emails OIDC-sourced accounts can carry, which hid them from the
// admin list entirely.
v, e := db.Many(r.Context(), db.FromContext(r.Context()), `SELECT jsonb_build_object('id',u.id,'name',u.name,'email',u.email,'image',u.image,'role',u.role,'createdAt',u."createdAt",'_count',jsonb_build_object('bookmarks',(SELECT count(*) FROM "Bookmark" b WHERE b."ownerId"=u.id),'pages',(SELECT count(*) FROM "Page" p WHERE p."ownerId"=u.id),'tags',(SELECT count(*) FROM "Tag" t WHERE t."ownerId"=u.id))) FROM "User" u WHERE COALESCE(email,'') NOT LIKE '%@example.com' ORDER BY "createdAt"`)
if e != nil {
handlers.WriteJSON(w, 500, handlers.Error{Error: "Database unavailable"})
return
}
handlers.WriteJSON(w, 200, v)
}
func HandleAdminPatchUser(w http.ResponseWriter, r *http.Request) {
var b struct {
UserID string `json:"userId"`
Role string `json:"role"`
}
if json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&b) != nil || (b.Role != "USER" && b.Role != "ADMIN") {
handlers.WriteJSON(w, 400, handlers.Error{Error: "Invalid role"})
return
}
s, _ := auth.SessionFromContext(r.Context())
if b.UserID == s.RealID && b.Role != "ADMIN" {
handlers.WriteJSON(w, 400, handlers.Error{Error: "You cannot demote yourself"})
return
}
d := db.FromContext(r.Context())
repo := db.NewUserRepo(d)
target, e := repo.FindByID(r.Context(), b.UserID)
if e != nil {
handlers.WriteJSON(w, 404, handlers.Error{Error: "Not found"})
return
}
// Review SEC-11: an installation must keep at least one administrator.
// Self-demotion is already refused; this closes the "demote the last
// other admin" path.
if b.Role == "USER" && target.Role == "ADMIN" {
var admins int
if e := d.QueryRowContext(r.Context(), `SELECT count(*) FROM "User" WHERE role='ADMIN' AND COALESCE(email,'') NOT LIKE '%@example.com'`).Scan(&admins); e != nil {
handlers.WriteJSON(w, 500, handlers.Error{Error: "Unable to change role"})
return
}
if admins <= 1 {
handlers.WriteJSON(w, 400, handlers.Error{Error: "At least one administrator must remain"})
return
}
}
if e := repo.SetRole(r.Context(), b.UserID, b.Role); e != nil {
handlers.WriteJSON(w, 500, handlers.Error{Error: "Unable to change role"})
return
}
// Review SEC-11: role changes are audited. Ids and roles only.
log.Printf("audit: admin %s changed role of %s: %s -> %s", s.RealID, b.UserID, target.Role, b.Role)
handlers.WriteJSON(w, 200, handlers.OK{OK: true})
}
func HandleAdminViewAs(w http.ResponseWriter, r *http.Request) {
var b struct {
UserID *string `json:"userId"`
}
if json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&b) != nil {
handlers.WriteJSON(w, 400, handlers.Error{Error: "Invalid JSON"})
return
}
s, _ := auth.SessionFromContext(r.Context())
c := http.Cookie{Name: auth.ViewAsCookie, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: config.Load().CookieSecure(), MaxAge: -1}
if b.UserID != nil && *b.UserID != "" && *b.UserID != s.RealID {
if _, e := db.NewUserRepo(db.FromContext(r.Context())).FindByID(r.Context(), *b.UserID); e != nil {
handlers.WriteJSON(w, 404, handlers.Error{Error: "Not found"})
return
}
c.Value = *b.UserID
c.MaxAge = int((8 * time.Hour).Seconds())
}
http.SetCookie(w, &c)
handlers.WriteJSON(w, 200, handlers.OK{OK: true})
}