initial
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@idp.global/idp.global',
|
||||
version: 'x.x.x',
|
||||
description: 'website for lossless.com'
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as plugins from './ffb.plugins.js';
|
||||
|
||||
export const packageDir = plugins.path.join(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'../',
|
||||
);
|
||||
export const distWebDir = plugins.path.join(packageDir, 'dist_serve/');
|
||||
@@ -0,0 +1,14 @@
|
||||
// native scope
|
||||
import * as path from 'path';
|
||||
export { path };
|
||||
|
||||
// @api.global scope
|
||||
import * as typedserver from '@api.global/typedserver';
|
||||
|
||||
export { typedserver };
|
||||
|
||||
// @pushrocks scope
|
||||
import * as qenv from '@push.rocks/qenv';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
|
||||
export { qenv, smartpath };
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import * as plugins from './ffb.plugins.js';
|
||||
import * as paths from './ffb.paths.js';
|
||||
|
||||
export const runCli = async () => {
|
||||
const serviceQenv = new plugins.qenv.Qenv('./', './.nogit', false);
|
||||
const websiteServer = new plugins.typedserver.utilityservers.UtilityWebsiteServer({
|
||||
feedMetadata: null,
|
||||
domain: 'idp.global',
|
||||
serveDir: paths.distWebDir,
|
||||
});
|
||||
await websiteServer.start();
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ApiTokenManager } from './classes.apitokenmanager.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
@plugins.smartdata.Manager(() => {
|
||||
return (this as any).manager;
|
||||
})
|
||||
export class ApiToken extends plugins.smartdata.SmartDataDbDoc<
|
||||
ApiToken,
|
||||
ApiToken,
|
||||
ApiTokenManager
|
||||
> {
|
||||
static clearOldApiTokens() {}
|
||||
|
||||
static clearApiTokensForUserId(userId: string) {}
|
||||
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
|
||||
@plugins.smartdata.unI()
|
||||
public ownerEntityId: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
data: {
|
||||
token: string;
|
||||
scopes: string[];
|
||||
} = {
|
||||
token: null,
|
||||
scopes: null,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Reception } from './classes.reception.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export class ApiTokenManager {
|
||||
public receptionRef: Reception;
|
||||
public get db() {
|
||||
return this.receptionRef.db.smartdataDb;
|
||||
}
|
||||
constructor(receptionRefArg: Reception) {
|
||||
this.receptionRef = receptionRefArg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { BillingPlanManager } from './classes.billingplanmanager.js';
|
||||
import { User } from './classes.user.js';
|
||||
|
||||
/**
|
||||
* a billing plan belongs to a user which can then attribute the billing plan to a organization
|
||||
*/
|
||||
@plugins.smartdata.Manager()
|
||||
export class BillingPlan extends plugins.smartdata.SmartDataDbDoc<
|
||||
BillingPlan,
|
||||
plugins.lointReception.data.IBillingPlan,
|
||||
BillingPlanManager
|
||||
> {
|
||||
// STATIC
|
||||
public static syncForUser(userArg: User) {
|
||||
// TODO sync this for user
|
||||
}
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public id: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.lointReception.data.IBillingPlan['data'] = {
|
||||
type: null,
|
||||
organizationId: null,
|
||||
lastProcessed: null,
|
||||
seats: null,
|
||||
status: null,
|
||||
billingEvents: [],
|
||||
communications: [],
|
||||
nextBilling: null,
|
||||
proEnabled: false,
|
||||
alternativePaymentData: null,
|
||||
paddleData: null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Reception } from './classes.reception.js';
|
||||
import { BillingPlan } from './classes.billingplan.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export class BillingPlanManager {
|
||||
public receptionRef: Reception;
|
||||
public get db() {
|
||||
return this.receptionRef.db.smartdataDb;
|
||||
}
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
public CBillingPlan = plugins.smartdata.setDefaultManagerForDoc(this, BillingPlan);
|
||||
|
||||
constructor(receptionRefArg: Reception) {
|
||||
this.receptionRef = receptionRefArg;
|
||||
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.typedrouter.addTypedHandler(new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_UpdatePaymentMethod>('updatePaymentMethod', async reqDataArg => {
|
||||
const user = await this.receptionRef.userManager.getUserByJwt(reqDataArg.jwtString);
|
||||
const organization = await this.receptionRef.organizationmanager.COrganization.getInstance({
|
||||
id: reqDataArg.orgId,
|
||||
});
|
||||
const userIsAdmin = await organization.checkIfUserIsAdmin(user);
|
||||
if (!userIsAdmin) {
|
||||
throw new plugins.typedrequest.TypedResponseError('user is not an admin for the organization that the billing plan is for');
|
||||
}
|
||||
// ok user is admin
|
||||
const newBillingPlan = new this.CBillingPlan();
|
||||
newBillingPlan.id = plugins.smartunique.shortId();
|
||||
newBillingPlan.data = {
|
||||
type: 'Paddle',
|
||||
proEnabled: false,
|
||||
organizationId: reqDataArg.orgId,
|
||||
status: 'active',
|
||||
seats: 0,
|
||||
alternativePaymentData: null,
|
||||
billingEvents: [],
|
||||
communications: [],
|
||||
lastProcessed: Date.now(),
|
||||
nextBilling: {
|
||||
items: [],
|
||||
method: 'paddle',
|
||||
ontrack: true,
|
||||
selectedBillingDate: Date.now(),
|
||||
},
|
||||
paddleData: {
|
||||
checkoutId: reqDataArg.paddle?.checkoutId
|
||||
}
|
||||
}
|
||||
await newBillingPlan.save();
|
||||
return {
|
||||
billingPlan: {
|
||||
id: newBillingPlan.id,
|
||||
data: {
|
||||
type: newBillingPlan.data.type,
|
||||
organizationId: newBillingPlan.data.organizationId,
|
||||
proEnabled: newBillingPlan.data.proEnabled,
|
||||
nextBilling: newBillingPlan.data.nextBilling,
|
||||
billingEvents: newBillingPlan.data.billingEvents,
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { Reception } from './classes.reception.js';
|
||||
import { logger } from './logging.js';
|
||||
|
||||
export class ReceptionHousekeeping {
|
||||
public receptionRef: Reception;
|
||||
public taskmanager = new plugins.taskbuffer.TaskManager();
|
||||
|
||||
constructor(receptionArg: Reception) {
|
||||
this.receptionRef = receptionArg;
|
||||
|
||||
// lets care about old loginsessions
|
||||
this.taskmanager.addAndScheduleTask(
|
||||
new plugins.taskbuffer.Task({
|
||||
name: 'oldLoginSessions',
|
||||
taskFunction: async () => {
|
||||
logger.log('info', 'running login sessions cleaning task');
|
||||
const oneWeekBeforeTimestamp =
|
||||
Date.now() - plugins.smarttime.getMilliSecondsFromUnits({ weeks: 1 });
|
||||
const oldLoginSessions =
|
||||
await this.receptionRef.loginSessionManager.CLoginSession.getInstances({
|
||||
data: {
|
||||
validUntil: {
|
||||
$lt: oneWeekBeforeTimestamp,
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
for (const loginSession of oldLoginSessions) {
|
||||
await loginSession.delete();
|
||||
}
|
||||
logger.log('info', `Completed deletion of ${oldLoginSessions.length} old loginSessions`);
|
||||
},
|
||||
}),
|
||||
'2 * * * * *'
|
||||
);
|
||||
|
||||
this.taskmanager.start();
|
||||
logger.log('info', 'housekeeping started');
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
this.taskmanager.stop();
|
||||
logger.log('info', 'housekeeping stopped');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { JwtManager } from './classes.jwtmanager.js';
|
||||
|
||||
/**
|
||||
* a User is identified by its username or email.
|
||||
* Both need to be unique and both can be changed.
|
||||
*/
|
||||
@plugins.smartdata.Manager()
|
||||
export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.lointReception.data.IJwt, JwtManager> {
|
||||
// STATIC
|
||||
public static async createJwtForRefreshToken(
|
||||
jwtManagerInstance: JwtManager,
|
||||
refreshTokenArg: string
|
||||
) {
|
||||
const loginSession =
|
||||
await jwtManagerInstance.receptionRef.loginSessionManager.CLoginSession.getLoginSessionByRefreshToken(
|
||||
refreshTokenArg
|
||||
);
|
||||
if (!loginSession) {
|
||||
return null;
|
||||
}
|
||||
const refreshTokenValid = await loginSession.validateRefreshToken(refreshTokenArg);
|
||||
if (!refreshTokenValid) {
|
||||
return null;
|
||||
}
|
||||
const user = await jwtManagerInstance.receptionRef.userManager.CUser.getInstance({
|
||||
id: loginSession.data.userId,
|
||||
});
|
||||
const validUntil = plugins.smarttime.ExtendedDate.fromMillis(
|
||||
Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 1 })
|
||||
);
|
||||
const jwt = new Jwt();
|
||||
jwt.id = plugins.smartunique.shortId();
|
||||
jwt.data = {
|
||||
userId: user.id,
|
||||
validUntil: validUntil.getTime(),
|
||||
refreshEvery: 1000000,
|
||||
refreshFrom: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 0.5 }),
|
||||
refreshToken: await loginSession.getRefreshToken(), // TODO: handle multiple refresh tokens
|
||||
justForLooks: {
|
||||
validUntilIsoString: validUntil.toISOString(),
|
||||
}
|
||||
};
|
||||
|
||||
await jwt.save();
|
||||
|
||||
const jwtString = await jwtManagerInstance.smartjwtInstance.createJWT({
|
||||
id: jwt.id,
|
||||
blocked: null,
|
||||
data: jwt.data,
|
||||
} as plugins.lointReception.data.IJwt);
|
||||
return jwtString;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public blocked: boolean = false;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.lointReception.data.IJwt['data'];
|
||||
|
||||
public async block() {
|
||||
this.blocked = true;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
public async getLoginSession() {
|
||||
const loginSession = await this.manager.receptionRef.loginSessionManager.CLoginSession.getInstance({
|
||||
data: {
|
||||
refreshToken: this.data.refreshToken,
|
||||
}
|
||||
});
|
||||
return loginSession;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { Reception } from './classes.reception.js';
|
||||
import { Jwt } from './classes.jwt.js';
|
||||
|
||||
export class JwtManager {
|
||||
public receptionRef: Reception;
|
||||
public get db() {
|
||||
return this.receptionRef.db.smartdataDb;
|
||||
}
|
||||
|
||||
public smartjwtInstance = new plugins.smartjwt.SmartJwt();
|
||||
public jwtManagerEasyStore: plugins.smartdata.EasyStore<{
|
||||
jwtJsonKeypair: plugins.tsclass.network.IJwtKeypair;
|
||||
}>;
|
||||
public blockedJwtIdList: string[] = [];
|
||||
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
public CJwt = plugins.smartdata.setDefaultManagerForDoc(this, Jwt);
|
||||
|
||||
constructor(receptionRefArg: Reception) {
|
||||
this.receptionRef = receptionRefArg;
|
||||
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.typedrouter.addTypedHandler<plugins.lointReception.request.IReq_RefreshJwt>(
|
||||
new plugins.typedrequest.TypedHandler(
|
||||
'refreshJwt',
|
||||
async (requestArg) => {
|
||||
const resultJwt = await Jwt.createJwtForRefreshToken(this, requestArg.refreshToken);
|
||||
return {
|
||||
status: 'loggedIn',
|
||||
jwt: resultJwt,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_GetPublicKeyForValidation>(
|
||||
'getPublicKeyForValidation',
|
||||
async (requestArg) => {
|
||||
// TODO control backend token
|
||||
return {
|
||||
publicKeyPem: this.smartjwtInstance.getKeyPairAsJson().publicPem,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_PushOrGetJwtIdBlocklist>(
|
||||
'pushOrGetJwtIdBlocklist',
|
||||
async (requestArg) => {
|
||||
// TODO control backend token
|
||||
return {
|
||||
blockedJwtIds: this.blockedJwtIdList
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async pushPublicKeyToClients() {
|
||||
const targetConnections =
|
||||
await this.receptionRef.serviceServer.typedsocket.findAllTargetConnectionsByTag<plugins.lointReception.tags.ITag_LolePubapi>(
|
||||
'lole-reception',
|
||||
{
|
||||
backendToken: '',
|
||||
}
|
||||
);
|
||||
for (const targetConnection of targetConnections) {
|
||||
const pushPublicKeyTr =
|
||||
this.receptionRef.serviceServer.typedsocket.createTypedRequest<plugins.lointReception.request.IReq_PushPublicKeyForValidation>(
|
||||
'pushPublicKeyForValidation',
|
||||
targetConnection
|
||||
);
|
||||
await pushPublicKeyTr.fire({
|
||||
publicKeyPem: this.smartjwtInstance.getKeyPairAsJson().publicPem,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async pushBlockedJwtIdListToClients() {
|
||||
const targetConnections =
|
||||
await this.receptionRef.serviceServer.typedsocket.findAllTargetConnectionsByTag<plugins.lointReception.tags.ITag_LolePubapi>(
|
||||
'lole-reception',
|
||||
{
|
||||
backendToken: '',
|
||||
}
|
||||
);
|
||||
for (const targetConnection of targetConnections) {
|
||||
const pushPublicKeyTr =
|
||||
this.receptionRef.serviceServer.typedsocket.createTypedRequest<plugins.lointReception.request.IReq_PushOrGetJwtIdBlocklist>(
|
||||
'pushOrGetJwtIdBlocklist',
|
||||
targetConnection
|
||||
);
|
||||
await pushPublicKeyTr.fire({
|
||||
blockedJwtIds: this.blockedJwtIdList
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async start() {
|
||||
this.jwtManagerEasyStore = await this.receptionRef.db.smartdataDb.createEasyStore(
|
||||
'jwtManagerEasyStore'
|
||||
);
|
||||
await this.smartjwtInstance.init();
|
||||
let existingKeyPair = await this.jwtManagerEasyStore.readKey('jwtJsonKeypair');
|
||||
if (!existingKeyPair) {
|
||||
await this.rotateKeyPair();
|
||||
}
|
||||
existingKeyPair = await this.jwtManagerEasyStore.readKey('jwtJsonKeypair');
|
||||
this.smartjwtInstance.setKeyPairAsJson(existingKeyPair);
|
||||
}
|
||||
|
||||
public async rotateKeyPair() {
|
||||
await this.smartjwtInstance.createNewKeyPair();
|
||||
await this.jwtManagerEasyStore.writeKey(
|
||||
'jwtJsonKeypair',
|
||||
this.smartjwtInstance.getKeyPairAsJson()
|
||||
);
|
||||
await this.pushPublicKeyToClients();
|
||||
}
|
||||
|
||||
public async verifyJWTAndGetData(jwtArg: string): Promise<Jwt> {
|
||||
const jwtData: plugins.lointReception.data.IJwt = await this.smartjwtInstance.verifyJWTAndGetData(jwtArg);
|
||||
const jwt = await Jwt.getInstance({
|
||||
id: jwtData.id,
|
||||
});
|
||||
if (jwt.blocked) {
|
||||
return null;
|
||||
}
|
||||
if (jwt) {
|
||||
const loginSession = await jwt.getLoginSession();
|
||||
if (!loginSession) {
|
||||
await jwt.block();
|
||||
this.blockedJwtIdList.push(jwt.id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return jwt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { LoginSessionManager } from './classes.loginsessionmanager.js';
|
||||
import { User } from './classes.user.js';
|
||||
|
||||
/**
|
||||
* a LoginSession keeps track of a login over the whole time of the user being loggedin
|
||||
*/
|
||||
@plugins.smartdata.Manager()
|
||||
export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
||||
LoginSession,
|
||||
plugins.lointReception.data.ILoginSession,
|
||||
LoginSessionManager
|
||||
> {
|
||||
// ======
|
||||
// static
|
||||
// ======
|
||||
public static async createLoginSessionForUser(userArg: User, deleteOtherSessions = false) {
|
||||
const loginSession = new LoginSession();
|
||||
loginSession.id = plugins.smartunique.shortId();
|
||||
loginSession.data.userId = userArg.id;
|
||||
await loginSession.save();
|
||||
return loginSession;
|
||||
}
|
||||
|
||||
public static async clearLoginSessionsForUser(userArg: User) {
|
||||
// lets find existing sessions
|
||||
const existingSessions = await LoginSession.getInstances({
|
||||
id: userArg.id,
|
||||
});
|
||||
|
||||
for (const existingSession of existingSessions) {
|
||||
await existingSession.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public static async getLoginSessionBySessionId(sessionIdArg: string) {
|
||||
return await LoginSession.getInstance({
|
||||
id: sessionIdArg,
|
||||
});
|
||||
}
|
||||
|
||||
public static async getLoginSessionByRefreshToken(refreshTokenArg: string) {
|
||||
const loginSession = await LoginSession.getInstance({
|
||||
data: {
|
||||
refreshToken: refreshTokenArg,
|
||||
},
|
||||
});
|
||||
return loginSession;
|
||||
}
|
||||
|
||||
// ========
|
||||
// INSTANCE
|
||||
// ========
|
||||
@plugins.smartdata.unI()
|
||||
public id: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.lointReception.data.ILoginSession['data'] = {
|
||||
userId: null,
|
||||
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ weeks: 1 }),
|
||||
invalidated: false,
|
||||
refreshToken: null,
|
||||
deviceId: null
|
||||
};
|
||||
|
||||
public transferToken: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* invalidates a session
|
||||
*/
|
||||
public async invalidate() {
|
||||
this.data.invalidated = true;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* a refresh token is unique to a login session and ONLY created once per login session
|
||||
* @returns
|
||||
*/
|
||||
public async getRefreshToken() {
|
||||
if (this.data.invalidated) {
|
||||
console.log('login session is invalidated. no refresh token can be generated.');
|
||||
return null;
|
||||
}
|
||||
if (!this.data.refreshToken) {
|
||||
this.data.refreshToken = plugins.smartunique.uni('refresh_');
|
||||
}
|
||||
await this.save();
|
||||
return this.data.refreshToken;
|
||||
}
|
||||
|
||||
public async getTransferToken() {
|
||||
this.transferToken = plugins.smartunique.uni('transfer_');
|
||||
return this.transferToken;
|
||||
}
|
||||
|
||||
public async validateRefreshToken(refreshTokenArg: string) {
|
||||
return this.data.refreshToken === refreshTokenArg;
|
||||
}
|
||||
|
||||
public async validateTransferToken(transferTokenArg: string) {
|
||||
const result = this.transferToken === transferTokenArg;
|
||||
|
||||
// a transfer token can only be used once, so we invalidate it here
|
||||
if (result) {
|
||||
this.transferToken = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { LoginSession } from './classes.loginsession.js';
|
||||
import { Reception } from './classes.reception.js';
|
||||
|
||||
export class LoginSessionManager {
|
||||
// refs
|
||||
public receptionRef: Reception;
|
||||
public get db() {
|
||||
return this.receptionRef.db.smartdataDb;
|
||||
}
|
||||
|
||||
public CLoginSession = plugins.smartdata.setDefaultManagerForDoc(this, LoginSession);
|
||||
|
||||
public loginSessions = new plugins.lik.ObjectMap<LoginSession>();
|
||||
|
||||
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
public emailTokenMap = new plugins.lik.ObjectMap<{
|
||||
email: string;
|
||||
token: string;
|
||||
action: 'emailLogin' | 'passwordReset';
|
||||
}>();
|
||||
|
||||
constructor(receptionRefArg: Reception) {
|
||||
this.receptionRef = receptionRefArg;
|
||||
this.receptionRef.typedrouter.addTypedRouter(this.typedRouter);
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_LoginWithEmailOrUsernameAndPassword>(
|
||||
'loginWithEmailOrUsernameAndPassword',
|
||||
async (requestData) => {
|
||||
let user = await this.receptionRef.userManager.CUser.getInstance({
|
||||
data: {
|
||||
username: requestData.username,
|
||||
passwordHash: await this.receptionRef.userManager.CUser.hashPassword(
|
||||
requestData.password
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
if (!user && requestData.username.includes('@')) {
|
||||
user = await this.receptionRef.userManager.CUser.getInstance({
|
||||
data: {
|
||||
email: requestData.username,
|
||||
passwordHash: await this.receptionRef.userManager.CUser.hashPassword(
|
||||
requestData.password
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (user) {
|
||||
// lets recheck
|
||||
if (
|
||||
(user.data.username !== requestData.username &&
|
||||
user.data.email !== requestData.username) ||
|
||||
user.data.passwordHash !==
|
||||
(await this.receptionRef.userManager.CUser.hashPassword(requestData.password))
|
||||
) {
|
||||
throw new Error(
|
||||
'database returned a user that does not match wanted criterea. CRITICAL!'
|
||||
);
|
||||
}
|
||||
|
||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||
this.loginSessions.add(loginSession);
|
||||
const refreshToken = await loginSession.getRefreshToken();
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
refreshToken: refreshToken,
|
||||
twoFaNeeded: false,
|
||||
};
|
||||
} else {
|
||||
throw new plugins.typedrequest.TypedResponseError('User not found!');
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_LoginWithEmail>(
|
||||
'loginWithEmail',
|
||||
async (requestDataArg) => {
|
||||
const existingUser = await this.receptionRef.userManager.CUser.getInstance({
|
||||
data: {
|
||||
email: requestDataArg.email,
|
||||
},
|
||||
});
|
||||
if (existingUser) {
|
||||
this.emailTokenMap.findOneAndRemoveSync(
|
||||
(itemArg) => itemArg.email === existingUser.data.email
|
||||
);
|
||||
const loginEmailToken = plugins.smartunique.uuid4();
|
||||
this.emailTokenMap.add({
|
||||
email: existingUser.data.email,
|
||||
token: loginEmailToken,
|
||||
action: 'emailLogin',
|
||||
});
|
||||
// lets make sure its only valid for 10 minutes
|
||||
plugins.smartdelay.delayFor(600000, null, true).then(() => {
|
||||
this.emailTokenMap.findOneAndRemoveSync(
|
||||
(itemArg) => itemArg.token === loginEmailToken
|
||||
);
|
||||
});
|
||||
this.receptionRef.receptionMailer.sendLoginWithEMailMail(existingUser, loginEmailToken);
|
||||
}
|
||||
return {
|
||||
status: 'ok',
|
||||
testOnlyToken: process.env.TEST_MODE
|
||||
? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
|
||||
.token
|
||||
: null,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_LoginWithEmailAfterEmailTokenAquired>(
|
||||
'loginWithEmailAfterEmailTokenAquired',
|
||||
async (requestArg) => {
|
||||
const tokenObject = this.emailTokenMap.findSync((itemArg) => {
|
||||
return itemArg.email === requestArg.email && itemArg.token === requestArg.token;
|
||||
});
|
||||
if (tokenObject) {
|
||||
const user = await this.receptionRef.userManager.CUser.getInstance({
|
||||
data: {
|
||||
email: requestArg.email,
|
||||
},
|
||||
});
|
||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||
this.loginSessions.add(loginSession);
|
||||
return {
|
||||
refreshToken: await loginSession.getRefreshToken(),
|
||||
};
|
||||
} else {
|
||||
throw new plugins.typedrequest.TypedResponseError('Validation Token not found');
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.typedRouter.addTypedHandler<plugins.lointReception.request.ILogoutRequest>(
|
||||
new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => {
|
||||
const loginSession = await this.CLoginSession.getLoginSessionByRefreshToken(requestDataArg.refreshToken);
|
||||
await loginSession.invalidate();
|
||||
return {}
|
||||
})
|
||||
);
|
||||
|
||||
this.typedRouter.addTypedHandler<plugins.lointReception.request.IReq_ExchangeRefreshTokenAndTransferToken>(
|
||||
new plugins.typedrequest.TypedHandler(
|
||||
'exchangeRefreshTokenAndTransferToken',
|
||||
async (requestDataArg) => {
|
||||
switch (true) {
|
||||
case !!requestDataArg.refreshToken:
|
||||
const loginSession = await this.loginSessions.find(async (loginSessionArg) => {
|
||||
return loginSessionArg.validateRefreshToken(requestDataArg.refreshToken);
|
||||
});
|
||||
if (!loginSession) {
|
||||
throw new plugins.typedrequest.TypedResponseError('your refresh token is invalid');
|
||||
}
|
||||
return {
|
||||
transferToken: await loginSession.getTransferToken(),
|
||||
};
|
||||
break;
|
||||
case !!requestDataArg.transferToken:
|
||||
let transferToken: string;
|
||||
const loginSession2 = await this.loginSessions.find(async (loginSessionArg) => {
|
||||
return loginSessionArg.validateTransferToken(requestDataArg.transferToken);
|
||||
});
|
||||
if (!loginSession2) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Your transfer token is not valid.'
|
||||
);
|
||||
}
|
||||
return {
|
||||
refreshToken: await loginSession2.getRefreshToken(),
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_ResetPassword>(
|
||||
'resetPassword',
|
||||
async (requestDataArg) => {
|
||||
const emailOfPasswordToReset = requestDataArg.email;
|
||||
const existingUser = await this.receptionRef.userManager.CUser.getInstance({
|
||||
data: {
|
||||
email: emailOfPasswordToReset,
|
||||
},
|
||||
});
|
||||
if (existingUser) {
|
||||
this.emailTokenMap.findOneAndRemoveSync(
|
||||
(itemArg) => itemArg.email === existingUser.data.email
|
||||
);
|
||||
this.emailTokenMap.add({
|
||||
email: existingUser.data.email,
|
||||
token: plugins.smartunique.shortId(),
|
||||
action: 'passwordReset',
|
||||
});
|
||||
plugins.smartdelay.delayFor(600000, null, true).then(() => {
|
||||
this.emailTokenMap.findOneAndRemoveSync(
|
||||
(itemArg) => itemArg.email === existingUser.data.email
|
||||
);
|
||||
});
|
||||
this.receptionRef.receptionMailer.sendPasswordResetMail(
|
||||
existingUser,
|
||||
this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
|
||||
.token
|
||||
);
|
||||
}
|
||||
// note: we always return ok here, since we don't want to give any indication as to wether a user is already registered with us.
|
||||
return {
|
||||
status: 'ok',
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_SetNewPassword>(
|
||||
'setNewPassword',
|
||||
async (requestData) => {
|
||||
return {
|
||||
status: 'ok',
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* returns a device id by simply returning a uuid4
|
||||
*/
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_ObtainDeviceId>('obtainDeviceId', async (reqData) => {
|
||||
reqData;
|
||||
return {
|
||||
deviceId: {
|
||||
id: plugins.smartunique.uuid4()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_AttachDeviceId>('attachDeviceId', async (reqData) => {
|
||||
// TODO: Blocked by proper JWT handling
|
||||
reqData.jwt;
|
||||
return {
|
||||
ok: false
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { OrganizationManager } from './classes.organizationmanager.js';
|
||||
import { User } from './classes.user.js';
|
||||
|
||||
@plugins.smartdata.Manager()
|
||||
export class Organization extends plugins.smartdata.SmartDataDbDoc<
|
||||
Organization,
|
||||
plugins.lointReception.data.IOrganization,
|
||||
OrganizationManager
|
||||
> {
|
||||
public static async createNewOrganizationForUser(
|
||||
organizationManagerArg: OrganizationManager,
|
||||
userIdArg: string,
|
||||
orgNameArg: string,
|
||||
slugNameArg: string,
|
||||
) {
|
||||
const newOrg = new Organization();
|
||||
newOrg.id = plugins.smartunique.shortId();
|
||||
newOrg.data = {
|
||||
name: orgNameArg,
|
||||
slug: slugNameArg,
|
||||
billingPlanId: null,
|
||||
roleIds: [],
|
||||
}
|
||||
await newOrg.save();
|
||||
return newOrg;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.unI()
|
||||
id: plugins.lointReception.data.IOrganization['id'];
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
data: plugins.lointReception.data.IOrganization['data'];
|
||||
|
||||
public async checkIfUserIsAdmin(userArg: User) {
|
||||
const role = await this.manager.receptionRef.roleManager.getRoleForUserAndOrg(userArg, this);
|
||||
return role.data.role === 'admin';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { Reception } from './classes.reception.js';
|
||||
import { Organization } from './classes.organization.js';
|
||||
import { User } from './classes.user.js';
|
||||
|
||||
export class OrganizationManager {
|
||||
public receptionRef: Reception;
|
||||
public get db() {
|
||||
return this.receptionRef.db.smartdataDb;
|
||||
}
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
public COrganization = plugins.smartdata.setDefaultManagerForDoc(this, Organization);
|
||||
|
||||
constructor(receptionRefArg: Reception) {
|
||||
this.receptionRef = receptionRefArg;
|
||||
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_CreateOrganization>(
|
||||
'createOrganization',
|
||||
async (requestArg) => {
|
||||
const nameIsAvailable = async () => {
|
||||
const existingOrg = await this.COrganization.getInstance({
|
||||
data: {
|
||||
slug: requestArg.organizationSlug,
|
||||
},
|
||||
});
|
||||
const nameAvailable = !existingOrg;
|
||||
return nameAvailable;
|
||||
};
|
||||
switch (requestArg.action) {
|
||||
case 'checkAvailability':
|
||||
return {
|
||||
nameAvailable: await nameIsAvailable(),
|
||||
};
|
||||
break;
|
||||
case 'manifest':
|
||||
const nameCheckedOk = await nameIsAvailable();
|
||||
const userData = await this.receptionRef.userManager.getUserByJwtValidation(
|
||||
requestArg.jwt
|
||||
);
|
||||
const newOrg = await this.COrganization.createNewOrganizationForUser(
|
||||
this,
|
||||
userData.id,
|
||||
requestArg.organizationName,
|
||||
requestArg.organizationSlug
|
||||
);
|
||||
const role = await this.receptionRef.roleManager.modifyRoleForUserAtOrg({
|
||||
action: 'create',
|
||||
organizationId: newOrg.id,
|
||||
userId: userData.id,
|
||||
role: 'admin',
|
||||
});
|
||||
newOrg.data.roleIds.push(role.id);
|
||||
await newOrg.save();
|
||||
return {
|
||||
nameAvailable: true,
|
||||
resultingOrganization: await newOrg.createSavableObject()
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_GetOrganizationById>(
|
||||
'getOrganizationById',
|
||||
async (requestArg) => {
|
||||
const verifiedJwt = await this.receptionRef.jwtManager.verifyJWTAndGetData(
|
||||
requestArg.jwt
|
||||
);
|
||||
const user = await this.receptionRef.userManager.CUser.getInstance({
|
||||
id: verifiedJwt.data.userId,
|
||||
});
|
||||
const organization = await this.COrganization.getInstance({
|
||||
id: requestArg.id
|
||||
});
|
||||
const role = await this.receptionRef.roleManager.CRole.getInstance({
|
||||
data: {
|
||||
organizationId: organization.id,
|
||||
userId: user.id,
|
||||
}
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new plugins.typedrequest.TypedResponseError('User not authorized for the requested organization.');
|
||||
}
|
||||
return {
|
||||
organization: await organization.createSavableObject()
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async getAllOrganizationsForUser(
|
||||
userArg: User,
|
||||
) {
|
||||
|
||||
const organizations: Organization[] = [];
|
||||
const userRoles = await this.receptionRef.roleManager.getAllRolesForUser(userArg);
|
||||
|
||||
for (const role of userRoles) {
|
||||
const organization = await this.receptionRef.organizationmanager.COrganization.getInstance({
|
||||
id: role.data.organizationId
|
||||
});
|
||||
if (!organizations.find(orgArg => orgArg.id === organization.id)) {
|
||||
organizations.push(organization);
|
||||
}
|
||||
}
|
||||
|
||||
return organizations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import { logger } from './logging.js';
|
||||
|
||||
import { JwtManager } from './classes.jwtmanager.js';
|
||||
import { LoginSessionManager } from './classes.loginsessionmanager.js';
|
||||
import { RegistrationSessionManager } from './classes.registrationsessionmanager.js';
|
||||
import { ReceptionServer } from './classes.receptionserver.js';
|
||||
import { ReceptionDb } from './classes.receptiondb.js';
|
||||
import { ReceptionMailer } from './classes.receptionmailer.js';
|
||||
import { UserManager } from './classes.usermanager.js';
|
||||
import { ApiTokenManager } from './classes.apitokenmanager.js';
|
||||
import { ReceptionHousekeeping } from './classes.housekeeping.js';
|
||||
import { OrganizationManager } from './classes.organizationmanager.js';
|
||||
import { RoleManager } from './classes.rolemanager.js';
|
||||
import { BillingPlanManager } from './classes.billingplanmanager.js';
|
||||
|
||||
export class Reception {
|
||||
public projectinfoNpm = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
public serviceQenv = new plugins.qenv.Qenv('./', './.nogit');
|
||||
public szPlatformClient = new plugins.szPlatformClient.SzPlatformClient();
|
||||
public db = new ReceptionDb(this);
|
||||
|
||||
// server
|
||||
public serviceServer = new ReceptionServer(this);
|
||||
|
||||
// managers
|
||||
public jwtManager = new JwtManager(this);
|
||||
public loginSessionManager = new LoginSessionManager(this);
|
||||
public registrationSessionManager = new RegistrationSessionManager(this);
|
||||
public apitokenManager = new ApiTokenManager(this);
|
||||
public receptionMailer = new ReceptionMailer(this);
|
||||
public userManager = new UserManager(this);
|
||||
public organizationmanager = new OrganizationManager(this);
|
||||
public roleManager = new RoleManager(this);
|
||||
public billingPlanManager = new BillingPlanManager(this);
|
||||
housekeeping = new ReceptionHousekeeping(this);
|
||||
|
||||
constructor(public databaseName?: string) {}
|
||||
|
||||
/**
|
||||
* starts the reception instance
|
||||
*/
|
||||
public async start() {
|
||||
logger.log('info', 'starting reception');
|
||||
await this.db.start(this.databaseName);
|
||||
await this.jwtManager.start();
|
||||
await this.serviceServer.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* stops the reception instance
|
||||
*/
|
||||
public async stop() {
|
||||
await this.housekeeping.stop();
|
||||
await this.serviceServer.stop();
|
||||
console.log('stopped serviceserver!');
|
||||
await this.db.stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { Reception } from './classes.reception.js';
|
||||
|
||||
export class ReceptionDb {
|
||||
public smartdataDb: plugins.smartdata.SmartdataDb;
|
||||
public receptionRef: Reception;
|
||||
|
||||
constructor(receptionRefArg: Reception) {
|
||||
this.receptionRef = receptionRefArg;
|
||||
}
|
||||
|
||||
public async start(databaseNameArg?: string) {
|
||||
this.smartdataDb = new plugins.smartdata.SmartdataDb({
|
||||
mongoDbUser: await this.receptionRef.serviceQenv.getEnvVarOnDemand('MONGO_DB_USER'),
|
||||
mongoDbName: databaseNameArg || await this.receptionRef.serviceQenv.getEnvVarOnDemand('MONGO_DB_NAME'),
|
||||
mongoDbPass: await this.receptionRef.serviceQenv.getEnvVarOnDemand('MONGO_DB_PASS'),
|
||||
mongoDbUrl: await this.receptionRef.serviceQenv.getEnvVarOnDemand('MONGO_DB_URL'),
|
||||
});
|
||||
await this.smartdataDb.init();
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
await this.smartdataDb.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import { Reception } from './classes.reception.js';
|
||||
import { RegistrationSession } from './classes.registrationsession.js';
|
||||
import { User } from './classes.user.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export class ReceptionMailer {
|
||||
public receptionRef: Reception;
|
||||
|
||||
constructor(receptionRefArg: Reception) {
|
||||
this.receptionRef = receptionRefArg;
|
||||
}
|
||||
|
||||
private createBodyString = (textArg) => `
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
|
||||
<style type="text/css">
|
||||
* {
|
||||
font-family:Arial,Helvetica Neue,Helvetica,sans-serif;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
body {
|
||||
width: 100%;
|
||||
}
|
||||
.bodyspacer {
|
||||
background-color: #f0f0f0;
|
||||
padding-top:20px;
|
||||
padding-bottom:20px;
|
||||
padding-left:10px;
|
||||
padding-right:10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: auto;
|
||||
width: 200px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.logoBottom {
|
||||
display: block;
|
||||
margin: auto;
|
||||
width: 150px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align:center;
|
||||
line-height:35px;
|
||||
margin-bottom:20;
|
||||
}
|
||||
h1.subheading{
|
||||
font-size:15px;
|
||||
font-weight:normal;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
h3 {
|
||||
padding:0;
|
||||
margin:0;
|
||||
margin-bottom:5px;
|
||||
}
|
||||
p {
|
||||
margin-top:5px;
|
||||
}
|
||||
a {
|
||||
text-decoration:none;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
.contentspacer{
|
||||
padding-left:20px;
|
||||
padding-right:20px;
|
||||
}
|
||||
.content {
|
||||
box-shadow: 0px 0px 3px rgba(0,0,0,0.5);
|
||||
color: #333333;
|
||||
background:#ffffff;
|
||||
max-width:600px;
|
||||
border-radius:3px;
|
||||
margin-left:auto;
|
||||
margin-right:auto;
|
||||
min-height:40px;
|
||||
overflow:hidden;
|
||||
}
|
||||
.headerimagewrapper {
|
||||
overflow:hidden;
|
||||
border:none;
|
||||
}
|
||||
.headerimage {
|
||||
min-height: 10px;
|
||||
width:100%;
|
||||
vertical-align:middle;
|
||||
background:#eeeeeb;
|
||||
}
|
||||
img, a {
|
||||
border:none;
|
||||
outline:none;
|
||||
}
|
||||
.textcontent {
|
||||
padding:20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
transition: all 0.2s ease;
|
||||
width: 200px;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
color: #333333;
|
||||
background: #f0f0f0;
|
||||
margin: 20px auto;
|
||||
padding: 10px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: #fff;
|
||||
background: #e4002b;
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: #CCCCCC;
|
||||
text-align:center;
|
||||
font-size:12px;
|
||||
margin-top:10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bodyspacer">
|
||||
<img class="logo" src="https://assetbroker.lossless.one/brandfiles/00general/brightdark_workspaceglobal@2x.png" />
|
||||
<div class="contentspacer">
|
||||
<div class="content">
|
||||
<div class="headerimagewrapper">
|
||||
|
||||
</div>
|
||||
<div class="textcontent">
|
||||
<span>
|
||||
${textArg}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
This email is a service by<br/>
|
||||
Task Venture Capital GmbH, Karl-Ferdinand-Braun-Str. 5, 28359 Bremen, Germany<br/>
|
||||
<a style="color: #666666" href="https://legal.task.vc">Legal Info (https://legal.task.vc)</a>
|
||||
<img class="logoBottom" src="https://assetbroker.lossless.one/brandfiles/00general/brightdark_taskvc@2x_transparent.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
public sendRegistrationEmail(signupSessionArg: RegistrationSession, validationTokenArg: string) {
|
||||
this.receptionRef.szPlatformClient.emailConnector.sendEmail({
|
||||
from: 'workspace.global <noreply@mail.workspace.global>',
|
||||
title: 'Verify your Email Address!',
|
||||
to: signupSessionArg.emailAddress,
|
||||
body: this.createBodyString(`
|
||||
<h1>Email Verification for <br><a style="color: #555555;" href="mailto:${
|
||||
signupSessionArg.emailAddress
|
||||
}">${signupSessionArg.emailAddress}</a></h1>
|
||||
<p>It looks like you requested to register an account with us. We just want to make sure it really was you.</p>
|
||||
<p>In case it was you, <b>please continue with the registration process by clicking the button below</b>. Otherwise, please ignore this email.</p>
|
||||
<a href="https://registration.workspace.global/finishregistration?validationtoken=${encodeURI(
|
||||
validationTokenArg
|
||||
)}"><div class="button">
|
||||
continue with registration
|
||||
</div></a>
|
||||
<p>
|
||||
<b>What do I need a workspace.global Account for?</b><br/>
|
||||
The workspace.global Account is needed to log into e.g. <b>social.io</b>
|
||||
</p>
|
||||
`),
|
||||
});
|
||||
}
|
||||
|
||||
public sendAlreadyRegisteredEmail(userArg: User) {
|
||||
this.receptionRef.szPlatformClient.emailConnector.sendEmail({
|
||||
from: 'workspace.global <noreply@mail.workspace.global>',
|
||||
title: 'Login Instead?!',
|
||||
to: userArg.data.email,
|
||||
body: this.createBodyString(`
|
||||
<h1>Email is already registered:<br><a style="color: #555555;" href="mailto:${
|
||||
userArg.data.email
|
||||
}">${userArg.data.email}</a></h1>
|
||||
<p>Someone retried to reregister with the email ${userArg.data.email}</p>
|
||||
<p>In case it was you, <b>please simply log in with your existing account</b>. Otherwise, please ignore this email.</p>
|
||||
<a href="https://account.lossless.org/finishsignup?email=${encodeURI(
|
||||
userArg.data.email
|
||||
)}"><div class="button">
|
||||
Simply login :)
|
||||
</div></a>
|
||||
<p>
|
||||
<b>Forgot your password?</b><br/>
|
||||
Just click the password reset link when logging in.
|
||||
</p>
|
||||
`),
|
||||
});
|
||||
}
|
||||
|
||||
public sendWelcomeEMail(userArg: User) {
|
||||
this.receptionRef.szPlatformClient.emailConnector.sendEmail({
|
||||
from: 'workspace.global <noreply@mail.workspace.global>',
|
||||
title: 'Welcome and Thank You!',
|
||||
to: userArg.data.email,
|
||||
body: this.createBodyString(`
|
||||
<h1>Welcome And Thank You, ${userArg.data.name}</h1>
|
||||
<p>You now have a fully registered workspace.global Account</p>
|
||||
|
||||
<p>
|
||||
<b>What can I use it for?</b><br/>
|
||||
The workspace.global Account can be used to log into all our apps.<br>
|
||||
Some of them are<br/>
|
||||
${(() => {
|
||||
const products = ['social.io', 'layer.io'];
|
||||
return products.map((productArg) => `<span>${productArg}</span>`).join(' ');
|
||||
})()}
|
||||
</p>
|
||||
|
||||
<a href="https://account.lossless.org/manage/
|
||||
userArg.username
|
||||
)}"><div class="button">
|
||||
Go to my account
|
||||
</div></a>
|
||||
`),
|
||||
});
|
||||
}
|
||||
|
||||
public sendLoginWithEMailMail(userArg: User, validationTokenArg: string) {
|
||||
this.receptionRef.szPlatformClient.emailConnector.sendEmail({
|
||||
from: 'workspace.global <noreply@mail.workspace.global>',
|
||||
title: 'Click to login!',
|
||||
to: userArg.data.email,
|
||||
body: this.createBodyString(`
|
||||
<h1>EMail Login Link for <br><a style="color: #555555;" href="mailto:${
|
||||
userArg.data.email
|
||||
}">${userArg.data.email}</a></h1>
|
||||
<p>It looks like you requested to login passwordless via this email.</p>
|
||||
<p>In case it was you, <b>please continue by clicking the button below</b>. Otherwise, please ignore this email.</p>
|
||||
<a href="https://account.lossless.org/?email=${encodeURI(
|
||||
userArg.data.email
|
||||
)}&validationtoken=${encodeURI(validationTokenArg)}"><div class="button">
|
||||
Login!
|
||||
</div></a>
|
||||
`),
|
||||
});
|
||||
}
|
||||
|
||||
public sendPasswordResetMail(userArg: User, validationTokenArg: string) {
|
||||
this.receptionRef.szPlatformClient.emailConnector.sendEmail({
|
||||
from: 'workspace.global <noreply@mail.workspace.global>',
|
||||
title: 'Password reset?',
|
||||
to: userArg.data.email,
|
||||
body: this.createBodyString(`
|
||||
<h1>Password Reset for <br><a style="color: #555555;" href="mailto:${userArg.data.email}">${
|
||||
userArg.data.email
|
||||
}</a></h1>
|
||||
<p>It looks like you requested to reset your password with us.</p>
|
||||
<p>In case it was you, <b>please continue by clicking the button below</b>. Otherwise, please ignore this email.</p>
|
||||
<a href="https://account.lossless.org/?email=${encodeURI(
|
||||
userArg.data.email
|
||||
)}&validationtoken=${encodeURI(validationTokenArg)}"><div class="button">
|
||||
Reset Password
|
||||
</div></a>
|
||||
`),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { Reception } from './classes.reception.js';
|
||||
|
||||
export class ReceptionServer {
|
||||
public receptionRef: Reception;
|
||||
public serviceServer: plugins.loleServiceServer.ServiceServer;
|
||||
public typedsocket: plugins.typedsocket.TypedSocket;
|
||||
|
||||
constructor(receptionRef: Reception) {
|
||||
this.receptionRef = receptionRef;
|
||||
this.serviceServer = new plugins.loleServiceServer.ServiceServer({
|
||||
serviceDomain: 'reception.lossless.one',
|
||||
serviceName: 'reception',
|
||||
serviceVersion: this.receptionRef.projectinfoNpm.version,
|
||||
port: parseInt(this.receptionRef.serviceQenv.getEnvVarOnDemand('TEST_PORT')) || 3000,
|
||||
addCustomRoutes: async (serverArg) => {
|
||||
serverArg.addRoute(
|
||||
'/typedrequest',
|
||||
new plugins.loleServiceServer.HandlerTypedRouter(this.receptionRef.typedrouter)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.serviceServer.start();
|
||||
this.typedsocket = this.serviceServer.typedServer.typedsocket;
|
||||
this.serviceServer.typedServer.typedrouter.addTypedRouter(this.receptionRef.typedrouter);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await this.typedsocket.stop();
|
||||
await this.serviceServer.stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
/**
|
||||
* can be used to store binary data for users and organizations
|
||||
*/
|
||||
@plugins.smartdata.Collection(() => {
|
||||
return null;
|
||||
})
|
||||
export class ReceptionStorage {}
|
||||
@@ -0,0 +1,200 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
import { RegistrationSessionManager } from './classes.registrationsessionmanager.js';
|
||||
import { logger } from './logging.js';
|
||||
import { User } from './classes.user.js';
|
||||
|
||||
/**
|
||||
* a RegistrationSession is a in memory session for signing up
|
||||
*/
|
||||
export class RegistrationSession {
|
||||
// ======
|
||||
// STATIC
|
||||
// ======
|
||||
public static async createRegistrationSessionForEmail(
|
||||
registrationSessionManageremailArg: RegistrationSessionManager,
|
||||
emailArg: string
|
||||
) {
|
||||
const newRegistrationSession = new RegistrationSession(
|
||||
registrationSessionManageremailArg,
|
||||
emailArg
|
||||
);
|
||||
const emailValidationResult = await newRegistrationSession
|
||||
.validateEMailAddress()
|
||||
.catch((error) => {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Error occured during email provider & dns validation'
|
||||
);
|
||||
});
|
||||
if (!emailValidationResult?.valid) {
|
||||
newRegistrationSession.destroy();
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Email Address is not valid. Please use a correctly formated email address'
|
||||
);
|
||||
}
|
||||
if (emailValidationResult.disposable) {
|
||||
newRegistrationSession.destroy();
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Email is disposable. Please use a non disposable email address.'
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`${newRegistrationSession.emailAddress} is valid. Continuing registration process!`
|
||||
);
|
||||
await newRegistrationSession.sendTokenValidationEmail();
|
||||
console.log(`Successfully sent email validation email`);
|
||||
return newRegistrationSession;
|
||||
}
|
||||
|
||||
// ========
|
||||
// INSTANCE
|
||||
// ========
|
||||
public registrationSessionManagerRef: RegistrationSessionManager;
|
||||
|
||||
public emailAddress: string;
|
||||
|
||||
/**
|
||||
* only used during testing
|
||||
*/
|
||||
public unhashedEmailToken?: string;
|
||||
public hashedEmailToken: string;
|
||||
private smsvalidationCounter = 0;
|
||||
public smsCode: string;
|
||||
|
||||
/**
|
||||
* the status of the registration. should progress in a linear fashion.
|
||||
*/
|
||||
public status: 'announced' | 'emailValidated' | 'mobileVerified' | 'registered' | 'failed' =
|
||||
'announced';
|
||||
|
||||
public collectedData: {
|
||||
userData: plugins.lointReception.data.IUser['data'];
|
||||
} = {
|
||||
userData: {
|
||||
username: null,
|
||||
connectedOrgs: [],
|
||||
email: null,
|
||||
name: null,
|
||||
status: null,
|
||||
mobileNumber: null,
|
||||
password: null,
|
||||
passwordHash: null,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(
|
||||
registrationSessionManagerRefArg: RegistrationSessionManager,
|
||||
emailAddressArg: string
|
||||
) {
|
||||
this.registrationSessionManagerRef = registrationSessionManagerRefArg;
|
||||
this.emailAddress = emailAddressArg;
|
||||
this.registrationSessionManagerRef.registrationSessions.addToMap(this.emailAddress, this);
|
||||
|
||||
// lets destroy this after 10 minutes,
|
||||
// works in unrefed mode so not blocking node exiting.
|
||||
plugins.smartdelay.delayFor(600000, null, true).then(() => this.destroy());
|
||||
}
|
||||
|
||||
/**
|
||||
* validates a token by comparing its hash against the stored hashed token
|
||||
* @param tokenArg
|
||||
*/
|
||||
public validateEmailToken(tokenArg: string): boolean {
|
||||
const result = this.hashedEmailToken === plugins.smarthash.sha256FromStringSync(tokenArg);
|
||||
if (result && this.status === 'announced') {
|
||||
this.status = 'emailValidated';
|
||||
this.collectedData.userData.email = this.emailAddress;
|
||||
}
|
||||
if (!result && this.status === 'announced') {
|
||||
this.status = 'failed';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** validates the sms code */
|
||||
public validateSmsCode(smsCodeArg: string) {
|
||||
this.smsvalidationCounter++;
|
||||
const result = this.smsCode === smsCodeArg;
|
||||
if (this.status === 'emailValidated' && result) {
|
||||
this.status = 'mobileVerified';
|
||||
return result;
|
||||
} else {
|
||||
if (this.smsvalidationCounter === 5) {
|
||||
this.destroy();
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Registration cancelled due to repeated wrong verification code submission'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* validate the email address with provider and dns sanity checks
|
||||
* @returns
|
||||
*/
|
||||
public async validateEMailAddress(): Promise<plugins.smartmail.IEmailValidationResult> {
|
||||
console.log(`validating email ${this.emailAddress}`);
|
||||
const result = await new plugins.smartmail.EmailAddressValidator().validate(this.emailAddress);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* send the validation email
|
||||
*/
|
||||
public async sendTokenValidationEmail() {
|
||||
const uuidToSend = plugins.smartunique.uuid4();
|
||||
this.unhashedEmailToken = uuidToSend;
|
||||
this.hashedEmailToken = plugins.smarthash.sha256FromStringSync(uuidToSend);
|
||||
this.registrationSessionManagerRef.receptionRef.receptionMailer.sendRegistrationEmail(
|
||||
this,
|
||||
uuidToSend
|
||||
);
|
||||
logger.log('info', `sent a validation email with a verification code to ${this.emailAddress}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* validate the mobile number of someone
|
||||
*/
|
||||
public async sendValidationSms() {
|
||||
if (!process.env.TEST_MODE) {
|
||||
this.smsCode =
|
||||
await this.registrationSessionManagerRef.receptionRef.loleSmsClientInstance.sendSmsVerifcation(
|
||||
{
|
||||
fromName: 'w...global',
|
||||
toNumber: parseInt(this.collectedData.userData.mobileNumber),
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('Not sending SMS in automated test mode');
|
||||
this.smsCode = '123456';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* this method can be called when this registrationsession is validated
|
||||
* and all data has been set
|
||||
*/
|
||||
public async manifestUserWithAccountData(): Promise<User> {
|
||||
if (this.status !== 'mobileVerified') {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'You can only manifest user that have a validated email Address and Mobile Number'
|
||||
);
|
||||
}
|
||||
if (!this.collectedData) {
|
||||
throw new Error('You have to set the accountdata first');
|
||||
}
|
||||
const manifestedUser =
|
||||
await this.registrationSessionManagerRef.receptionRef.userManager.CUser.createNewUserForUserData(
|
||||
this.collectedData.userData
|
||||
);
|
||||
return manifestedUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* destroys the registrationsession
|
||||
*/
|
||||
public destroy() {
|
||||
this.registrationSessionManagerRef.registrationSessions.removeFromMap(this.emailAddress);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { RegistrationSession } from './classes.registrationsession.js';
|
||||
import { Reception } from './classes.reception.js';
|
||||
import { logger } from './logging.js';
|
||||
|
||||
export class RegistrationSessionManager {
|
||||
public receptionRef: Reception;
|
||||
|
||||
public registrationSessions = new plugins.lik.FastMap<RegistrationSession>();
|
||||
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
constructor(receptionRefArg: Reception) {
|
||||
this.receptionRef = receptionRefArg;
|
||||
this.receptionRef.typedrouter.addTypedRouter(this.typedRouter);
|
||||
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_FirstRegistration>(
|
||||
'firstRegistrationRequest',
|
||||
async (requestData) => {
|
||||
// check for exiting User
|
||||
const existingUser = await this.receptionRef.userManager.CUser.getInstance({
|
||||
data: {
|
||||
email: requestData.email,
|
||||
},
|
||||
});
|
||||
if (existingUser) {
|
||||
this.receptionRef.receptionMailer.sendAlreadyRegisteredEmail(existingUser);
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
`We sent you an Email with more information.`
|
||||
);
|
||||
}
|
||||
// check for exiting SignupSession
|
||||
const existingSession = this.registrationSessions.getByKey(requestData.email);
|
||||
if (existingSession) {
|
||||
logger.log('warn', `destroyed old signupSession for ${requestData.email}`);
|
||||
existingSession.destroy();
|
||||
}
|
||||
|
||||
// lets check the email before we create a signup session
|
||||
|
||||
const newSignupSession = await RegistrationSession.createRegistrationSessionForEmail(
|
||||
this,
|
||||
requestData.email
|
||||
).catch((e: plugins.typedrequest.TypedResponseError) => {
|
||||
console.log(e.errorText);
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (newSignupSession) {
|
||||
logger.log('info', `created signupSession for ${requestData.email}`);
|
||||
return {
|
||||
status: 'ok',
|
||||
testOnlyToken: process.env.TEST_MODE ? newSignupSession.unhashedEmailToken : null,
|
||||
};
|
||||
} else {
|
||||
return { status: 'not ok' };
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_AfterRegistrationEmailClicked>(
|
||||
'afterRegistrationEmailClicked',
|
||||
async (requestData) => {
|
||||
const signupSession = await this.registrationSessions.find(async (itemArg) =>
|
||||
itemArg.validateEmailToken(requestData.token)
|
||||
);
|
||||
if (signupSession) {
|
||||
return {
|
||||
email: signupSession.emailAddress,
|
||||
status: 'ok',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
email: null,
|
||||
status: 'not ok',
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_SetDataForRegistration>(
|
||||
'setDataForRegistration',
|
||||
async (requestData) => {
|
||||
const registrationSession = await this.registrationSessions.find(async (itemArg) =>
|
||||
itemArg.validateEmailToken(requestData.token)
|
||||
);
|
||||
if (!registrationSession) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'could not find a matching signupsession'
|
||||
);
|
||||
}
|
||||
|
||||
if (requestData.userData.name) {
|
||||
registrationSession.collectedData.userData.name = requestData.userData.name;
|
||||
}
|
||||
|
||||
if (requestData.userData.password) {
|
||||
registrationSession.collectedData.userData.password = requestData.userData.password;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_MobileVerificationForRegistration>(
|
||||
'mobileVerificationForRegistration',
|
||||
async (requestData) => {
|
||||
const registrationSession = await this.registrationSessions.find(async (itemArg) =>
|
||||
itemArg.validateEmailToken(requestData.token)
|
||||
);
|
||||
if (!registrationSession) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'could not find a matching signupsession'
|
||||
);
|
||||
}
|
||||
|
||||
// check prerequisites
|
||||
if (registrationSession.status === 'announced') {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'You must validate the email address first'
|
||||
);
|
||||
}
|
||||
|
||||
if (requestData.mobileNumber) {
|
||||
registrationSession.status = 'emailValidated';
|
||||
registrationSession.collectedData.userData.mobileNumber = requestData.mobileNumber;
|
||||
await registrationSession.sendValidationSms();
|
||||
return {
|
||||
messageSent: true,
|
||||
testOnlySmsCode: process.env.TEST_MODE ? registrationSession.smsCode : null,
|
||||
};
|
||||
}
|
||||
|
||||
if (requestData.verificationCode) {
|
||||
const validationResult = registrationSession.validateSmsCode(
|
||||
requestData.verificationCode
|
||||
);
|
||||
return {
|
||||
verficationCodeOk: validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'you misused the purpose of this TypedHandler'
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.typedRouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.lointReception.request.IReq_FinishRegistration>(
|
||||
'finishRegistration',
|
||||
async (requestData) => {
|
||||
const registrationSession = await this.registrationSessions.find(async (itemArg) =>
|
||||
itemArg.validateEmailToken(requestData.token)
|
||||
);
|
||||
if (!registrationSession) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'could not find a matching signupsession'
|
||||
);
|
||||
}
|
||||
|
||||
const resultingUser = await registrationSession.manifestUserWithAccountData();
|
||||
registrationSession.destroy();
|
||||
this.receptionRef.receptionMailer.sendWelcomeEMail(resultingUser);
|
||||
return {
|
||||
accountData: {
|
||||
id: resultingUser.id,
|
||||
data: {
|
||||
email: resultingUser.data.email,
|
||||
name: resultingUser.data.name,
|
||||
username: resultingUser.data.username,
|
||||
},
|
||||
},
|
||||
status: 'ok',
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
@plugins.smartdata.Manager()
|
||||
export class Role extends plugins.smartdata.SmartDataDbDoc<
|
||||
Role,
|
||||
plugins.lointReception.data.IRole
|
||||
> {
|
||||
@plugins.smartdata.unI()
|
||||
id: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
data: plugins.lointReception.data.IRole['data'];
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Organization } from './classes.organization.js';
|
||||
import { Reception } from './classes.reception.js';
|
||||
import { Role } from './classes.role.js';
|
||||
import { User } from './classes.user.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export class RoleManager {
|
||||
// INSTANCE
|
||||
public receptionRef: Reception;
|
||||
public get db() {
|
||||
return this.receptionRef.db.smartdataDb;
|
||||
}
|
||||
public CRole = plugins.smartdata.setDefaultManagerForDoc(this, Role);
|
||||
constructor(receptionRefArg: Reception) {
|
||||
this.receptionRef = receptionRefArg;
|
||||
}
|
||||
|
||||
public async modifyRoleForUserAtOrg(optionsArg: {
|
||||
action: 'create' | 'change' | 'delete';
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
role: plugins.lointReception.data.IRole['data']['role'];
|
||||
}) {
|
||||
let returnRole: Role;
|
||||
switch (optionsArg.action) {
|
||||
case 'create':
|
||||
returnRole = new this.CRole();
|
||||
returnRole.id = plugins.smartunique.shortId();
|
||||
returnRole.data = {
|
||||
userId: optionsArg.userId,
|
||||
organizationId: optionsArg.organizationId,
|
||||
role: optionsArg.role,
|
||||
};
|
||||
await returnRole.save();
|
||||
}
|
||||
return returnRole;
|
||||
}
|
||||
|
||||
public async getRoleForUserAndOrg(userArg: User, orgArg: Organization) {
|
||||
const role = await this.CRole.getInstance({
|
||||
data: {
|
||||
userId: userArg.id,
|
||||
organizationId: orgArg.id,
|
||||
}
|
||||
})
|
||||
return role;
|
||||
}
|
||||
|
||||
public async getAllRolesForUser(userArg: User) {
|
||||
const roles = await this.CRole.getInstances({
|
||||
data: {
|
||||
userId: userArg.id
|
||||
}
|
||||
});
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { UserManager } from './classes.usermanager.js';
|
||||
|
||||
/**
|
||||
* a User is identified by its username or email.
|
||||
* Both need to be unique and both can be changed.
|
||||
*/
|
||||
@plugins.smartdata.Manager()
|
||||
export class User extends plugins.smartdata.SmartDataDbDoc<
|
||||
User,
|
||||
plugins.lointReception.data.IUser
|
||||
> {
|
||||
// STATIC
|
||||
public static async createNewUserForUserData(
|
||||
userDataArg: plugins.lointReception.data.IUser['data']
|
||||
): Promise<User> {
|
||||
const newUser = new User();
|
||||
newUser.id = plugins.smartunique.shortId();
|
||||
newUser.data = {
|
||||
connectedOrgs: null,
|
||||
status: 'new',
|
||||
name: userDataArg.name,
|
||||
username: userDataArg.username,
|
||||
email: userDataArg.email,
|
||||
passwordHash: userDataArg.passwordHash,
|
||||
};
|
||||
if (!newUser.data.passwordHash && userDataArg.password) {
|
||||
newUser.data.passwordHash = await User.hashPassword(userDataArg.password);
|
||||
}
|
||||
await newUser.save();
|
||||
return newUser;
|
||||
}
|
||||
|
||||
public static hashPassword(passwordArg: string) {
|
||||
return plugins.smarthash.sha256FromString(passwordArg);
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
@plugins.smartdata.unI()
|
||||
id: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public data: plugins.lointReception.data.IUser['data'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public setLegalData() {}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Reception } from './classes.reception.js';
|
||||
import { User } from './classes.user.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
/**
|
||||
* a user manager
|
||||
*/
|
||||
export class UserManager {
|
||||
// refs
|
||||
public receptionRef: Reception;
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
public get db() {
|
||||
return this.receptionRef.db.smartdataDb;
|
||||
}
|
||||
|
||||
// classes
|
||||
public CUser = plugins.smartdata.setDefaultManagerForDoc(this, User);
|
||||
|
||||
constructor(receptionRefArg: Reception) {
|
||||
this.receptionRef = receptionRefArg;
|
||||
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.typedrouter.addTypedHandler<plugins.lointReception.request.IReq_GetRolesAndOrganizationsForUserId>(
|
||||
new plugins.typedrequest.TypedHandler('getRolesAndOrganizationsForUserId', async reqArg => {
|
||||
const user = await this.getUserByJwtValidation(reqArg.jwt);
|
||||
const organizations = await this.receptionRef.organizationmanager.getAllOrganizationsForUser(
|
||||
user
|
||||
);
|
||||
const roles = await this.receptionRef.roleManager.getAllRolesForUser(user);
|
||||
return {
|
||||
organizations,
|
||||
roles
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the user by validating a JWT
|
||||
*/
|
||||
public async getUserByJwt(jwtString: string) {
|
||||
const jwtInstance = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwtString);
|
||||
const user = await this.CUser.getInstance({
|
||||
id: jwtInstance.data.userId
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* just validate jwt
|
||||
* faster than the "getUserByJwt"
|
||||
*/
|
||||
public async getUserByJwtValidation(jwtStringArg: string) {
|
||||
const jwtDataArg: plugins.lointReception.data.IJwt = await this.receptionRef.jwtManager.smartjwtInstance.verifyJWTAndGetData(jwtStringArg);
|
||||
const resultingUser = await this.CUser.getInstance({
|
||||
id: jwtDataArg.data.userId
|
||||
});
|
||||
return resultingUser;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// general exports for testing
|
||||
export * from './classes.reception.js';
|
||||
|
||||
// running it in production
|
||||
import { Reception } from './classes.reception.js';
|
||||
|
||||
let reception: Reception;
|
||||
export const runCli = async () => {
|
||||
reception = new Reception();
|
||||
await reception.start();
|
||||
};
|
||||
|
||||
export const stop = async () => {
|
||||
await reception.stop();
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
|
||||
const projectinfoNpm = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
|
||||
|
||||
export const logger = plugins.loleLog.createLoleLogger({
|
||||
companyUnit: 'Lossless Cloud',
|
||||
containerName: 'reception',
|
||||
containerVersion: projectinfoNpm.version,
|
||||
sentryAppName: 'reception',
|
||||
sentryDsn: 'https://fd929bdcad0a41c0b7853cdea04f9c96@o169278.ingest.sentry.io/5272722',
|
||||
zone: 'servezone',
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export const packageDir = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '../');
|
||||
@@ -0,0 +1,57 @@
|
||||
// node native
|
||||
import * as path from 'path';
|
||||
|
||||
export { path };
|
||||
|
||||
// project scope
|
||||
import * as lointReception from '../../dist_ts_interfaces/index.js';
|
||||
|
||||
export { lointReception, };
|
||||
|
||||
// @apiglobal scope
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import * as typedsocket from '@api.global/typedsocket';
|
||||
|
||||
export { typedrequest, typedsocket };
|
||||
|
||||
// @serve.zone scope
|
||||
import * as szPlatformClient from '@serve.zone/platformclient';
|
||||
|
||||
export { szPlatformClient };
|
||||
|
||||
|
||||
// @pushrocks scope
|
||||
import * as lik from '@push.rocks/lik';
|
||||
import * as projectinfo from '@push.rocks/projectinfo';
|
||||
import * as qenv from '@push.rocks/qenv';
|
||||
import * as smartdata from '@push.rocks/smartdata';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartmail from '@push.rocks/smartmail';
|
||||
import * as smarthash from '@push.rocks/smarthash';
|
||||
import * as smartjwt from '@push.rocks/smartjwt';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smarttime from '@push.rocks/smarttime';
|
||||
import * as smartunique from '@push.rocks/smartunique';
|
||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||
|
||||
export {
|
||||
lik,
|
||||
projectinfo,
|
||||
qenv,
|
||||
smartdata,
|
||||
smartdelay,
|
||||
smartmail,
|
||||
smarthash,
|
||||
smartjwt,
|
||||
smartpath,
|
||||
smartpromise,
|
||||
smarttime,
|
||||
smartunique,
|
||||
taskbuffer,
|
||||
};
|
||||
|
||||
// @tsclass scope
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
export { tsclass };
|
||||
Reference in New Issue
Block a user