mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-08 06:54:18 +00:00
* refactor(auth): one signup path for form, temp, OIDC and team accounts Signup steps (validate hook, phone/card forcing, bonus verdict, provisioning, signup events) move from AuthController and OIDCService into SignupService. Username rules, availability and generation move to util/username.ts, and user-row + group + default-folder creation to provisionUser, which TeamService and DefaultUserService now share. Behaviour fixes: a provisioned team seat joins default_user_group like every other account, and generated temp usernames skip names whose home path is occupied. * refactor(auth): one web-session start for password and OIDC sign-in startWebSession creates the session with the request's IP, user agent, origin and host and sets the session cookie. OIDC sign-ins used to create their session without that metadata, so they showed up blank in the session list. * refactor(auth): one confirmEmail for the code and link confirmation routes Both routes now re-run the email checks, refuse an address another account confirmed, strip it from other rows, promote the group and push user.email_confirmed to the account's sockets (with the originating socket id on the code route). The link route's full-table UPDATE clearing other accounts' pending change-email requests is dropped: /change_email/confirm already refuses an address a confirmed account owns. * refactor(auth): build window.user once for whoami and sign-in buildUserDetails and scrubSensitive move from the whoami extension into util/userDetails.ts. /whoami calls them; sign-in responses now return the same user-actor payload, scrubbed, so the login copy no longer ships the phone number and a no-reload login gets oidc_only/otp/directories up front. Taskbar resolution is best-effort on both. * refactor(http): userProtected route option replaces AuthController's route walker copy The user-protected gate is materialized from a userProtected RouteOption (implies requireUserActor), so AuthController no longer re-implements the decorator walker to wire it per instance; those routes and the session routes are plain decorators now. AuthRequired also counts requireVerified and userProtected. * refactor(http): internalAuth route option and a shared bodyRecord EventsController and PeerController each checked x-puter-internal-auth by hand with different compares; an internalAuth RouteOption now picks the secret from config and one constant-time gate checks it. ShareController and EventsController share bodyRecord instead of private copies. * fix(fs): cap legacy /delete at 500 paths per request The bulk form resolved, ACL-checked and removed an unbounded paths[] list one by one. It now refuses more than FS_BATCH_WRITE_MAX_ITEMS (500), the cap the batch write routes already use, before touching any entry. * refactor(controllers): user and subdomain writes and reads through their stores AuthController's 2FA setup/enable/disable and recovery-code writes go through UserStore.update instead of raw UPDATEs plus a manual invalidate, and /readdir-subdomains pages SubdomainStore.listByUserId instead of an unbounded SELECT; the response shape is unchanged. * refactor(auth): note SignupService's call-time peers * fix(auth): type userDetails' taskbar deps independently of TaskbarDeps.clients
135 lines
5.3 KiB
TypeScript
135 lines
5.3 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 bcrypt from 'bcrypt';
|
|
import crypto from 'node:crypto';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { PuterService } from '../types.js';
|
|
import type { UserRow } from '../../stores/user/UserStore.js';
|
|
import { provisionUser } from '../../util/userProvisioning.js';
|
|
import { LOCAL_UNLIMITED_USER } from '../../data/subPolicies/localUnlimitedUserPolicy.js';
|
|
import { UNLIMITED_SUBSCRIPTION } from '../metering/consts.js';
|
|
|
|
const USERNAME = 'admin';
|
|
export const ADMIN_GROUP_UID = 'ca342a5e-b13d-4dee-9048-58b11a57cc55';
|
|
const ADMIN_STORAGE_BYTES = 10 * 1024 * 1024 * 1024;
|
|
|
|
/**
|
|
* Bootstraps the `admin` user on first boot for self-hosted deployments.
|
|
*
|
|
* If no admin exists, creates one with a random 8-char hex password, places
|
|
* them in the admin group, and stashes the plaintext under
|
|
* `metadata.tmp_password` so we can detect on later boots whether the operator
|
|
* has rotated it yet.
|
|
*
|
|
* Each boot where the current password hash still matches the stashed
|
|
* plaintext, the credentials are re-printed to stdout (CI scrapes this line to
|
|
* extract the default password). Once rotation is detected the stash is deleted
|
|
* so the plaintext isn't retained longer than needed.
|
|
*/
|
|
export class DefaultUserService extends PuterService {
|
|
override async onServerStart(): Promise<void> {
|
|
// Dev convenience: grant the bootstrap `admin` user unlimited metering.
|
|
// Gated to env === 'dev' so prod deployments never get a free-usage
|
|
// actor. Registering the policy makes its `'unlimited'` id resolvable
|
|
// (extraPolicies is always in the available set); the resolver returns
|
|
// null for everyone else, so all other users keep their normal tier.
|
|
if (this.config.env === 'dev') {
|
|
this.services.metering.registerPolicy(LOCAL_UNLIMITED_USER);
|
|
this.services.metering.registerSubscriptionResolver((actor) =>
|
|
actor.user?.username === USERNAME
|
|
? UNLIMITED_SUBSCRIPTION
|
|
: null,
|
|
);
|
|
}
|
|
|
|
if (this.config.no_default_user) return;
|
|
let user = await this.stores.user.getByUsername(USERNAME);
|
|
let tmpPassword: string;
|
|
|
|
if (!user) {
|
|
// 16 bytes (128 bits) — the old 4 bytes was only 32 bits,
|
|
// brute-forceable if the box is reachable before the admin
|
|
// rotates the printed bootstrap password.
|
|
tmpPassword = crypto.randomBytes(16).toString('hex');
|
|
user = await this.#createAdminUser(tmpPassword);
|
|
} else {
|
|
const metadata = (user.metadata ?? {}) as Record<string, unknown>;
|
|
const stashed = metadata.tmp_password;
|
|
if (typeof stashed !== 'string' || stashed === '') return;
|
|
tmpPassword = stashed;
|
|
}
|
|
|
|
if (!user.password) return;
|
|
const isDefault = await bcrypt.compare(
|
|
tmpPassword,
|
|
String(user.password),
|
|
);
|
|
if (!isDefault) {
|
|
// Password was rotated — stop retaining the plaintext stash.
|
|
await this.stores.user.updateMetadata(user.id, {
|
|
tmp_password: null,
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.#printCredentials(tmpPassword);
|
|
}
|
|
|
|
async #createAdminUser(tmpPassword: string): Promise<UserRow> {
|
|
const passwordHash = await bcrypt.hash(tmpPassword, 8);
|
|
|
|
const created = await provisionUser(
|
|
{
|
|
db: this.clients.db,
|
|
userStore: this.stores.user,
|
|
groupStore: this.stores.group,
|
|
},
|
|
{
|
|
username: USERNAME,
|
|
uuid: uuidv4(),
|
|
password: passwordHash,
|
|
email: null,
|
|
free_storage: ADMIN_STORAGE_BYTES,
|
|
requires_email_confirmation: false,
|
|
},
|
|
ADMIN_GROUP_UID,
|
|
);
|
|
|
|
await this.stores.user.updateMetadata(created.id, {
|
|
tmp_password: tmpPassword,
|
|
});
|
|
return (await this.stores.user.getById(created.id)) ?? created;
|
|
}
|
|
|
|
#printCredentials(tmpPassword: string): void {
|
|
console.log(`password for admin is: ${tmpPassword}`);
|
|
console.log(
|
|
'\n************************************************************',
|
|
);
|
|
console.log('* Your default login credentials are:');
|
|
console.log('* Username: admin');
|
|
console.log(`* Password: ${tmpPassword}`);
|
|
console.log('* (change the password to remove this message)');
|
|
console.log(
|
|
'************************************************************\n',
|
|
);
|
|
}
|
|
}
|