package app import ( "encoding/json" "golang.org/x/sys/unix" "net/http" ) func (a *App) requireAdmin(w http.ResponseWriter, r *http.Request) bool { if !a.isAdmin(r) { fail(w, 401, "请先登录管理员") return false } return true } func (a *App) adminLogin(w http.ResponseWriter, r *http.Request) { if !a.allow(r, "login", 5) { fail(w, 429, "登录尝试过于频繁") return } var in struct { Username string `json:"username"` Password string `json:"password"` } if !decode(w, r, &in) { return } select { case a.passwordSlots <- struct{}{}: default: fail(w, 429, "登录繁忙") return } defer func() { <-a.passwordSlots }() var username, hash string e := a.db.QueryRow("SELECT username,password_hash FROM admins WHERE id=1").Scan(&username, &hash) valid := e == nil && verifyPassword(in.Password, hash) && in.Username == username if !valid { a.audit("admin.login_failed", "", a.remoteIP(r)) fail(w, 401, "账号或密码错误") return } a.mu.Lock() defer a.mu.Unlock() if _, e = a.db.Exec("DELETE FROM admin_sessions WHERE expires<=? OR token_hash IN(SELECT token_hash FROM admin_sessions ORDER BY expires DESC LIMIT -1 OFFSET 49)", a.now().Unix()); e != nil { a.serverError(w, e) return } token := randomToken() _, e = a.db.Exec("INSERT INTO admin_sessions VALUES(?,?)", tokenHash(token), a.now().Unix()+8*3600) if e != nil { a.serverError(w, e) return } a.cookie(w, r, "bb_admin", token, "/api/v1/admin", 8*3600) a.audit("admin.login", "", a.remoteIP(r)) respond(w, 200, map[string]bool{"authenticated": true}) } func (a *App) adminLogout(w http.ResponseWriter, r *http.Request) { _, e := a.db.Exec("DELETE FROM admin_sessions WHERE token_hash=?", tokenHash(cookieValue(r, "bb_admin"))) if e != nil { a.serverError(w, e) return } a.cookie(w, r, "bb_admin", "", "/api/v1/admin", -1) respond(w, 200, map[string]bool{"authenticated": false}) } func (a *App) adminStatus(w http.ResponseWriter, r *http.Request) { respond(w, 200, map[string]bool{"authenticated": a.isAdmin(r)}) } func (a *App) adminStats(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } a.mu.Lock() defer a.mu.Unlock() used, reserved, meta, e := a.capacity() if e != nil { a.serverError(w, e) return } var raw int64 var files, objects, parcels int queries := []struct { sql string dst any }{{"SELECT COALESCE(sum(size),0) FROM files WHERE object_id IS NOT NULL", &raw}, {"SELECT count(*) FROM files", &files}, {"SELECT count(*) FROM objects", &objects}, {"SELECT count(*) FROM parcels WHERE state='ready'", &parcels}} for _, q := range queries { if e = a.db.QueryRow(q.sql).Scan(q.dst); e != nil { a.serverError(w, e) return } } limits, e := a.runtimeSettings() if e != nil { a.serverError(w, e) return } var disk unix.Statfs_t if e = unix.Statfs(a.cfg.DataDir, &disk); e != nil { a.serverError(w, e) return } var auditCount, auditBytes int64 if e = a.db.QueryRow("SELECT records,content_bytes FROM audit_usage WHERE id=1").Scan(&auditCount, &auditBytes); e != nil { a.serverError(w, e) return } a.transferMu.Lock() up, down := a.transfers["upload"], a.transfers["download"] a.transferMu.Unlock() respond(w, 200, map[string]any{"object_bytes": used, "reserved_bytes": reserved, "metadata_bytes": meta, "logical_bytes": raw, "objects": objects, "files": files, "active_parcels": parcels, "data_budget": limits.DataBudget, "file_budget": limits.FileBudget, "min_free_bytes": limits.MinFreeBytes, "disk_total_bytes": int64(disk.Blocks) * int64(disk.Bsize), "disk_free_bytes": int64(disk.Bavail) * int64(disk.Bsize), "audit_records": auditCount, "audit_content_bytes": auditBytes, "active_uploads": up, "active_downloads": down, "encryption": a.cfg.Encryption, "deduplication": a.cfg.Deduplication}) } func (a *App) adminDelete(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } a.mu.Lock() defer a.mu.Unlock() id := r.PathValue("parcel") res, e := a.db.Exec("UPDATE parcels SET state='revoked',version=version+1 WHERE id=?", id) if e != nil { a.serverError(w, e) return } n, _ := res.RowsAffected() if n == 0 { fail(w, 404, "包裹不存在") return } a.audit("admin.delete", id, a.remoteIP(r)) respond(w, 200, map[string]any{"revoked": true, "message": "已禁止新下载,实体文件由清理任务回收"}) } func (a *App) adminPolicy(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } if r.Method == "GET" { p, e := a.policy() if e != nil { a.serverError(w, e) return } respond(w, 200, p) return } var p Policy if !decode(w, r, &p) { return } if e := p.Validate(); e != nil { fail(w, 400, e.Error()) return } a.mu.Lock() defer a.mu.Unlock() b, e := json.Marshal(p) if e == nil { _, e = a.db.Exec("UPDATE settings SET json=? WHERE id=1", string(b)) } if e != nil { a.serverError(w, e) return } a.audit("policy.update", "", a.remoteIP(r)) respond(w, 200, p) } func (a *App) adminCleanup(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } if e := a.Cleanup(); e != nil { a.serverError(w, e) return } a.audit("admin.cleanup", "", a.remoteIP(r)) respond(w, 200, map[string]bool{"cleaned": true}) } func (a *App) adminDeleteFile(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } a.mu.Lock() defer a.mu.Unlock() fid := r.PathValue("file") res, e := a.db.Exec("UPDATE files SET status='deleting' WHERE id=?", fid) if e != nil { a.serverError(w, e) return } n, _ := res.RowsAffected() if n == 0 { fail(w, 404, "文件不存在") return } a.audit("admin.file_delete", fid, a.remoteIP(r)) respond(w, 200, map[string]bool{"deleted": true}) } func (a *App) adminReissue(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } a.mu.Lock() defer a.mu.Unlock() id := r.PathValue("parcel") var state string if e := a.db.QueryRow("SELECT state FROM parcels WHERE id=?", id).Scan(&state); e != nil || state != "restored" { fail(w, 409, "仅允许重新发布恢复待审核包裹") return } p, e := a.policy() if e != nil { a.serverError(w, e) return } now := a.now().Unix() expiry := now + int64(p.DefaultDays)*86400 hard := now + int64(p.MaxLifetimeDays)*86400 code := pickupCode() tx, e := a.db.Begin() if e != nil { a.serverError(w, e) return } defer tx.Rollback() res, e := tx.Exec("UPDATE files SET status='ready',remaining=?,expires=?,hard_expires=? WHERE parcel=? AND object_id IS NOT NULL AND status NOT IN('missing','deleted','deleting')", p.DefaultDownloads, expiry, hard, id) if e != nil { a.serverError(w, e) return } n, _ := res.RowsAffected() if n == 0 { fail(w, 409, "没有可重新发布的文件") return } _, e = tx.Exec("UPDATE parcels SET state='ready',created=?,expires=?,hard_expires=?,code_hash=?,version=version+1,renewal_days=?,renewal_mode=? WHERE id=?", now, expiry, hard, a.keyed(code), p.RenewalDays, p.RenewalMode, id) if e == nil { e = tx.Commit() } if e != nil { a.serverError(w, e) return } a.audit("admin.reissue", id, a.remoteIP(r)) respond(w, 200, a.shareResult(r, code, expiry)) }