Add authenticated profiles and prefill Google account names
Foundry Go checks / check (push) Successful in 1m42s
Foundry Go checks / check (pull_request) Successful in 1m51s

This commit is contained in:
2026-11-05 18:17:44 +08:00
parent 0fd84f0f6e
commit e626a2359e
4 changed files with 193 additions and 5 deletions
+22 -5
View File
@@ -153,7 +153,7 @@ func (s *service) start(w http.ResponseWriter, r *http.Request, project string)
}
query := url.Values{
"client_id": {s.clientID}, "redirect_uri": {s.redirectURI}, "response_type": {"code"},
"scope": {"openid email"}, "state": {state}, "code_challenge": {digest(verifier)},
"scope": {"openid email profile"}, "state": {state}, "code_challenge": {digest(verifier)},
"code_challenge_method": {"S256"}, "prompt": {"select_account"},
}
http.SetCookie(w, oauthCookie(state, browser, 600))
@@ -240,6 +240,8 @@ func (s *service) callback(w http.ResponseWriter, r *http.Request) error {
Subject string `json:"sub"`
Email *string `json:"email"`
Verified bool `json:"email_verified"`
Name string `json:"name"`
Picture string `json:"picture"`
}
if err := s.googleJSON(ctx, "GET", "https://openidconnect.googleapis.com/v1/userinfo", nil, tokens.AccessToken, &user); err != nil {
return err
@@ -253,14 +255,29 @@ func (s *service) callback(w http.ResponseWriter, r *http.Request) error {
grant := token()
_, err = s.db.ExecContext(ctx, `
WITH account AS (
INSERT INTO project_accounts (project, id, google_subject, email) VALUES ($1, gen_random_uuid(), $2, $3)
ON CONFLICT (project, google_subject) DO UPDATE SET email = EXCLUDED.email
WITH identity AS (
SELECT gen_random_uuid() AS id
), account AS (
INSERT INTO project_accounts (project, id, google_subject, email, username, display_name, avatar_url)
SELECT $1, id, $2, $3,
left(COALESCE(NULLIF(regexp_replace(lower($6), '[^a-z0-9]', '', 'g'), ''), 'user'),7)
|| '_' || replace(id::text,'-',''),
$6, NULLIF($7,'') FROM identity
ON CONFLICT (project, google_subject) DO UPDATE SET
email = EXCLUDED.email,
username = COALESCE(project_accounts.username, EXCLUDED.username),
display_name = CASE WHEN project_accounts.display_name='' THEN EXCLUDED.display_name ELSE project_accounts.display_name END,
avatar_url = COALESCE(project_accounts.avatar_url, EXCLUDED.avatar_url),
updated_at = now()
RETURNING id
), provider AS (
INSERT INTO account_identities(project,provider,subject,account_id)
SELECT $1,'google',$2,id FROM account
ON CONFLICT (project,provider,subject) DO NOTHING
)
INSERT INTO login_grants
SELECT $4, $1, id, $5, now() + interval '1 minute' FROM account
`, project, user.Subject, user.Email, digest(grant), challenge)
`, project, user.Subject, user.Email, digest(grant), challenge, user.Name, user.Picture)
if err != nil {
return err
}
+27
View File
@@ -0,0 +1,27 @@
package auth
import (
"context"
"database/sql"
"errors"
"strings"
)
var ErrInvalidSession = errors.New("invalid session")
// Authenticate resolves only an unexpired session belonging to this project.
func Authenticate(ctx context.Context, db *sql.DB, project, authorization string) (string, error) {
value, ok := strings.CutPrefix(authorization, "Bearer ")
if !ok || !tokenPattern.MatchString(value) {
return "", ErrInvalidSession
}
var id string
err := db.QueryRowContext(ctx, `
SELECT account_id::text FROM project_sessions
WHERE project=$1 AND token_hash=$2 AND expires_at>now()
`, project, digest(value)).Scan(&id)
if errors.Is(err, sql.ErrNoRows) {
return "", ErrInvalidSession
}
return id, err
}