agilentics / boiler
"""Google sign-in: turning a Google-verified email into an account here.

The identity half of "Sign in with Google", kept transport-free like the rest of
app/core: the endpoint (app/endpoints/oauth.py) runs the OAuth redirect dance and
verifies the ID token; this module takes the resulting (google_sub, email, name)
and resolves it to a User, creating one - with an organisation - on first sign-in.

Three cases, in order:

  1. a user already linked to this Google subject -> return them
  2. a user with this email but no Google link    -> link and return. Safe
     because Google has verified the address, so this proves the same person
     rather than being a way to seize somebody's account.
  3. nobody yet                                    -> create the account and
     provision a tenant with them as owner. No password: an account that signs
     in only through a provider has no hash, which is what the nullable
     password_hash column exists to represent.
"""
import logging

from db.db_declarations import User

from . import auth as auth_core
from . import orgs as orgs_core

logger = logging.getLogger(__name__)


def find_or_create_google_user(session, google_sub: str, email: str,
                               name: str | None = None) -> User:
    email = (email or "").strip().lower()
    if not google_sub or "@" not in email:
        # The endpoint verifies the ID token before calling this, so a malformed
        # pair here is a programming error, not a user-facing one.
        raise ValueError("A Google sign-in needs a subject id and a verified email.")

    # 1. Already linked to this Google identity.
    user = session.query(User).filter(User.google_sub == google_sub).one_or_none()
    if user is not None:
        return user

    # 2. An existing account with this address - link it.
    user = session.query(User).filter(User.email == email).one_or_none()
    if user is not None:
        user.google_sub = google_sub
        session.commit()
        logger.info("Linked Google sign-in to existing account %s.", user.email)
        return user

    # 3. Brand new: the account, then a tenant with them as owner.
    user = auth_core.create_user(session, email, None, name=name)
    user.google_sub = google_sub

    # A Google account carries no organisation name, so the tenant is named
    # after the person. provision falls back to their address on its own.
    org = orgs_core.provision(session, user, name)
    session.commit()
    logger.info("Created account %s from a Google sign-in (new org %r).",
                user.email, org.slug)
    return user