341 lines
9.2 KiB
Go
341 lines
9.2 KiB
Go
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 = <uuid>$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 := <str>$0,
|
|
email := <str>$1,
|
|
name := <str>$2,
|
|
avatar := <str>$3,
|
|
setting := (
|
|
INSERT Setting {
|
|
default_model := "gpt-3.5-turbo"
|
|
}
|
|
),
|
|
identity := (SELECT ext::auth::Identity FILTER .id = <uuid>$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,
|
|
selected := true,
|
|
}`)
|
|
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": "Response Status is not OK"}, "layouts/main")
|
|
}
|
|
|
|
var tokenResponse TokenResponse
|
|
err = json.NewDecoder(resp.Body).Decode(&tokenResponse)
|
|
if err != nil {
|
|
return c.Render("error", fiber.Map{"Text": "Can't decode response"}, "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": "Email auth not yet implemented"}, "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)
|
|
}
|
|
}
|