Files
website/server/src/model/userIdentities/userIdentities.db.js
wtclaude 5e398d8acd feat(auth): sign in with Steam (MODULE_API 1.17.0)
Steam is a built-in provider beside Google and Discord (STEAM_SIGNIN.md,
D392-D396). OpenID 2.0: the assertion is verified server to server with
check_authentication, return_to must match byte for byte, and the claimed id
must be a 17-digit SteamID64. A Steam Web API key is required to enable it
(D394); persona name and avatar are stored on the identity and refreshed at
each sign-in, and a failed lookup never blocks one. Steam never signs anybody
up (D395), on the web or the mobile bridge. Several Steam accounts may be
linked; DELETE /auth/me/account/identities/:provider takes ?id= to unlink one.

MODULE_API 1.17.0: ctx.identities.list, api.onIdentityLinking (refuse-only,
fails closed, module text never shown) and api.onIdentityLinked. No unlink
hook (D393). Schema additive; OpenAPI, routes and engagement manifests
regenerated with no modules installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-19 14:02:49 -05:00

63 lines
2.4 KiB
JavaScript

const { query } = require('../../utils/db')
// Find the identity row for an external (provider, subject) pair. This is the
// link-only login lookup: no row → no account → login refused.
async function findByProviderSubject(provider, subject) {
const rows = await query(
'SELECT * FROM user_identities WHERE provider = ? AND subject = ? LIMIT 1',
[provider, subject],
)
return rows[0] || null
}
// All identities linked to a given internal user (for the Account page).
async function listForUser(userId) {
return query(
`SELECT id, provider, subject, email, display_name, avatar_url, created_at
FROM user_identities WHERE user_id = ? ORDER BY provider, created_at`,
[userId],
)
}
async function insert({ userId, provider, subject, email = null, displayName = null, avatarUrl = null }) {
const res = await query(
`INSERT INTO user_identities (user_id, provider, subject, email, display_name, avatar_url)
VALUES (?, ?, ?, ?, ?, ?)`,
[userId, provider, subject, email, displayName, avatarUrl],
)
return res.insertId
}
// Refresh the name and avatar a provider reported at sign-in (Steam, D394). Only
// what the provider sent is written: a lookup that failed leaves the last good
// values in place rather than blanking them.
async function updateProfile(id, { displayName, avatarUrl }) {
const sets = []
const vals = []
if (displayName) { sets.push('display_name = ?'); vals.push(displayName) }
if (avatarUrl) { sets.push('avatar_url = ?'); vals.push(avatarUrl) }
if (!sets.length) return 0
const res = await query(`UPDATE user_identities SET ${sets.join(', ')} WHERE id = ?`, [...vals, id])
return Number(res.affectedRows || 0)
}
// Remove a user's link to a provider — every identity of it, or, given an
// identity id, that one only (a user may hold several Steam accounts). Returns
// rows deleted.
async function deleteForUserProvider(userId, provider, identityId = null) {
if (identityId != null) {
const res = await query(
'DELETE FROM user_identities WHERE user_id = ? AND provider = ? AND id = ?',
[userId, provider, identityId],
)
return Number(res.affectedRows || 0)
}
const res = await query(
'DELETE FROM user_identities WHERE user_id = ? AND provider = ?',
[userId, provider],
)
return Number(res.affectedRows || 0)
}
module.exports = { findByProviderSubject, listForUser, insert, updateProfile, deleteForUserProvider }