feat(auth): store Subsonic API keys hashed; a new key is shown once (M462 #4983)
test-web / test (push) Successful in 2m4s
test-go / test (push) Successful in 2m23s
test-go / integration (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 6m29s
release / Build + push container image (push) Successful in 29s
release / Verify release artifacts (tag releases only) (push) Skipped
test-web / test (push) Successful in 2m4s
test-go / test (push) Successful in 2m23s
test-go / integration (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 6m29s
release / Build + push container image (push) Successful in 29s
release / Verify release artifacts (tag releases only) (push) Skipped
users.api_token held each user's apiKey in plaintext and was looked up by equality, so a leaked row or backup handed out working keys. Migration 0063 replaces it with api_token_hash (sha256, hex), computed in place from the existing keys so every Subsonic client keeps working. The key can no longer be read back: GET /api/me/api-token is gone, and POST returns the new key once. Settings shows it right after Regenerate with a copy button and a "won't be shown again" note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,6 @@ vi.mock('$lib/api/me', () => ({
|
||||
changePassword: vi.fn(),
|
||||
// Default to a resolved value so the page's $effect doesn't crash
|
||||
// on `.then()` of undefined when individual tests don't override.
|
||||
getAPIToken: vi.fn().mockResolvedValue({ api_token: '' }),
|
||||
regenerateAPIToken: vi.fn()
|
||||
}));
|
||||
|
||||
@@ -46,7 +45,6 @@ import {
|
||||
import {
|
||||
updateProfile,
|
||||
changePassword,
|
||||
getAPIToken,
|
||||
regenerateAPIToken
|
||||
} from '$lib/api/me';
|
||||
|
||||
@@ -133,7 +131,6 @@ function setupPage() {
|
||||
);
|
||||
(createTokenMutation as ReturnType<typeof vi.fn>).mockReturnValue(mockMutationStore());
|
||||
(createEnabledMutation as ReturnType<typeof vi.fn>).mockReturnValue(mockMutationStore());
|
||||
(getAPIToken as ReturnType<typeof vi.fn>).mockResolvedValue({ api_token: 'tok_abc123' });
|
||||
}
|
||||
|
||||
describe('Settings page — Profile card', () => {
|
||||
@@ -350,5 +347,17 @@ describe('Settings page — API Token card', () => {
|
||||
);
|
||||
await fireEvent.click(screen.getByRole('button', { name: /click again to confirm/i }));
|
||||
await waitFor(() => expect(regenerateAPIToken).toHaveBeenCalled());
|
||||
// The new key is shown once, with a way to copy it.
|
||||
expect(await screen.findByText('new_tok_xyz')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /^copy$/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('no token is shown or fetched before Regenerate', async () => {
|
||||
setupPage();
|
||||
render(SettingsPage);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('button', { name: /regenerate/i })).toBeInTheDocument()
|
||||
);
|
||||
expect(screen.queryByRole('button', { name: /^copy$/i })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user