town/request/gitea.go

189 lines
4.5 KiB
Go

package request
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"os"
"os/user"
"path/filepath"
"syscall"
"time"
email "git.tilde.town/tildetown/town/email"
)
const pwLetters = "!@#$%^&*()[]{}:;,.<>/?abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890"
func ProcessGitea(rp string) error {
apiToken := os.Getenv("GITEA_TOKEN")
if apiToken == "" {
return errors.New("need GITEA_TOKEN")
}
gtPath := filepath.Join(RequestPath, "gitea")
files, err := ioutil.ReadDir(gtPath)
if err != nil {
return err
}
usernames := []string{}
for _, file := range files {
sysInfo := file.Sys()
uid := sysInfo.(*syscall.Stat_t).Uid
user, err := user.LookupId(fmt.Sprintf("%d", uid))
if err != nil {
fmt.Fprintf(os.Stderr, "failed to get owner of file named '%s': %s", file.Name(), err)
continue
}
usernames = append(usernames, user.Username)
}
if len(usernames) == 0 {
return nil
}
for _, username := range usernames {
exists, err := giteaUserExists(apiToken, username)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to check for existing account for %s: %s\n", username, err)
continue
}
if !exists {
password, err := createGiteaUser(apiToken, username)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to create account for %s: %s\n", username, err)
continue
}
err = sendGiteaEmail(username, password)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to send email to %s; reach out manually: %s\n", username, err)
}
}
os.Remove(filepath.Join(gtPath, username))
}
return nil
}
func createGiteaUser(apiToken, username string) (string, error) {
client := &http.Client{}
password := genGiteaPassword()
// TODO using local email sucks for obvious reasons but it'll have to do for now. ideally password
// resets can be set local to the server, so the thing to change is not the local email but the
// ability for gitea to send mail internally.
createPayload := struct {
Email string
FullName string `json:"full_name"`
Login string `json:"login_name"`
MustChangePassword bool `json:"must_change_password"`
Password string
SendNotify bool `json:"send_notify"`
Username string
SourceId int `json:"source_id"`
}{
Email: fmt.Sprintf("%s@tilde.town", username),
FullName: username,
Login: username,
MustChangePassword: true,
Password: password,
SendNotify: false,
Username: username,
SourceId: 0,
}
body, err := json.Marshal(createPayload)
if err != nil {
return "", err
}
req, err := giteaAPIReq(apiToken, "POST", "admin/users", bytes.NewBuffer(body))
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
if resp.StatusCode != 201 {
lol, _ := ioutil.ReadAll(resp.Body)
fmt.Printf("DBG %#v\n", string(lol))
return "", fmt.Errorf("failed to create user for %s; error code %d", username, resp.StatusCode)
}
return password, nil
}
func giteaUserExists(apiToken, username string) (bool, error) {
client := &http.Client{}
req, err := giteaAPIReq(apiToken, "GET", "users/"+username, nil)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
if resp.StatusCode == 200 {
return true, nil
} else if resp.StatusCode == 404 {
return false, nil
} else {
return false, fmt.Errorf("unexpected response code: %d", resp.StatusCode)
}
}
func giteaAPIReq(apiToken, method, path string, body *bytes.Buffer) (*http.Request, error) {
if body == nil {
body = bytes.NewBufferString("")
}
basePath := "https://git.tilde.town/api/v1/"
req, err := http.NewRequest(method, basePath+path, body)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("token %s", apiToken))
req.Header.Add("Content-Type", "application/json")
return req, nil
}
func sendGiteaEmail(username, password string) error {
body := fmt.Sprintf(`hi %s!
you requested a git.tilde.town account and now you have one~
please log in with username %s and password %s
you'll be prompted to change the password.
if you did _not_ request this, please let an admin know.
`, username, username, password)
return email.SendLocalEmail(username, "gitea", body)
}
func genGiteaPassword() string {
rand.Seed(time.Now().UnixNano())
b := make([]byte, 20)
for i := range b {
b[i] = pwLetters[rand.Intn(len(pwLetters))]
}
// Bootleg, but hack to ensure we meet complexity requirement
return string(b) + "!" + "1" + "A"
}