mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-27 17:20:12 +00:00
`user.requires_password_change` shipped with the team columns but nothing enforced it and nothing ever cleared it, so a provisioned seat kept its administrator-issued password indefinitely and `reissueCredential`'s "already activated" 409 was unreachable. Adds the fourth clause to `assertVerifiedAccount`, the only place a verification gate may live -- WebDAV builds its own actor and calls that function directly, so a second implementation would bypass it the way the phone and card gates once were bypassed. A gate that refuses everything also refuses the endpoint that clears it, so `/user-protected/change-password` opts out with `allowUnconfirmed`. That widens the route: an account pending email, phone or card verification can now change its password, which it could not before. The caller is authenticated and proves the current password, so this is benign, but it is a behaviour change to a shared route. Also here, because the gate is worthless without them: - change-password and the recovery-token path clear the flag, and record an `activate` entry when the account is a seat. - Reset takes a live account back with a fresh credential, capped at 20 per day and audited as `reset_member_password` with no credential in the row. Re-issue is audited the same way; it stays closed once a seat has chosen its own password. - An issued credential expires after 24h (new `temp_password_expires_at` column, three dialects) and login refuses it after that, so an unused reset dies instead of becoming a standing credential. - 2FA is untouched by a reset, so a reset alone is not takeover.
86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
/*
|
|
* Copyright (C) 2024-present Puter Technologies Inc.
|
|
*
|
|
* This file is part of Puter.
|
|
*
|
|
* Puter is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
generateTemporaryPassword,
|
|
isTemporaryPasswordExpired,
|
|
TEMP_PASSWORD_TTL_SECONDS,
|
|
temporaryPasswordExpiry,
|
|
} from './temporaryPassword.js';
|
|
|
|
describe('generateTemporaryPassword', () => {
|
|
it('avoids the glyphs a human would mistype', () => {
|
|
const joined = Array.from({ length: 200 }, () =>
|
|
generateTemporaryPassword(),
|
|
).join('');
|
|
expect(joined).not.toMatch(/[0O1lI]/u);
|
|
});
|
|
|
|
it('never repeats a credential', () => {
|
|
const seen = new Set(
|
|
Array.from({ length: 200 }, () => generateTemporaryPassword()),
|
|
);
|
|
expect(seen.size).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('isTemporaryPasswordExpired', () => {
|
|
const now = 1_800_000_000_000;
|
|
|
|
it('treats a password the account chose itself as never expiring', () => {
|
|
expect(isTemporaryPasswordExpired({}, now)).toBe(false);
|
|
expect(
|
|
isTemporaryPasswordExpired({ temp_password_expires_at: null }, now),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('accepts an unexpired credential', () => {
|
|
const expiry = temporaryPasswordExpiry(now);
|
|
expect(
|
|
isTemporaryPasswordExpired(
|
|
{ temp_password_expires_at: expiry },
|
|
now,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('rejects one issued more than the TTL ago', () => {
|
|
const expiry = temporaryPasswordExpiry(now);
|
|
const later = now + (TEMP_PASSWORD_TTL_SECONDS + 1) * 1000;
|
|
expect(
|
|
isTemporaryPasswordExpired(
|
|
{ temp_password_expires_at: expiry },
|
|
later,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('reads the string postgres returns for a bigint column', () => {
|
|
const expiry = String(temporaryPasswordExpiry(now));
|
|
const later = now + (TEMP_PASSWORD_TTL_SECONDS + 1) * 1000;
|
|
expect(
|
|
isTemporaryPasswordExpired(
|
|
{ temp_password_expires_at: expiry },
|
|
later,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
});
|