Public Access
feat(settings): link the Steam account through Steam
A Steam card in the settings links, verifies or unlinks the Steam account through "Sign in through Steam". After the way back the page shows the outcome and refreshes the session, so Core gets the new Steam ID at once. Registration and settings no longer ask for a typed Steam ID.
This commit is contained in:
1 parent
23ab5a91e7
commit
ca75e6a530
13 files changed
+393
-86
No files matched your search
@@ -0,0 +1,26 @@
|
||||
import {apiFetch} from './api.js';
|
||||
|
||||
// Only Steam's own login page; a URL from an API must not send the user anywhere else.
|
||||
export const STEAM_LOGIN_PREFIX = 'https://steamcommunity.com/openid/login?';
|
||||
|
||||
/**
|
||||
* POST /api/users/steam-link, then leaves the page for Steam's login. The caller handles a response that is not ok.
|
||||
*
|
||||
* @returns the response; when it is ok the browser is already on its way to Steam
|
||||
*/
|
||||
export async function startSteamLink() {
|
||||
const response = await apiFetch(`${import.meta.env.VITE_API_URL}/api/users/steam-link`, {method: 'POST'});
|
||||
if (!response.ok) return response;
|
||||
const {redirectUrl} = await response.json();
|
||||
if (typeof redirectUrl !== 'string' || !redirectUrl.startsWith(STEAM_LOGIN_PREFIX)) {
|
||||
throw new Error('Auth sent no Steam login URL');
|
||||
}
|
||||
window.location.assign(redirectUrl);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/users/steam-link. Auth sets a new access cookie without the Steam ID.
|
||||
*/
|
||||
export const unlinkSteam = () =>
|
||||
apiFetch(`${import.meta.env.VITE_API_URL}/api/users/steam-link`, {method: 'DELETE'});
|
||||
@@ -15,13 +15,6 @@ const inviteCodeField = z
|
||||
.string()
|
||||
.min(1, 'VALIDATION.INVITE_CODE_REQUIRED');
|
||||
|
||||
// A SteamID64 has 17 digits. Keep it a string: a JS number loses precision above 2^53.
|
||||
const steamIdField = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'VALIDATION.STEAM_ID_REQUIRED')
|
||||
.regex(/^\d{17}$/, 'VALIDATION.STEAM_ID_INVALID');
|
||||
|
||||
// Same rule as the Auth service: 3-24 letters, digits, space, _ . - and no space at the ends.
|
||||
// An empty field means "no display name" and is sent as null.
|
||||
const displayNameField = z
|
||||
@@ -39,7 +32,6 @@ export const registrationSchema = z
|
||||
password: passwordField,
|
||||
passwordConfirmation: z.string().min(1, 'VALIDATION.PASSWORD_CONFIRMATION_REQUIRED'),
|
||||
inviteCode: inviteCodeField,
|
||||
steamId: steamIdField,
|
||||
displayName: displayNameField,
|
||||
})
|
||||
.refine((d) => d.password === d.passwordConfirmation, {
|
||||
@@ -50,6 +42,5 @@ export const registrationSchema = z
|
||||
export const settingsSchema = z.object({
|
||||
useDarkTheme: z.boolean(),
|
||||
language: z.enum(['en', 'de'], {message: 'VALIDATION.LANGUAGE_INVALID'}),
|
||||
steamId: steamIdField,
|
||||
displayName: displayNameField,
|
||||
});
|
||||
@@ -1,36 +1,20 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {registrationSchema, settingsSchema} from './validation.js';
|
||||
|
||||
const settings = (steamId) => ({useDarkTheme: true, language: 'de', steamId});
|
||||
const settings = () => ({useDarkTheme: true, language: 'de'});
|
||||
|
||||
describe('steamId validation', () => {
|
||||
it('keeps a SteamID64 as the exact string', () => {
|
||||
// Number('76561198005679788') is 76561198005679790
|
||||
expect(settingsSchema.parse(settings('76561198005679788')).steamId).toBe('76561198005679788');
|
||||
});
|
||||
|
||||
it('trims whitespace', () => {
|
||||
expect(settingsSchema.parse(settings(' 76561198005679788 ')).steamId).toBe('76561198005679788');
|
||||
});
|
||||
|
||||
it.each(['', '12345', '7656119800567978x', '765611980056797880'])('rejects %j', (steamId) => {
|
||||
expect(settingsSchema.safeParse(settings(steamId)).success).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the exact string on registration', () => {
|
||||
const result = registrationSchema.parse({
|
||||
email: 'a@b.de',
|
||||
password: 'password1',
|
||||
passwordConfirmation: 'password1',
|
||||
inviteCode: 'code',
|
||||
describe('Steam ID', () => {
|
||||
it('is no form field any more: it comes only from "Sign in through Steam"', () => {
|
||||
expect(settingsSchema.parse({...settings(), steamId: '76561198005679788'})).not.toHaveProperty('steamId');
|
||||
expect(registrationSchema.parse({
|
||||
email: 'a@b.de', password: 'password1', passwordConfirmation: 'password1', inviteCode: 'code',
|
||||
steamId: '76561198005679788',
|
||||
});
|
||||
expect(result.steamId).toBe('76561198005679788');
|
||||
})).not.toHaveProperty('steamId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayName validation', () => {
|
||||
const withName = (displayName) => ({...settings('76561198005679788'), displayName});
|
||||
const withName = (displayName) => ({...settings(), displayName});
|
||||
|
||||
it('turns an empty field into null (no display name)', () => {
|
||||
expect(settingsSchema.parse(withName(' ')).displayName).toBeNull();
|
||||
|
||||
Reference in new issue
Block a user