diff --git a/TODO b/TODO index abaa1b5..d3392ea 100644 --- a/TODO +++ b/TODO @@ -1,2 +1,4 @@ -- [ ] admin check for a user struct +- [x] admin check for a user struct +- [x] gitea requesting +- [x] gemini requesting - [ ] get user stats/info diff --git a/request/gemini.go b/request/gemini.go new file mode 100644 index 0000000..81b47f5 --- /dev/null +++ b/request/gemini.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + email "git.tilde.town/tildetown/town/email" +) + +const geminiHomeDocBase = "/home/gemini/users" + +func processGemini(requestRootPath string) error { + rp := filepath.Join(requestRootPath, "gemini") + + files, err := ioutil.ReadDir(rp) + if err != nil { + return fmt.Errorf("failed to list directory %s: %w", rp, err) + } + + usernames := []string{} + + for _, file := range files { + usernames = append(usernames, file.Name()) + } + + if len(usernames) == 0 { + return nil + } + + for _, username := range usernames { + err := linkGemini(username) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to process gemini request for %s: %s\n", username, err.Error()) + } + + os.Remove(filepath.Join(rp, username)) + } + + return nil +} + +func linkGemini(username string) error { + pgPath := filepath.Join("/home", username, "public_gemini") + if !pathExists(pgPath) { + return fmt.Errorf("public_gemini missing for %s", username) + } + + geminiPath := filepath.Join(geminiHomeDocBase, username) + + if !pathExists(geminiPath) { + err := os.Symlink(pgPath, geminiPath) + if err != nil { + return fmt.Errorf("failed to link public_gemini for %s: %w", username, err) + } + } + body := fmt.Sprintf(`hi %s! + +you requested a gemini space on the town. this space has been activated and anything you do in your public_gemini directory should now be reflected by the server. + +if you did _not_ request this, please let an admin know.`, username) + return email.SendLocalEmail(username, "gemini", body) +} + +func pathExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} diff --git a/request/gitea.go b/request/gitea.go new file mode 100644 index 0000000..48cad79 --- /dev/null +++ b/request/gitea.go @@ -0,0 +1,179 @@ +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" +} diff --git a/request/main.go b/request/main.go index e82f9f2..b5ad211 100644 --- a/request/main.go +++ b/request/main.go @@ -3,11 +3,9 @@ package main import ( "errors" "fmt" - "io/ioutil" "os" "os/user" - "path/filepath" - //email "git.tilde.town/tildetown/town/email" + townUser "git.tilde.town/tildetown/town/user" ) @@ -28,8 +26,16 @@ func _main(args []string) error { } errs := []error{} - errs = append(errs, processGitea(requestPath)) - errs = append(errs, processGemini(requestPath)) + + err = processGitea(requestPath) + if err != nil { + errs = append(errs, err) + } + + err = processGemini(requestPath) + if err != nil { + errs = append(errs, err) + } if len(errs) > 0 { errMsg := "errors encountered during request processing: " @@ -53,35 +59,3 @@ func main() { os.Exit(retcode) } - -func processGitea(rp string) error { - return nil -} - -func processGemini(requestRootPath string) error { - rp := filepath.Join(requestRootPath, "gemini") - - files, err := ioutil.ReadDir(rp) - if err != nil { - return fmt.Errorf("failed to list directory %s: %w", rp, err) - } - - usernames := []string{} - - for _, file := range files { - usernames = append(usernames, file.Name()) - } - - if len(usernames) == 0 { - return nil - } - - for _, username := range usernames { - // TODO check for public_gemini already in their home dir - // TODO create publiC_gemini - // TODO check for existing symlink - // TODO create symlink - } - - return nil -} diff --git a/request/request b/request/request new file mode 100755 index 0000000..5b37fb8 Binary files /dev/null and b/request/request differ