This commit is contained in:
2024-09-29 13:56:38 +02:00
commit 31a6ef96d8
85 changed files with 13360 additions and 0 deletions
+8
View File
@@ -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'
}
+7
View File
@@ -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/');
+14
View File
@@ -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
View File
@@ -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();
};
+34
View File
@@ -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();
}
}
+12
View File
@@ -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;
}
}
+36
View File
@@ -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,
}
}
}
}))
}
}
+45
View File
@@ -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');
}
}
+78
View File
@@ -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;
}
}
+141
View File
@@ -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;
}
}
+114
View File
@@ -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;
}
}
+259
View File
@@ -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
}
})
)
}
}
+40
View File
@@ -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';
}
}
+115
View File
@@ -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;
}
}
+61
View File
@@ -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();
}
}
+25
View File
@@ -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();
}
}
+270
View File
@@ -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>
`),
});
}
}
+35
View File
@@ -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();
}
}
+9
View File
@@ -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 {}
+200
View File
@@ -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',
};
}
)
);
}
}
+14
View File
@@ -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'];
}
+57
View File
@@ -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;
}
}
+50
View File
@@ -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() {}
}
+60
View File
@@ -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;
}
}
+15
View File
@@ -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();
};
+13
View File
@@ -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',
});
+3
View File
@@ -0,0 +1,3 @@
import * as plugins from './plugins.js';
export const packageDir = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '../');
+57
View File
@@ -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 };