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)
}