Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/database/postgres/passport.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// DEVELOPMENT: use scripts/auth/gen-sso.py to generate some test data67import { PassportStrategyDB } from "@cocalc/database/settings/auth-sso-types";8import {9getPassportsCached,10setPassportsCached11} from "@cocalc/database/settings/server-settings";12import { to_json } from "@cocalc/util/misc";13import { CB } from "@cocalc/util/types/database";14import {15set_account_info_if_different,16set_account_info_if_not_set,17set_email_address_verified18} from "./account-queries";19import {20CreatePassportOpts,21PassportExistsOpts,22PostgreSQL,23UpdateAccountInfoAndPassportOpts24} from "./types";2526export async function set_passport_settings(27db: PostgreSQL,28opts: PassportStrategyDB & { cb?: CB }29): Promise<void> {30const { strategy, conf, info } = opts;31let err = null;32try {33await db.async_query({34query: "INSERT INTO passport_settings",35values: {36"strategy::TEXT ": strategy,37"conf ::JSONB": conf,38"info ::JSONB": info,39},40conflict: "strategy",41});42} catch (err) {43err = err;44}45if (typeof opts.cb === "function") {46opts.cb(err);47}48}4950export async function get_passport_settings(51db: PostgreSQL,52opts: { strategy: string; cb?: (data: object) => void }53): Promise<any> {54const { rows } = await db.async_query({55query: "SELECT conf, info FROM passport_settings",56where: { "strategy = $::TEXT": opts.strategy },57});58if (typeof opts.cb === "function") {59opts.cb(rows[0]);60}61return rows[0];62}6364export async function get_all_passport_settings(65db: PostgreSQL66): Promise<PassportStrategyDB[]> {67return (68await db.async_query<PassportStrategyDB>({69query: "SELECT strategy, conf, info FROM passport_settings",70})71).rows;72}7374export async function get_all_passport_settings_cached(75db: PostgreSQL76): Promise<PassportStrategyDB[]> {77const passports = getPassportsCached();78if (passports != null) {79return passports;80}81const res = await get_all_passport_settings(db);82setPassportsCached(res);83return res;84}8586// Passports -- accounts linked to Google/Dropbox/Facebook/Github, etc.87// The Schema is slightly redundant, but indexed properly:88// {passports:['google-id', 'facebook-id'], passport_profiles:{'google-id':'...', 'facebook-id':'...'}}8990export function _passport_key(opts) {91const { strategy, id } = opts;92// note: strategy is *our* name of the strategy in the DB, not it's type string!93if (typeof strategy !== "string") {94throw new Error("_passport_key: strategy must be defined");95}96if (typeof id !== "string") {97throw new Error("_passport_key: id must be defined");98}99100return `${strategy}-${id}`;101}102103export async function create_passport(104db: PostgreSQL,105opts: CreatePassportOpts106): Promise<void> {107const dbg = db._dbg("create_passport");108dbg({ id: opts.id, strategy: opts.strategy, profile: to_json(opts.profile) });109110try {111dbg("setting the passport for the account");112await db.async_query({113query: "UPDATE accounts",114jsonb_set: {115passports: { [_passport_key(opts)]: opts.profile },116},117where: {118"account_id = $::UUID": opts.account_id,119},120});121122dbg(123`setting other account info ${opts.account_id}: ${opts.email_address}, ${opts.first_name}, ${opts.last_name}`124);125await set_account_info_if_not_set({126db: db,127account_id: opts.account_id,128email_address: opts.email_address,129first_name: opts.first_name,130last_name: opts.last_name,131});132// we still record that email address as being verified133if (opts.email_address != null) {134await set_email_address_verified({135db,136account_id: opts.account_id,137email_address: opts.email_address,138});139}140opts.cb?.(undefined); // all good141} catch (err) {142if (opts.cb != null) {143opts.cb(err);144} else {145throw err;146}147}148}149150export async function passport_exists(151db: PostgreSQL,152opts: PassportExistsOpts153): Promise<string | undefined> {154try {155const result = await db.async_query({156query: "SELECT account_id FROM accounts",157where: [158// this uses the corresponding index to only scan a subset of all accounts!159"passports IS NOT NULL",160{ "(passports->>$::TEXT) IS NOT NULL": _passport_key(opts) },161],162});163const account_id = result?.rows[0]?.account_id;164if (opts.cb != null) {165opts.cb(null, account_id);166} else {167return account_id;168}169} catch (err) {170if (opts.cb != null) {171opts.cb(err);172} else {173throw err;174}175}176}177178export async function update_account_and_passport(179db: PostgreSQL,180opts: UpdateAccountInfoAndPassportOpts181) {182// we deliberately do not update the email address, because if the SSO183// strategy sends a different one, this would break the "link".184// rather, if the email (and hence most likely the email address) changes on the185// SSO side, this would equal to creating a new account.186const dbg = db._dbg("update_account_and_passport");187dbg(188`updating account info ${to_json({189first_name: opts.first_name,190last_name: opts.last_name,191})}`192);193await set_account_info_if_different({194db: db,195account_id: opts.account_id,196first_name: opts.first_name,197last_name: opts.last_name,198});199const key = _passport_key(opts);200dbg(`updating passport ${to_json({ key, profile: opts.profile })}`);201await db.async_query({202query: "UPDATE accounts",203jsonb_set: {204passports: { [key]: opts.profile },205},206where: {207"account_id = $::UUID": opts.account_id,208},209});210}211212213