From a2e4ad87dbc85adae6bd07b8e46b6d024f27bba9 Mon Sep 17 00:00:00 2001 From: "corrado.mulas" Date: Tue, 19 Nov 2024 22:32:50 +0100 Subject: [PATCH] First commit --- .env.example | 6 +++ .gitignore | 1 + Dockerfile | 10 +++++ captcha.go | 32 ++++++++++++++ handlers.go | 73 +++++++++++++++++++++++++++++++ main.go | 28 ++++++++++++ overleaf.go | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 268 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 captcha.go create mode 100644 handlers.go create mode 100644 main.go create mode 100644 overleaf.go diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c5c07a5 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +ALLOWED_DOMAINS=xyz.com,abc.de,fgh.il +CAPTCHA_SERVER_KEY= +OL_INSTANCE= +OL_ADMIN_EMAIL= +OL_ADMIN_PASSWORD= +PORT=3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a1af874 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.20-alpine + +WORKDIR /app + +COPY . . + +RUN go mod tidy +RUN go build -o app . + +CMD ["./app"] diff --git a/captcha.go b/captcha.go new file mode 100644 index 0000000..b804123 --- /dev/null +++ b/captcha.go @@ -0,0 +1,32 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/url" + "os" +) + +type CaptchaResponse struct { + Success bool `json:"success"` +} + +func ValidateCaptcha(captcha string) bool { + secretKey := os.Getenv("CAPTCHA_SERVER_KEY") + if secretKey == "" { + return true // Skip validation if CAPTCHA is disabled + } + + resp, err := http.PostForm("https://www.google.com/recaptcha/api/siteverify", url.Values{ + "secret": {secretKey}, + "response": {captcha}, + }) + if err != nil { + return false + } + defer resp.Body.Close() + + var result CaptchaResponse + err = json.NewDecoder(resp.Body).Decode(&result) + return err == nil && result.Success +} diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..b3db6c7 --- /dev/null +++ b/handlers.go @@ -0,0 +1,73 @@ +package main + +import ( + "encoding/json" + "net/http" + "os" + "strings" +) + +type RegisterRequest struct { + Email string `json:"email"` + Password string `json:"password"` + Captcha string `json:"captcha"` +} + +func RegisterHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse the request + var req RegisterRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // Validate email domain + allowedDomains := strings.Split(os.Getenv("ALLOWED_DOMAINS"), ",") + if !isDomainAllowed(req.Email, allowedDomains) { + http.Error(w, "Email domain not allowed", http.StatusForbidden) + return + } + + // Validate CAPTCHA + if !ValidateCaptcha(req.Captcha) { + http.Error(w, "Invalid CAPTCHA", http.StatusUnauthorized) + return + } + + // Register user + overleaf := NewOverleaf(os.Getenv("OL_INSTANCE")) + err = overleaf.Login(os.Getenv("OL_ADMIN_EMAIL"), os.Getenv("OL_ADMIN_PASSWORD")) + if err != nil { + http.Error(w, "Failed to log in as admin", http.StatusInternalServerError) + return + } + + err = overleaf.RegisterUser(req.Email, req.Password) + if err != nil { + http.Error(w, "Failed to register user: "+err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]string{"message": "User registered successfully"}) +} + +func isDomainAllowed(email string, allowedDomains []string) bool { + parts := strings.Split(email, "@") + if len(parts) != 2 { + return false + } + domain := parts[1] + for _, allowed := range allowedDomains { + if domain == allowed { + return true + } + } + return false +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..761540d --- /dev/null +++ b/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "log" + "net/http" + "os" + + "github.com/joho/godotenv" +) + +func main() { + // Load environment variables + err := godotenv.Load() + if err != nil { + log.Println("No .env file found, using environment variables") + } + + // Register the HTTP handler + http.HandleFunc("/register", RegisterHandler) + + // Start the HTTP server + port := os.Getenv("PORT") + if port == "" { + port = "3000" + } + log.Printf("Server running on port %s\n", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) +} diff --git a/overleaf.go b/overleaf.go new file mode 100644 index 0000000..f942acd --- /dev/null +++ b/overleaf.go @@ -0,0 +1,118 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/PuerkitoBio/goquery" +) + +type Overleaf struct { + BaseURL string + Client *http.Client + CSRF string +} + +// NewOverleaf initializes a new Overleaf instance +func NewOverleaf(baseURL string) *Overleaf { + return &Overleaf{ + BaseURL: strings.TrimSuffix(baseURL, "/"), + Client: &http.Client{}, + } +} + +// get performs a GET request +func (o *Overleaf) get(endpoint string) (*http.Response, error) { + url := fmt.Sprintf("%s%s", o.BaseURL, endpoint) + return o.Client.Get(url) +} + +// post performs a POST request +func (o *Overleaf) post(endpoint string, data map[string]string) (*http.Response, error) { + if o.CSRF == "" { + if err := o.obtainCSRF(); err != nil { + return nil, err + } + } + + url := fmt.Sprintf("%s%s", o.BaseURL, endpoint) + data["_csrf"] = o.CSRF + formData := encodeFormData(data) + + req, err := http.NewRequest("POST", url, bytes.NewBufferString(formData)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + return o.Client.Do(req) +} + +// obtainCSRF retrieves the CSRF token +func (o *Overleaf) obtainCSRF() error { + resp, err := o.get("/login") + if err != nil { + return err + } + defer resp.Body.Close() + + doc, err := goquery.NewDocumentFromReader(resp.Body) + if err != nil { + return err + } + + csrf, exists := doc.Find(`meta[name="ol-csrfToken"]`).Attr("content") + if !exists { + return errors.New("CSRF token not found") + } + o.CSRF = csrf + return nil +} + +// Login as admin +func (o *Overleaf) Login(email, password string) error { + resp, err := o.post("/login", map[string]string{ + "email": email, + "password": password, + }) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return errors.New("failed to log in as admin") + } + return nil +} + +// RegisterUser registers a new user +func (o *Overleaf) RegisterUser(email, password string) error { + resp, err := o.post("/admin/register", map[string]string{ + "email": email, + "password": password, + }) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to register user: %s", string(body)) + } + return nil +} + +// encodeFormData encodes a map into x-www-form-urlencoded format +func encodeFormData(data map[string]string) string { + var formData []string + for key, value := range data { + formData = append(formData, fmt.Sprintf("%s=%s", key, value)) + } + return strings.Join(formData, "&") +}