mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
feat(oidc): use the picture claim as avatar when none is uploaded
When a user signs in via OIDC and hasn't uploaded a custom avatar, their `picture` claim is now used as their avatar. The users.avatar column holds either an uploaded file name or an absolute https URL from the claim, and a single resolver on each side (server avatarUrl, client avatarSrc) renders both. An uploaded avatar always wins and is never overwritten; the picture refreshes on each login otherwise. Only https URLs are stored, matching the image CSP. All the scattered /uploads/avatars/ builders now go through the resolvers, which also fixes collection member avatars that were rendering a bare file name. Discussion #1399
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Unit tests for avatarUrl — AVATAR-URL-001 through AVATAR-URL-003.
|
||||
* The stored value is either an uploaded file name or an absolute https URL from
|
||||
* an OIDC `picture` claim (#1399); the helper resolves both to a renderable src.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { avatarUrl } from '../../../src/services/avatarUrl';
|
||||
|
||||
describe('avatarUrl', () => {
|
||||
it('AVATAR-URL-001: prefixes an uploaded file name with the avatars path', () => {
|
||||
expect(avatarUrl({ avatar: 'abc123.jpg' })).toBe('/uploads/avatars/abc123.jpg');
|
||||
});
|
||||
|
||||
it('AVATAR-URL-002: passes an absolute https URL (OIDC picture) through untouched', () => {
|
||||
expect(avatarUrl({ avatar: 'https://idp.example.com/u/pic.png' })).toBe(
|
||||
'https://idp.example.com/u/pic.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('AVATAR-URL-003: returns null when no avatar is set', () => {
|
||||
expect(avatarUrl({ avatar: null })).toBeNull();
|
||||
expect(avatarUrl({ avatar: undefined })).toBeNull();
|
||||
expect(avatarUrl({})).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -464,6 +464,63 @@ describe('findOrCreateUser', () => {
|
||||
const token = testDb.prepare("SELECT used_count FROM invite_tokens WHERE token = 'tok-full'").get() as any;
|
||||
expect(token.used_count).toBe(1);
|
||||
});
|
||||
|
||||
// ── OIDC picture claim → avatar (#1399) ──────────────────────────────────
|
||||
|
||||
it('OIDC-SVC-040: new user stores the https picture claim as their avatar', () => {
|
||||
const result = findOrCreateUser(
|
||||
{ sub: 'sub-pic-1', email: 'pic1@example.com', name: 'Pic One', picture: 'https://idp.example.com/u/pic1.png' },
|
||||
MOCK_CONFIG
|
||||
);
|
||||
expect('user' in result).toBe(true);
|
||||
const row = testDb.prepare("SELECT avatar FROM users WHERE email = 'pic1@example.com'").get() as any;
|
||||
expect(row.avatar).toBe('https://idp.example.com/u/pic1.png');
|
||||
});
|
||||
|
||||
it('OIDC-SVC-041: new user with a non-https picture claim stores no avatar', () => {
|
||||
findOrCreateUser(
|
||||
{ sub: 'sub-pic-2', email: 'pic2@example.com', name: 'Pic Two', picture: 'http://idp.example.com/u/pic2.png' },
|
||||
MOCK_CONFIG
|
||||
);
|
||||
const row = testDb.prepare("SELECT avatar FROM users WHERE email = 'pic2@example.com'").get() as any;
|
||||
expect(row.avatar).toBeNull();
|
||||
});
|
||||
|
||||
it('OIDC-SVC-042: existing user with no avatar gets the OIDC picture', () => {
|
||||
const { user } = createUser(testDb, { email: 'pic3@example.com' });
|
||||
testDb.prepare('UPDATE users SET oidc_sub = ?, oidc_issuer = ?, avatar = NULL WHERE id = ?')
|
||||
.run('sub-pic-3', MOCK_CONFIG.issuer, user.id);
|
||||
findOrCreateUser(
|
||||
{ sub: 'sub-pic-3', email: 'pic3@example.com', name: 'Pic Three', picture: 'https://idp.example.com/u/pic3.png' },
|
||||
MOCK_CONFIG
|
||||
);
|
||||
const row = testDb.prepare('SELECT avatar FROM users WHERE id = ?').get(user.id) as any;
|
||||
expect(row.avatar).toBe('https://idp.example.com/u/pic3.png');
|
||||
});
|
||||
|
||||
it('OIDC-SVC-043: a custom uploaded avatar is never overwritten by the OIDC picture', () => {
|
||||
const { user } = createUser(testDb, { email: 'pic4@example.com' });
|
||||
testDb.prepare('UPDATE users SET oidc_sub = ?, oidc_issuer = ?, avatar = ? WHERE id = ?')
|
||||
.run('sub-pic-4', MOCK_CONFIG.issuer, 'uploaded-abc.jpg', user.id);
|
||||
findOrCreateUser(
|
||||
{ sub: 'sub-pic-4', email: 'pic4@example.com', name: 'Pic Four', picture: 'https://idp.example.com/u/pic4.png' },
|
||||
MOCK_CONFIG
|
||||
);
|
||||
const row = testDb.prepare('SELECT avatar FROM users WHERE id = ?').get(user.id) as any;
|
||||
expect(row.avatar).toBe('uploaded-abc.jpg');
|
||||
});
|
||||
|
||||
it('OIDC-SVC-044: a previously stored OIDC picture URL is refreshed on next login', () => {
|
||||
const { user } = createUser(testDb, { email: 'pic5@example.com' });
|
||||
testDb.prepare('UPDATE users SET oidc_sub = ?, oidc_issuer = ?, avatar = ? WHERE id = ?')
|
||||
.run('sub-pic-5', MOCK_CONFIG.issuer, 'https://idp.example.com/u/old.png', user.id);
|
||||
findOrCreateUser(
|
||||
{ sub: 'sub-pic-5', email: 'pic5@example.com', name: 'Pic Five', picture: 'https://idp.example.com/u/new.png' },
|
||||
MOCK_CONFIG
|
||||
);
|
||||
const row = testDb.prepare('SELECT avatar FROM users WHERE id = ?').get(user.id) as any;
|
||||
expect(row.avatar).toBe('https://idp.example.com/u/new.png');
|
||||
});
|
||||
});
|
||||
|
||||
// ── exchangeCodeForToken ──────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user