/*
* 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 .
*/
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 {
// 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;
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 {
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',
);
}
}