package main import ( "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/smtp" "os" "github.com/edgedb/edgedb-go" "github.com/gofiber/fiber/v2" ) type DiscoveryDocument struct { UserInfoEndpoint string `json:"userinfo_endpoint"` } type UserProfile struct { Email string `json:"email"` Name string `json:"name"` AvatarGitHub string `json:"avatar_url"` AvatarGoogle string `json:"picture"` } type TokenResponse struct { AuthToken string `json:"auth_token"` IdentityID string `json:"identity_id"` ProviderToken string `json:"provider_token"` } func getGoogleUserProfile(providerToken string) (string, string, string) { // Fetch the discovery document resp, err := http.Get("https://accounts.google.com/.well-known/openid-configuration") if err != nil { fmt.Println("Error fetching discovery document") panic(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { fmt.Println("Error fetching discovery document") panic(resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println("Error reading discovery document") panic(err) } var discoveryDocument DiscoveryDocument if err := json.Unmarshal(body, &discoveryDocument); err != nil { fmt.Println("Error unmarshalling discovery document") panic(err) } // Fetch the user profile req, err := http.NewRequest("GET", discoveryDocument.UserInfoEndpoint, nil) if err != nil { fmt.Println("Error fetching user profile") panic(err) } req.Header.Set("Authorization", "Bearer "+providerToken) req.Header.Set("Accept", "application/json") client := &http.Client{} resp, err = client.Do(req) if err != nil { fmt.Println("Error fetching user profile") panic(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { panic("Error fetching user profile") } body, err = io.ReadAll(resp.Body) if err != nil { fmt.Println("Error reading user profile") panic(err) } var userProfile UserProfile if err := json.Unmarshal(body, &userProfile); err != nil { fmt.Println("Error unmarshalling user profile") panic(err) } return userProfile.Email, userProfile.Name, userProfile.AvatarGoogle } func getGitHubUserProfile(providerToken string) (string, string, string) { // Create the request to fetch the user profile req, err := http.NewRequest("GET", "https://api.github.com/user", nil) if err != nil { fmt.Println("failed to create request: user profile") panic(err) } req.Header.Set("Authorization", "Bearer "+providerToken) req.Header.Set("Accept", "application/vnd.github+json") req.Header.Set("X-GitHub-Api-Version", "2022-11-28") client := &http.Client{} resp, err := client.Do(req) if err != nil { fmt.Println("failed to execute request") panic(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { fmt.Println("failed to fetch user profile: status code") panic(resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println("failed to read response body") panic(err) } var userProfile UserProfile if err := json.Unmarshal(body, &userProfile); err != nil { fmt.Println("failed to unmarshal user profile") panic(err) } return userProfile.Email, userProfile.Name, userProfile.AvatarGitHub } func generatePKCE() (string, string) { verifier_source := make([]byte, 32) _, err := rand.Read(verifier_source) if err != nil { fmt.Println("failed to generate PKCE") panic(err) } verifier := base64.RawURLEncoding.EncodeToString(verifier_source) challenge := sha256.Sum256([]byte(verifier)) return verifier, base64.RawURLEncoding.EncodeToString(challenge[:]) } func handleUiSignIn(c *fiber.Ctx) error { verifier, challenge := generatePKCE() c.Cookie(&fiber.Cookie{ Name: "jade-edgedb-pkce-verifier", Value: verifier, HTTPOnly: true, Path: "/", Secure: true, }) return c.Redirect(fmt.Sprintf("%s/ui/signin?challenge=%s", os.Getenv("EDGEDB_AUTH_BASE_URL"), challenge), fiber.StatusTemporaryRedirect) } func handleCallbackSignup(c *fiber.Ctx) error { if c.Query("verification_email_sent_at") != "" { return c.Redirect("/") } code := c.Query("code") if code == "" { err := c.Query("error") fmt.Println("OAuth callback is missing 'code'. OAuth provider responded with error") panic(err) } verifier := c.Cookies("jade-edgedb-pkce-verifier", "") if verifier == "" { return c.SendString("Could not find 'verifier' in the cookie store. Is this the same user agent/browser that started the authorization flow?") } codeExchangeURL := fmt.Sprintf("%s/token?code=%s&verifier=%s", os.Getenv("EDGEDB_AUTH_BASE_URL"), code, verifier) resp, err := http.Get(codeExchangeURL) if err != nil { fmt.Println("Error exchanging code for access token") panic(err) } defer resp.Body.Close() if resp.StatusCode != fiber.StatusOK { body, _ := io.ReadAll(resp.Body) fmt.Println("Error exchanging code for access token") panic(string(body)) } var tokenResponse TokenResponse err = json.NewDecoder(resp.Body).Decode(&tokenResponse) if err != nil { fmt.Println("Error decoding auth server response") panic(err) } c.Cookie(&fiber.Cookie{ Name: "jade-edgedb-auth-token", Value: tokenResponse.AuthToken, HTTPOnly: true, Path: "/", Secure: true, }) // Get the issuer of the identity var identity Identity identityUUID, err := edgedb.ParseUUID(tokenResponse.IdentityID) if err != nil { fmt.Println("Error parsing UUID") panic(err) } err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` SELECT ext::auth::Identity { issuer } FILTER .id = $0 `, &identity, identityUUID) if err != nil { fmt.Println("Error fetching identity") panic(err) } var ( providerEmail string providerName string providerAvatar string ) // Get the email and name from the provider if identity.Issuer == "https://accounts.google.com" { providerEmail, providerName, providerAvatar = getGoogleUserProfile(tokenResponse.ProviderToken) } else if identity.Issuer == "https://github.com" { providerEmail, providerName, providerAvatar = getGitHubUserProfile(tokenResponse.ProviderToken) // Work !!!! } stripCustID := CreateNewStripeCustomer(providerName, providerEmail) err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": tokenResponse.AuthToken}).Execute(edgeCtx, ` INSERT User { stripe_id := $0, email := $1, name := $2, avatar := $3, setting := ( INSERT Setting { default_model := "gpt-3.5-turbo" } ), identity := (SELECT ext::auth::Identity FILTER .id = $4) } `, stripCustID, providerEmail, providerName, providerAvatar, identityUUID) if err != nil { fmt.Println("Error creating user") panic(err) } err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": tokenResponse.AuthToken}).Execute(edgeCtx, ` INSERT Conversation { name := 'Default', user := global currentUser, position := 1 }`) if err != nil { fmt.Println("Error creating default conversation") panic(err) } return c.Redirect("/", fiber.StatusPermanentRedirect) } func handleCallback(c *fiber.Ctx) error { code := c.Query("code") if code == "" { fmt.Println("OAuth callback is missing 'code'. OAuth provider responded with error") return c.Render("error", fiber.Map{"Text": "Error: OAuth provider responded with an error. Please contact the support or try later."}, "layouts/main") } verifier := c.Cookies("jade-edgedb-pkce-verifier", "") if verifier == "" { return c.Render("error", fiber.Map{"Text": "Error: No verifier cookie found. Please make sure to use the same devide and browser when login for the first time."}, "layouts/main") } codeExchangeURL := fmt.Sprintf("%s/token?code=%s&verifier=%s", os.Getenv("EDGEDB_AUTH_BASE_URL"), code, verifier) resp, err := http.Get(codeExchangeURL) if err != nil { return c.Render("error", fiber.Map{"Text": "Internal JADE error code "}, "layouts/main") } defer resp.Body.Close() if resp.StatusCode != fiber.StatusOK { return c.Render("error", fiber.Map{"Text": "Hello"}, "layouts/main") } var tokenResponse TokenResponse err = json.NewDecoder(resp.Body).Decode(&tokenResponse) if err != nil { return c.Render("error", fiber.Map{"Text": "Hello"}, "layouts/main") } c.Cookie(&fiber.Cookie{ Name: "jade-edgedb-auth-token", Value: tokenResponse.AuthToken, HTTPOnly: true, Path: "/", Secure: true, SameSite: "Strict", }) return c.Redirect("/", fiber.StatusPermanentRedirect) } func handleSignOut(c *fiber.Ctx) error { c.ClearCookie("jade-edgedb-auth-token") return c.Redirect("/", fiber.StatusTemporaryRedirect) } func handleEmailVerification(c *fiber.Ctx) error { return c.Render("error", fiber.Map{"Text": "Hello"}, "layouts/main") } func SendNoreplyEmail(to string, subject string, content string) { auth := smtp.PlainAuth("", "noreply@bouvai.com", os.Getenv("NOREPLY_APP_PASSWORD"), "smtp.gmail.com") msg := []byte("To: " + to + "\r\n" + "Subject: " + subject + "\r\n" + "\r\n" + content + "\r\n") err := smtp.SendMail("smtp.gmail.com:587", auth, "noreply@bouvai.com", []string{to}, msg) if err != nil { panic(err) } }