211 lines
6.7 KiB
TypeScript
211 lines
6.7 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;
|
|
}
|
|
|
|
interface IReq_AdminValidateIdentity {
|
|
method: 'adminValidateIdentity';
|
|
request: {
|
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
|
};
|
|
response: {
|
|
valid: boolean;
|
|
reason?: string;
|
|
};
|
|
}
|
|
|
|
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,
|
|
},
|
|
};
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<IReq_AdminValidateIdentity>('adminValidateIdentity', async (dataArg) => {
|
|
const valid = await this.adminIdentityGuard.exec(dataArg).catch(() => false);
|
|
return {
|
|
valid,
|
|
reason: valid ? undefined : 'identity is not valid',
|
|
};
|
|
}),
|
|
);
|
|
}
|
|
|
|
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) => {
|
|
try {
|
|
const jwt = dataArg.identity?.jwt;
|
|
if (!jwt) {
|
|
return false;
|
|
}
|
|
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
|
const expired = jwtData.expiresAt < Date.now();
|
|
return (
|
|
jwtData.status === 'loggedIn' &&
|
|
!expired &&
|
|
dataArg.identity.expiresAt === jwtData.expiresAt &&
|
|
dataArg.identity.userId === jwtData.userId
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
{
|
|
failedHint: 'identity is not valid.',
|
|
name: 'validIdentityGuard',
|
|
},
|
|
);
|
|
|
|
public adminIdentityGuard = new plugins.smartguard.Guard<{
|
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
|
}>(
|
|
async (dataArg) => {
|
|
const validIdentity = await this.validIdentityGuard.exec(dataArg);
|
|
if (!validIdentity) {
|
|
return false;
|
|
}
|
|
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';
|
|
},
|
|
{
|
|
failedHint: 'identity is not valid or user is not admin.',
|
|
name: 'adminIdentityGuard',
|
|
},
|
|
);
|
|
|
|
public adminOrClusterIdentityGuard = new plugins.smartguard.Guard<{
|
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
|
}>(
|
|
async (dataArg) => {
|
|
const validIdentity = await this.validIdentityGuard.exec(dataArg);
|
|
if (!validIdentity) {
|
|
return false;
|
|
}
|
|
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: 'identity is not valid or user is not admin or cluster.',
|
|
name: 'adminOrClusterIdentityGuard',
|
|
},
|
|
);
|
|
}
|