package main import ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "math/rand" "net/http" "os" "path/filepath" "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 { usernames = append(usernames, file.Name()) } 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" }