f40ef6b7c0
Align Cloudly with the current typedserver, smartconfig, smartstate, and Docker tooling releases so builds and Docker output stay compatible with the upgraded stack.
192 lines
6.5 KiB
TypeScript
192 lines
6.5 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
|
|
import type { Cloudly } from '../classes.cloudly.js';
|
|
import { logger } from '../logger.js';
|
|
import { Authorization } from './classes.authorization.js';
|
|
import { User } from './classes.user.js';
|
|
|
|
export interface IJwtData {
|
|
userId: string;
|
|
status: 'loggedIn' | 'loggedOut';
|
|
expiresAt: number;
|
|
}
|
|
|
|
export class CloudlyAuthManager {
|
|
cloudlyRef: Cloudly;
|
|
public get db() {
|
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
|
}
|
|
public CUser = plugins.smartdata.setDefaultManagerForDoc(this, User);
|
|
public CAuthorization = plugins.smartdata.setDefaultManagerForDoc(this, Authorization);
|
|
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
|
|
|
|
constructor(cloudlyRef: Cloudly) {
|
|
this.cloudlyRef = cloudlyRef;
|
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
}
|
|
|
|
public async createNewSecureToken() {
|
|
return plugins.smartunique.uniSimple('secureToken', 64);
|
|
}
|
|
|
|
public async start() {
|
|
// lets setup the smartjwtInstance
|
|
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
|
|
await this.smartjwtInstance.init();
|
|
const kvStore = await this.cloudlyRef.config.appData.getKvStore();
|
|
|
|
const existingJwtKeys: plugins.tsclass.network.IJwtKeypair = (await kvStore.readKey(
|
|
'jwtKeypair',
|
|
)) as plugins.tsclass.network.IJwtKeypair;
|
|
|
|
if (!existingJwtKeys) {
|
|
await this.smartjwtInstance.createNewKeyPair();
|
|
const newJwtKeys = this.smartjwtInstance.getKeyPairAsJson();
|
|
await kvStore.writeKey('jwtKeypair', newJwtKeys);
|
|
} else {
|
|
this.smartjwtInstance.setKeyPairAsJson(existingJwtKeys);
|
|
}
|
|
|
|
await this.bootstrapInitialAdmin();
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.admin.IReq_Admin_LoginWithUsernameAndPassword>(
|
|
'adminLoginWithUsernameAndPassword',
|
|
async (dataArg) => {
|
|
let jwt: string;
|
|
let expiresAtTimestamp: number = Date.now() + 3600 * 1000 * 24 * 7;
|
|
const user = await User.findUserByUsernameAndPassword(dataArg.username, dataArg.password);
|
|
if (!user) {
|
|
logger.log('warn', 'login failed');
|
|
throw new plugins.typedrequest.TypedResponseError('login failed');
|
|
} else {
|
|
jwt = await this.smartjwtInstance.createJWT({
|
|
userId: user.id,
|
|
status: 'loggedIn',
|
|
expiresAt: expiresAtTimestamp,
|
|
});
|
|
logger.log('success', 'login successful');
|
|
}
|
|
return {
|
|
identity: {
|
|
jwt,
|
|
userId: user.id,
|
|
name: user.data.username || user.id,
|
|
expiresAt: expiresAtTimestamp,
|
|
role: user.data.role,
|
|
type: user.data.type,
|
|
},
|
|
};
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
private async bootstrapInitialAdmin() {
|
|
const users = await this.CUser.getInstances({});
|
|
const hasHumanUser = users.some((userArg) => userArg.data?.type === 'human');
|
|
if (hasHumanUser) {
|
|
return;
|
|
}
|
|
|
|
const adminAccount = this.cloudlyRef.config.data.servezoneAdminaccount;
|
|
if (!adminAccount) {
|
|
throw new Error('SERVEZONE_ADMINACCOUNT is required for first-run Cloudly bootstrap');
|
|
}
|
|
|
|
const separatorIndex = adminAccount.indexOf(':');
|
|
if (separatorIndex <= 0 || separatorIndex === adminAccount.length - 1) {
|
|
throw new Error('SERVEZONE_ADMINACCOUNT must use username:password format');
|
|
}
|
|
|
|
const username = adminAccount.slice(0, separatorIndex).trim();
|
|
const password = adminAccount.slice(separatorIndex + 1);
|
|
if (!username || !password) {
|
|
throw new Error('SERVEZONE_ADMINACCOUNT must include a non-empty username and password');
|
|
}
|
|
|
|
const user = new this.CUser({
|
|
id: await this.CUser.getNewId(),
|
|
data: {
|
|
type: 'human',
|
|
username,
|
|
password,
|
|
role: 'admin',
|
|
},
|
|
});
|
|
await user.save();
|
|
logger.log('success', `created initial admin user ${username}`);
|
|
}
|
|
|
|
public async stop() {}
|
|
|
|
public validIdentityGuard = new plugins.smartguard.Guard<{
|
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
|
}>(
|
|
async (dataArg) => {
|
|
const jwt = dataArg.identity.jwt;
|
|
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
|
const expired = jwtData.expiresAt < Date.now();
|
|
plugins.smartexpect
|
|
.expect(jwtData.status)
|
|
.setFailMessage('user not logged in')
|
|
.toEqual('loggedIn');
|
|
plugins.smartexpect.expect(expired).setFailMessage(`jwt expired`).toBeFalse();
|
|
plugins.smartexpect
|
|
.expect(dataArg.identity.expiresAt)
|
|
.setFailMessage(
|
|
`expiresAt >>identity valid until:${dataArg.identity.expiresAt}, but jwt says: ${jwtData.expiresAt}<< has been tampered with`,
|
|
)
|
|
.toEqual(jwtData.expiresAt);
|
|
plugins.smartexpect
|
|
.expect(dataArg.identity.userId)
|
|
.setFailMessage('userId has been tampered with')
|
|
.toEqual(jwtData.userId);
|
|
if (expired) {
|
|
throw new Error('identity is expired');
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
failedHint: 'identity is not valid.',
|
|
name: 'validIdentityGuard',
|
|
},
|
|
);
|
|
|
|
public adminIdentityGuard = new plugins.smartguard.Guard<{
|
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
|
}>(
|
|
async (dataArg) => {
|
|
await plugins.smartguard.passGuardsOrReject(dataArg, [this.validIdentityGuard]);
|
|
const jwt = dataArg.identity.jwt;
|
|
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
|
const user = await this.CUser.getInstance({ id: jwtData.userId });
|
|
const isAdminBool = user.data.role === 'admin';
|
|
console.log(`user is admin: ${isAdminBool}`);
|
|
return isAdminBool;
|
|
},
|
|
{
|
|
failedHint: 'user is not admin.',
|
|
name: 'adminIdentityGuard',
|
|
},
|
|
);
|
|
|
|
public adminOrClusterIdentityGuard = new plugins.smartguard.Guard<{
|
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
|
}>(
|
|
async (dataArg) => {
|
|
await plugins.smartguard.passGuardsOrReject(dataArg, [this.validIdentityGuard]);
|
|
const jwt = dataArg.identity.jwt;
|
|
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
|
const user = await this.CUser.getInstance({ id: jwtData.userId });
|
|
return user.data.role === 'admin' || user.data.role === 'cluster';
|
|
},
|
|
{
|
|
failedHint: 'user is not admin or cluster.',
|
|
name: 'adminOrClusterIdentityGuard',
|
|
},
|
|
);
|
|
}
|