package main import ( "html/template" "net/http" "strconv" "strings" "time" ) const adminSchema = ` create table if not exists settings ( key text primary key, value text not null ); create table if not exists redirects ( owner text not null, name text not null, repo integer not null references repos(id) on delete cascade, primary key(owner, name) ); ` var registrationModes = []string{"open", "invite", "closed"} func (s *Store) Setting(key, fallback string) string { var v string if err := s.db.QueryRow(`select value from settings where key = ?`, key).Scan(&v); err != nil { return fallback } return v } func (s *Store) SetSetting(key, value string) error { _, err := s.db.Exec(`insert into settings(key, value) values (?, ?) on conflict(key) do update set value = excluded.value`, key, value) return err } func (s *Store) Registration() string { return s.Setting("registration", "open") } func validRegistration(mode string) bool { for _, m := range registrationModes { if m == mode { return true } } return false } func (s *Store) IsAdmin(id int64) bool { var admin bool s.db.QueryRow(`select admin from users where id = ? and banned = 0`, id).Scan(&admin) return admin } func (s *Store) SetAdmin(name string, admin bool) error { res, err := s.db.Exec(`update users set admin = ? where name = ? and org = 0`, admin, name) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return ErrNotFound } return nil } func (s *Store) AddRedirect(owner, name string, repo int64) { s.db.Exec(`insert into redirects(owner, name, repo) values (?, ?, ?) on conflict(owner, name) do update set repo = excluded.repo`, owner, name, repo) } func (s *Store) Redirect(owner, name string) (Repo, bool) { var id int64 if err := s.db.QueryRow(`select repo from redirects where owner = ? and name = ?`, owner, name).Scan(&id); err != nil { return Repo{}, false } repo, err := s.RepoByID(id) return repo, err == nil } func (s *Store) Invites() []string { rows, err := s.db.Query(`select code from invites where used = 0 order by rowid desc limit 50`) if err != nil { return nil } defer rows.Close() var codes []string for rows.Next() { var c string if rows.Scan(&c) == nil { codes = append(codes, c) } } return codes } type account struct { User Admin bool Banned bool Org bool Repos int } func (s *Store) Accounts() []account { rows, err := s.db.Query(`select u.id, u.name, u.created, u.admin, u.banned, u.org, (select count(*) from repos r where r.owner = u.id) from users u order by u.created desc limit 1000`) if err != nil { return nil } defer rows.Close() var out []account for rows.Next() { var a account var created int64 if rows.Scan(&a.ID, &a.Name, &created, &a.Admin, &a.Banned, &a.Org, &a.Repos) == nil { a.Created = time.Unix(created, 0) out = append(out, a) } } return out } func (s *Server) admin(w http.ResponseWriter, r *http.Request) { u := s.require(w, r) if u == nil { return } if !s.store.IsAdmin(u.ID) { s.fail(w, r, 404, "Not found") return } if r.Method == "POST" { s.adminAction(w, r, u) return } featured, _ := s.store.FeaturedRepos() ranks := map[int64]int{} for i, f := range featured { ranks[f.ID] = i + 1 } public, _ := s.store.PublicRepos("", "") s.render(w, r, "admin", page{Title: "Admin", User: u, Error: r.URL.Query().Get("error"), Data: map[string]any{ "Registration": s.store.Registration(), "Modes": registrationModes, "Invites": s.store.Invites(), "Accounts": s.store.Accounts(), "Ranks": ranks, "Public": public, "Quota": compactSize(readQuota(s.root)), }}) } func (s *Server) adminAction(w http.ResponseWriter, r *http.Request, u *User) { back := "/admin" fail := func(msg string) { http.Redirect(w, r, back+"?error="+template.URLQueryEscaper(msg), http.StatusSeeOther) } name := strings.TrimPrefix(strings.TrimSpace(r.FormValue("name")), "~") switch r.FormValue("action") { case "registration": mode := r.FormValue("mode") if !validRegistration(mode) { fail("Unknown registration mode") return } s.store.SetSetting("registration", mode) case "invite": if _, err := s.store.CreateInvite(); err != nil { fail(err.Error()) return } back += "#invites" case "quota": n, err := parseSize(strings.TrimSpace(r.FormValue("quota"))) if err != nil { fail(err.Error()) return } if err := writeQuota(s.root, n); err != nil { fail(err.Error()) return } case "ban", "unban": if name == u.Name { fail("You cannot ban yourself") return } if err := s.store.SetBanned(name, r.FormValue("action") == "ban"); err != nil { fail("No such account") return } back += "#accounts" case "admin", "unadmin": if name == u.Name { fail("You cannot change your own admin rights") return } if err := s.store.SetAdmin(name, r.FormValue("action") == "admin"); err != nil { fail("No such user") return } back += "#accounts" case "feature": owner, repo, ok := strings.Cut(name, "/") rank, _ := strconv.Atoi(r.FormValue("rank")) if !ok || rank < 0 || s.store.SetFeatured(owner, repo, rank) != nil { fail("No such repository") return } back += "#featured" default: fail("Unknown action") return } http.Redirect(w, r, back, http.StatusSeeOther) } func (s *Server) newOrg(w http.ResponseWriter, r *http.Request) { u := s.require(w, r) if u == nil { return } if r.Method == "GET" { s.render(w, r, "neworg", page{Title: "New organisation", User: u}) return } name := strings.TrimSpace(r.FormValue("name")) if s.limiter.blocked("org:" + u.Name) { s.render(w, r, "neworg", page{Title: "New organisation", User: u, Error: "Too many organisations created recently, try again later", Data: map[string]any{"Name": name}}) return } org, err := s.store.CreateOrg(name) if err == nil { err = s.store.SetMember(org.ID, u.ID, true) } if err != nil { w.WriteHeader(400) s.render(w, r, "neworg", page{Title: "New organisation", User: u, Error: err.Error(), Data: map[string]any{"Name": name}}) return } s.limiter.fail("org:" + u.Name) http.Redirect(w, r, "/~"+org.Name+"/settings", http.StatusSeeOther) }