mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 00:35:41 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b4a6cadf5 | ||
|
|
ac852b7a8f | ||
|
|
4c2e8d56d3 | ||
|
|
61a742d142 | ||
|
|
ae5a33863e | ||
|
|
2129945604 | ||
|
|
9136c27281 |
@@ -106,6 +106,10 @@ jobs:
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
|
||||
GH_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
|
||||
APPLE_SIGNIN_CLIENT_ID: ${{ secrets.APPLE_SIGNIN_CLIENT_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_SIGNIN_KEY_ID: ${{ secrets.APPLE_SIGNIN_KEY_ID }}
|
||||
APPLE_SIGNIN_PRIVATE_KEY: ${{ secrets.APPLE_SIGNIN_PRIVATE_KEY }}
|
||||
JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
|
||||
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
|
||||
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
|
||||
@@ -114,16 +118,6 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Copy update-env script to server
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.DO_DROPLET_IP }}
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
source: "deploy/update-env.sh,deploy/env.template"
|
||||
target: /opt/eagle0/
|
||||
strip_components: 1
|
||||
|
||||
- name: Deploy auth service to production
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
@@ -131,25 +125,27 @@ jobs:
|
||||
username: deploy
|
||||
key: ${{ secrets.DO_SSH_KEY }}
|
||||
script_stop: true
|
||||
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GH_OAUTH_CLIENT_ID,GH_OAUTH_CLIENT_SECRET,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
|
||||
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GH_OAUTH_CLIENT_ID,GH_OAUTH_CLIENT_SECRET,APPLE_SIGNIN_CLIENT_ID,APPLE_TEAM_ID,APPLE_SIGNIN_KEY_ID,APPLE_SIGNIN_PRIVATE_KEY,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
|
||||
script: |
|
||||
set -x
|
||||
cd /opt/eagle0
|
||||
|
||||
# Update env vars using shared script (preserves vars set by other workflows)
|
||||
chmod +x update-env.sh
|
||||
./update-env.sh \
|
||||
"AUTH_IMAGE=${AUTH_IMAGE}" \
|
||||
"DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}" \
|
||||
"DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET}" \
|
||||
"GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}" \
|
||||
"GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}" \
|
||||
"GH_OAUTH_CLIENT_ID=${GH_OAUTH_CLIENT_ID}" \
|
||||
"GH_OAUTH_CLIENT_SECRET=${GH_OAUTH_CLIENT_SECRET}" \
|
||||
"JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY}" \
|
||||
"FASTMAIL_API_TOKEN=${FASTMAIL_API_TOKEN}" \
|
||||
"FASTMAIL_FROM_EMAIL=${FASTMAIL_FROM_EMAIL}" \
|
||||
"FASTMAIL_FROM_NAME=${FASTMAIL_FROM_NAME}"
|
||||
# Export env vars for docker compose (appleboy/ssh-action sets them but doesn't export)
|
||||
export AUTH_IMAGE="${AUTH_IMAGE}"
|
||||
export DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
|
||||
export DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
|
||||
export GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
|
||||
export GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
|
||||
export GH_OAUTH_CLIENT_ID="${GH_OAUTH_CLIENT_ID}"
|
||||
export GH_OAUTH_CLIENT_SECRET="${GH_OAUTH_CLIENT_SECRET}"
|
||||
export APPLE_SIGNIN_CLIENT_ID="${APPLE_SIGNIN_CLIENT_ID}"
|
||||
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
|
||||
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
|
||||
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
|
||||
export JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
|
||||
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
|
||||
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
|
||||
|
||||
# Login to registry
|
||||
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
|
||||
|
||||
@@ -215,69 +215,40 @@ jobs:
|
||||
set -ex
|
||||
cd /opt/eagle0
|
||||
|
||||
# Environment variables passed via heredoc
|
||||
EAGLE_IMAGE="${EAGLE_IMAGE}"
|
||||
ADMIN_IMAGE="${ADMIN_IMAGE}"
|
||||
JFR_SIDECAR_IMAGE="${JFR_SIDECAR_IMAGE}"
|
||||
OPENAI_API_KEY="${OPENAI_API_KEY}"
|
||||
GPT_MODEL_NAME="${GPT_MODEL_NAME}"
|
||||
EAGLE_ENABLE_S3="${EAGLE_ENABLE_S3}"
|
||||
DO_SPACES_ACCESS_KEY="${DO_SPACES_ACCESS_KEY}"
|
||||
DO_SPACES_SECRET_KEY="${DO_SPACES_SECRET_KEY}"
|
||||
JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
|
||||
DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
|
||||
DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
|
||||
GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
|
||||
GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
|
||||
GH_OAUTH_CLIENT_ID="${GH_OAUTH_CLIENT_ID}"
|
||||
GH_OAUTH_CLIENT_SECRET="${GH_OAUTH_CLIENT_SECRET}"
|
||||
APPLE_SIGNIN_CLIENT_ID="${APPLE_SIGNIN_CLIENT_ID}"
|
||||
APPLE_TEAM_ID="${APPLE_TEAM_ID}"
|
||||
APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
|
||||
APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
|
||||
SHARDOK_ADDRESS="${SHARDOK_ADDRESS}"
|
||||
SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
|
||||
SENTRY_DSN="${SENTRY_DSN}"
|
||||
FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
|
||||
FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
|
||||
DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
|
||||
# Export environment variables for docker compose
|
||||
# These are passed via heredoc and exported so child processes (docker compose) can access them
|
||||
export EAGLE_IMAGE="${EAGLE_IMAGE}"
|
||||
export ADMIN_IMAGE="${ADMIN_IMAGE}"
|
||||
export JFR_SIDECAR_IMAGE="${JFR_SIDECAR_IMAGE}"
|
||||
export OPENAI_API_KEY="${OPENAI_API_KEY}"
|
||||
export GPT_MODEL_NAME="${GPT_MODEL_NAME:-gpt-4o}"
|
||||
export EAGLE_ENABLE_S3="${EAGLE_ENABLE_S3:-false}"
|
||||
export DO_SPACES_ACCESS_KEY="${DO_SPACES_ACCESS_KEY}"
|
||||
export DO_SPACES_SECRET_KEY="${DO_SPACES_SECRET_KEY}"
|
||||
export JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
|
||||
export DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
|
||||
export DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
|
||||
export GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
|
||||
export GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
|
||||
export GH_OAUTH_CLIENT_ID="${GH_OAUTH_CLIENT_ID}"
|
||||
export GH_OAUTH_CLIENT_SECRET="${GH_OAUTH_CLIENT_SECRET}"
|
||||
export APPLE_SIGNIN_CLIENT_ID="${APPLE_SIGNIN_CLIENT_ID}"
|
||||
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
|
||||
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
|
||||
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
|
||||
export SHARDOK_ADDRESS="${SHARDOK_ADDRESS:-shardok:40042}"
|
||||
export SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
|
||||
export SENTRY_DSN="${SENTRY_DSN}"
|
||||
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
|
||||
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
|
||||
export DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
|
||||
|
||||
# Check Docker has IPv6 support
|
||||
if ! cat /etc/docker/daemon.json 2>/dev/null | grep -q '"ip6tables"'; then
|
||||
echo "WARNING: Docker IPv6 not configured. Eagle may not reach Hetzner Shardok."
|
||||
fi
|
||||
|
||||
# Update env vars
|
||||
chmod +x deploy/update-env.sh
|
||||
cd deploy && ./update-env.sh \
|
||||
"EAGLE_IMAGE=\${EAGLE_IMAGE}" \
|
||||
"ADMIN_IMAGE=\${ADMIN_IMAGE}" \
|
||||
"JFR_SIDECAR_IMAGE=\${JFR_SIDECAR_IMAGE}" \
|
||||
"OPENAI_API_KEY=\${OPENAI_API_KEY}" \
|
||||
"GPT_MODEL_NAME=\${GPT_MODEL_NAME:-gpt-4o}" \
|
||||
"EAGLE_ENABLE_S3=\${EAGLE_ENABLE_S3:-false}" \
|
||||
"DO_SPACES_ACCESS_KEY=\${DO_SPACES_ACCESS_KEY}" \
|
||||
"DO_SPACES_SECRET_KEY=\${DO_SPACES_SECRET_KEY}" \
|
||||
"JWT_PRIVATE_KEY=\${JWT_PRIVATE_KEY}" \
|
||||
"DISCORD_CLIENT_ID=\${DISCORD_CLIENT_ID}" \
|
||||
"DISCORD_CLIENT_SECRET=\${DISCORD_CLIENT_SECRET}" \
|
||||
"GOOGLE_CLIENT_ID=\${GOOGLE_CLIENT_ID}" \
|
||||
"GOOGLE_CLIENT_SECRET=\${GOOGLE_CLIENT_SECRET}" \
|
||||
"GH_OAUTH_CLIENT_ID=\${GH_OAUTH_CLIENT_ID}" \
|
||||
"GH_OAUTH_CLIENT_SECRET=\${GH_OAUTH_CLIENT_SECRET}" \
|
||||
"APPLE_SIGNIN_CLIENT_ID=\${APPLE_SIGNIN_CLIENT_ID}" \
|
||||
"APPLE_TEAM_ID=\${APPLE_TEAM_ID}" \
|
||||
"APPLE_SIGNIN_KEY_ID=\${APPLE_SIGNIN_KEY_ID}" \
|
||||
"APPLE_SIGNIN_PRIVATE_KEY=\${APPLE_SIGNIN_PRIVATE_KEY}" \
|
||||
"SHARDOK_ADDRESS=\${SHARDOK_ADDRESS:-shardok:40042}" \
|
||||
"SHARDOK_AUTH_TOKEN=\${SHARDOK_AUTH_TOKEN}" \
|
||||
"SENTRY_DSN=\${SENTRY_DSN}" \
|
||||
"FASTMAIL_API_TOKEN=\${FASTMAIL_API_TOKEN}" \
|
||||
"FASTMAIL_FROM_EMAIL=\${FASTMAIL_FROM_EMAIL}" \
|
||||
"FASTMAIL_FROM_NAME=\${FASTMAIL_FROM_NAME}"
|
||||
cd ..
|
||||
|
||||
# Login to registry
|
||||
echo "\$DO_REGISTRY_TOKEN" | docker login registry.digitalocean.com -u "\$DO_REGISTRY_TOKEN" --password-stdin
|
||||
|
||||
|
||||
@@ -44,15 +44,19 @@ for arg in "$@"; do
|
||||
fi
|
||||
|
||||
# Remove existing line for this key and add new one
|
||||
# Values are quoted to handle special characters (/, =, etc.)
|
||||
# Use single quotes to avoid issues with special characters in values
|
||||
# (JSON strings, base64 with /, etc.)
|
||||
# Escape any single quotes in the value by replacing ' with '\''
|
||||
ESCAPED_VALUE="${VALUE//\'/\'\\\'\'}"
|
||||
|
||||
if grep -q "^${KEY}=" "$ENV_FILE" 2>/dev/null; then
|
||||
# Key exists, remove old line and add new one (sed with special chars is fragile)
|
||||
grep -v "^${KEY}=" "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
echo "${KEY}=\"${VALUE}\"" >> "$ENV_FILE"
|
||||
echo "${KEY}='${ESCAPED_VALUE}'" >> "$ENV_FILE"
|
||||
echo "Updated $KEY"
|
||||
else
|
||||
# Key doesn't exist, add it
|
||||
echo "${KEY}=\"${VALUE}\"" >> "$ENV_FILE"
|
||||
echo "${KEY}='${ESCAPED_VALUE}'" >> "$ENV_FILE"
|
||||
echo "Added $KEY"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -46,6 +46,11 @@ http {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# Apple domain verification
|
||||
location /.well-known/apple-developer-domain-association.txt {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# Redirect all other HTTP to HTTPS
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
@@ -126,6 +131,11 @@ http {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Apple domain verification (for Sign In with Apple)
|
||||
location /.well-known/apple-developer-domain-association.txt {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -111,6 +112,8 @@ func (s *OAuthService) ExchangeAppleCode(config OAuthProviderConfig, code string
|
||||
data.Set("code", code)
|
||||
data.Set("redirect_uri", callbackURL)
|
||||
|
||||
log.Printf("Apple token exchange: client_id=%s redirect_uri=%s", config.ClientID, callbackURL)
|
||||
|
||||
req, err := http.NewRequest("POST", config.TokenEndpoint, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -180,6 +183,8 @@ func ParseAppleIDToken(idToken string) (*ProviderUserInfo, error) {
|
||||
// HandleAppleCallback handles the Apple OAuth callback
|
||||
// Note: Apple sends a POST request to the callback URL, not GET
|
||||
func (s *OAuthService) HandleAppleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Apple callback received: method=%s", r.Method)
|
||||
|
||||
// Apple POSTs the callback data
|
||||
if r.Method != "POST" {
|
||||
// Fallback to GET for the state parameter only
|
||||
@@ -188,17 +193,20 @@ func (s *OAuthService) HandleAppleCallback(w http.ResponseWriter, r *http.Reques
|
||||
// This might be an error redirect
|
||||
errorParam := r.URL.Query().Get("error")
|
||||
if errorParam != "" {
|
||||
log.Printf("Apple callback error (GET): %s", errorParam)
|
||||
s.completeWithError(state, fmt.Sprintf("Apple OAuth error: %s", errorParam))
|
||||
http.Error(w, "OAuth failed: "+errorParam, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Printf("Apple callback rejected: not POST")
|
||||
http.Error(w, "Apple Sign-In requires POST callback", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
log.Printf("Apple callback: failed to parse form: %v", err)
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -207,13 +215,17 @@ func (s *OAuthService) HandleAppleCallback(w http.ResponseWriter, r *http.Reques
|
||||
state := r.FormValue("state")
|
||||
errorParam := r.FormValue("error")
|
||||
|
||||
log.Printf("Apple callback: state=%s hasCode=%v error=%s", state, code != "", errorParam)
|
||||
|
||||
if errorParam != "" {
|
||||
log.Printf("Apple callback error (POST): %s", errorParam)
|
||||
s.completeWithError(state, fmt.Sprintf("Apple OAuth error: %s", errorParam))
|
||||
http.Error(w, "OAuth failed: "+errorParam, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if code == "" || state == "" {
|
||||
log.Printf("Apple callback: missing code or state")
|
||||
http.Error(w, "Missing code or state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -221,6 +233,7 @@ func (s *OAuthService) HandleAppleCallback(w http.ResponseWriter, r *http.Reques
|
||||
// Get pending OAuth info
|
||||
stateData, ok := s.pendingOAuth.Load(state)
|
||||
if !ok {
|
||||
log.Printf("Apple callback: state not found: %s", state)
|
||||
http.Error(w, "Invalid or expired state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -237,6 +250,7 @@ func (s *OAuthService) HandleAppleCallback(w http.ResponseWriter, r *http.Reques
|
||||
config := s.configs[oauthState.Provider]
|
||||
tokenResp, err := s.ExchangeAppleCode(config, code)
|
||||
if err != nil {
|
||||
log.Printf("Apple token exchange failed: %v", err)
|
||||
s.completeWithError(state, err.Error())
|
||||
http.Error(w, "Token exchange failed", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -340,7 +340,70 @@ func (s *OAuthService) fetchUserInfo(provider string, config OAuthProviderConfig
|
||||
return nil, fmt.Errorf("user info fetch failed: %d - %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return s.parseUserInfo(provider, body)
|
||||
userInfo, err := s.parseUserInfo(provider, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// GitHub: fetch email from /user/emails if not in main response
|
||||
// (GitHub only returns email in /user if user has made it public)
|
||||
if provider == "github" && userInfo.Email == "" {
|
||||
email, err := s.fetchGitHubEmail(accessToken)
|
||||
if err != nil {
|
||||
log.Printf("GitHub email fetch failed (non-fatal): %v", err)
|
||||
} else {
|
||||
userInfo.Email = email
|
||||
}
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (s *OAuthService) fetchGitHubEmail(accessToken string) (string, error) {
|
||||
req, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("email fetch failed: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var emails []struct {
|
||||
Email string `json:"email"`
|
||||
Primary bool `json:"primary"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &emails); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Return primary verified email, or first verified email
|
||||
for _, e := range emails {
|
||||
if e.Primary && e.Verified {
|
||||
return e.Email, nil
|
||||
}
|
||||
}
|
||||
for _, e := range emails {
|
||||
if e.Verified {
|
||||
return e.Email, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no verified email found")
|
||||
}
|
||||
|
||||
func (s *OAuthService) parseUserInfo(provider string, data []byte) (*ProviderUserInfo, error) {
|
||||
|
||||
Reference in New Issue
Block a user