This commit is contained in:
2024-09-29 13:56:38 +02:00
commit 31a6ef96d8
85 changed files with 13360 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
node_modules/
+71
View File
@@ -0,0 +1,71 @@
name: Docker (tags)
on:
push:
tags-ignore:
- '**'
env:
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_LOGIN_DOCKER_GITEA: ${{ github.server_url }}|${{ gitea.repository_owner }}|${{ secrets.GITEA_TOKEN }}
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
jobs:
security:
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
continue-on-error: true
steps:
- uses: actions/checkout@v3
- name: Install pnpm and npmci
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci npm prepare
npmci node install stable
npmci npm install
npmci command npm run build
+106
View File
@@ -0,0 +1,106 @@
name: Docker (tags)
on:
push:
tags:
- '*'
env:
IMAGE: code.foss.global/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_LOGIN_DOCKER_GITEA: ${{ github.server_url }}|${{ gitea.repository_owner }}|${{ secrets.GITEA_TOKEN }}
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
jobs:
security:
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
continue-on-error: true
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci command npm run build
release:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: code.foss.global/hosttoday/ht-docker-dbase:npmci
steps:
- uses: actions/checkout@v3
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @ship.zone/npmci
- name: Release
run: |
npmci docker login
npmci docker build
npmci docker test
# npmci docker push
npmci docker push
metadata:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Trigger
run: npmci trigger
+20
View File
@@ -0,0 +1,20 @@
.nogit/
# artifacts
coverage/
public/
pages/
# installs
node_modules/
# caches
.yarn/
.cache/
.rpt2_cache
# builds
dist/
dist_*/
# custom
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "npm test",
"name": "Run npm test",
"request": "launch",
"type": "node-terminal"
}
]
}
+26
View File
@@ -0,0 +1,26 @@
{
"json.schemas": [
{
"fileMatch": ["/npmextra.json"],
"schema": {
"type": "object",
"properties": {
"npmci": {
"type": "object",
"description": "settings for npmci"
},
"gitzone": {
"type": "object",
"description": "settings for gitzone",
"properties": {
"projectType": {
"type": "string",
"enum": ["website", "element", "service", "npm", "wcc"]
}
}
}
}
}
}
]
}
+46
View File
@@ -0,0 +1,46 @@
# gitzone dockerfile_service
## STAGE 1 // BUILD
FROM code.foss.global/hosttoday/ht-docker-node:npmci as node1
COPY ./ /app
WORKDIR /app
ARG NPMCI_TOKEN_NPM2
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
RUN npmci npm prepare
RUN pnpm config set store-dir .pnpm-store
RUN rm -rf node_modules && pnpm install
RUN pnpm run build
# gitzone dockerfile_service
## STAGE 2 // install production
FROM code.foss.global/hosttoday/ht-docker-node:npmci as node2
WORKDIR /app
COPY --from=node1 /app /app
RUN rm -rf .pnpm-store
ARG NPMCI_TOKEN_NPM2
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
RUN npmci npm prepare
RUN pnpm config set store-dir .pnpm-store
RUN rm -rf node_modules/ && pnpm install --prod
## STAGE 3 // rebuild dependencies for alpine
FROM code.foss.global/hosttoday/ht-docker-node:alpinenpmci as node3
WORKDIR /app
COPY --from=node2 /app /app
ARG NPMCI_TOKEN_NPM2
ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2
RUN npmci npm prepare
RUN pnpm config set store-dir .pnpm-store
RUN pnpm rebuild -r
## STAGE 4 // the final production image with all dependencies in place
FROM code.foss.global/hosttoday/ht-docker-node:alpine as node4
WORKDIR /app
COPY --from=node3 /app /app
### Healthchecks
RUN pnpm install -g @servezone/healthy
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD [ "healthy" ]
EXPOSE 80
CMD ["npm", "start"]
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env node
process.env.CLI_CALL = 'true';
import * as cliTool from './ts/index.js';
cliTool.runCli();
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env node
process.env.CLI_CALL = 'true';
const cliTool = await import('./dist_ts/index.js');
cliTool.runCli();
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env node
process.env.CLI_CALL = 'true';
import * as tsrun from '@git.zone/tsrun';
tsrun.runPath('./cli.child.js', import.meta.url);
+110
View File
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!--Lets set some basic meta tags-->
<meta
name="viewport"
content="user-scalable=0, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="theme-color" content="#000000" />
<!--Lets make sure we recognize this as an PWA-->
<link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/png" href="/assetbroker/manifest/favicon.png" />
<!--Lets load standard fonts-->
<link rel="preconnect" href="https://assetbroker.lossless.one/" crossorigin>
<link rel="stylesheet" href="https://assetbroker.lossless.one/fonts/fonts.css">
<!--Lets avoid a rescaling flicker due to default body margins-->
<style>
html {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
body {
position: relative;
background: #000;
margin: 0px;
}
</style>
<script>
projectVersion = '';
</script>
</head>
<body>
<noscript>
<style>
body {
background: #303f9f;
font-family: Inter, Roboto, sans-serif;
color: #ffffff;
}
a {
color: #ffffff;
text-decoration: none;
}
img {
width: 130px;
}
.container {
width: 600px;
margin: auto;
margin-top: 20px;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
overflow: hidden;
border-radius: 3px;
background: #4357d9;
}
.contentHeader {
padding: 20px;
text-align: center;
font-size: 25px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.content {
padding: 20px;
}
.footer {
padding: 10px;
text-align: center;
}
</style>
<div class="container">
<div class="contentHeader">We need JavaScript to run properly!</div>
<div class="content">
This site is being built using lit-element (made by Google). This technology works with
JavaScript. Subsequently this website does not work as intended without
JavaScript.
</div>
</div>
<div class="footer">
<a href="https://">Legal Info</a> |
<a href="https:///privacy">Privacy Policy</a>
</div>
</noscript>
<script type="text/javascript" async defer>
window.revenueEnabled = true;
const runRevenueCheck = async () => {
var e = document.createElement('div');
e.id = '476kjuhzgtr764';
e.style.display = 'none';
document.body.appendChild(e);
if (document.getElementById('476kjuhzgtr764')) {
window.revenueEnabled = true;
} else {
window.revenueEnabled = false;
}
console.log(`revenue enabled: ${window.revenueEnabled}`);
};
runRevenueCheck();
</script>
</body>
<script defer type="module" src="/bundle.js"></script>
</html>
+24
View File
@@ -0,0 +1,24 @@
{
"gitzone": {
"projectType": "website",
"module": {
"githost": "code.foss.global",
"gitscope": "idp.global",
"gitrepo": "idp.global",
"description": "the code that runs the idp.global software",
"npmPackagename": "@idp.global/idp.global",
"license": "MIT",
"projectDomain": "idp.global"
}
},
"npmci": {
"npmGlobalTools": [],
"dockerRegistryRepoMap": {
"registry.gitlab.com": "code.foss.global/idp.global/idp.global"
},
"dockerBuildargEnvMap": {
"NPMCI_TOKEN_NPM2": "NPMCI_TOKEN_NPM2"
},
"npmRegistryUrl": "registry.npmjs.org"
}
}
+82
View File
@@ -0,0 +1,82 @@
{
"name": "@idp.global/idp.global",
"version": "1.0.0",
"description": "the code that runs the idp.global software",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
"scripts": {
"test": "npm run build",
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle website --production",
"watch": "tswatch website",
"start": "(node cli.js)",
"startTs": "(node cli.ts.js)",
"buildDocs": "tsdoc"
},
"author": "Task Venture Capital GmbH",
"license": "MIT",
"dependencies": {
"@api.global/typedrequest": "^3.0.32",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^3.0.51",
"@api.global/typedsocket": "^3.0.1",
"@consentsoftware_private/catalog": "^1.0.73",
"@design.estate/dees-catalog": "^1.1.8",
"@design.estate/dees-domtools": "^2.0.23",
"@design.estate/dees-element": "^2.0.15",
"@push.rocks/lik": "^6.0.15",
"@push.rocks/qenv": "^6.0.5",
"@push.rocks/smartdata": "^5.2.10",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smarthash": "^3.0.4",
"@push.rocks/smartjson": "^5.0.20",
"@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.0.7",
"@push.rocks/smartmail": "^1.0.24",
"@push.rocks/smartpath": "^5.0.5",
"@push.rocks/smartpromise": "^4.0.4",
"@push.rocks/smartrx": "^3.0.7",
"@push.rocks/smartstate": "^2.0.0",
"@push.rocks/smarttime": "^4.0.8",
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smarturl": "^3.0.7",
"@push.rocks/taskbuffer": "^3.1.7",
"@push.rocks/webjwt": "^1.0.9",
"@push.rocks/websetup": "^3.0.15",
"@push.rocks/webstore": "^2.0.20",
"@serve.zone/platformclient": "^1.0.6",
"@tsclass/tsclass": "^4.1.2"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.1.17",
"@git.zone/tsbundle": "^2.0.3",
"@git.zone/tsrun": "^1.2.8",
"@git.zone/tswatch": "^2.0.1",
"@push.rocks/projectinfo": "^5.0.1",
"@types/node": "^22.7.2"
},
"private": true,
"repository": {
"type": "git",
"url": "git+https://code.foss.global/idp.global/idp.global.git"
},
"bugs": {
"url": "https://code.foss.global/idp.global/idp.global/issues"
},
"homepage": "https://code.foss.global/idp.global/idp.global#readme",
"browserslist": [
"last 1 chrome versions"
],
"files": [
"ts/**/*",
"ts_web/**/*",
"dist/**/*",
"dist_*/**/*",
"dist_ts/**/*",
"dist_ts_web/**/*",
"assets/**/*",
"cli.js",
"npmextra.json",
"readme.md"
]
}
+8171
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
required:
+3
View File
@@ -0,0 +1,3 @@
# Project Readme Hints
This is the initial readme hints file.
+1
View File
@@ -0,0 +1 @@
## Usage
+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 };
+362
View File
@@ -0,0 +1,362 @@
import { IdpRequests } from './classes.idprequests.js';
import * as plugins from './plugins.js';
export class IdpClient {
// INSTANCE PRIVATE
private helpers = {
async extractDataFromJwtString(jwtString: string): Promise<plugins.lointReception.data.IJwt> {
return plugins.webjwt.getDataFromJwtString(jwtString);
},
};
// INSTANCE PUBLIC
public appData: plugins.lointReception.data.IApp;
public rolesReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1);
public organizationsReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1);
public receptionTrUrl: string;
constructor(receptionBaseUrlArg: string, appDataArg?: plugins.lointReception.data.IApp) {
this.receptionTrUrl = receptionBaseUrlArg
if (this.receptionTrUrl.endsWith('/')) {
this.receptionTrUrl = this.receptionTrUrl.slice(0, -1);
}
if (!this.receptionTrUrl.endsWith('/typedrequest')) {
this.receptionTrUrl = `${this.receptionTrUrl}/typedrequest`;
}
console.log(`reception client connecting to ${this.receptionTrUrl}`);
if (!appDataArg) {
appDataArg = {
id: '', // TODO
appUrl: `https://${window.location.host}/`,
description: null,
logoUrl: null,
name: null,
};
}
this.appData = appDataArg;
}
public requests = new IdpRequests(this);
/**
* app data can be transferred when redirecting to the sso domain using query params
* this message retrieves the app data when on the sso domain
*/
public async getAppDataOnSsoDomain() {
if (!window.location.href.startsWith('https://sso.workspace.global/')) {
console.error('You are trying to access SSO appData on a non sso domain.');
return null;
}
const appDataString = plugins.smarturl.Smarturl.createFromUrl(window.location.href).searchParams
.appdata;
if (!appDataString) {
console.error('no appdata query arg detected');
return null;
}
const appData = plugins.smartjson.parseBase64(appDataString);
return appData;
}
public async setJwt(jwtStringArg: string) {
await this.storeJwt(jwtStringArg);
}
/**
* a typedsocket for going reactive
*/
public typedsocket: plugins.typedsocket.TypedSocket;
/**
* a typed router to go reactive
*/
public typedrouter = new plugins.typedrequest.TypedRouter();
public statusObservable =
new plugins.smartrx.rxjs.Subject<plugins.lointReception.data.TLoginStatus>();
public ssoStore = new plugins.webstore.WebStore({
storeName: 'wgsso',
dbName: 'wgsso',
});
public async storeJwt(jwtString: string) {
await this.ssoStore.set('wgJwt', jwtString);
}
public async getJwt(): Promise<string> {
return await this.ssoStore.get('wgJwt');
}
public async getJwtData(): Promise<plugins.lointReception.data.IJwt> {
return this.helpers.extractDataFromJwtString(await this.getJwt());
}
public async deleteJwt() {
await this.ssoStore.delete('wgJwt');
console.log('removed jwt');
}
/**
* performs jwt housekeeping
* only call if jwt is present
* @returns
*/
public async performJwtHousekeeping() {
let jwt = await this.getJwt();
if (!jwt) {
return null;
}
const extractedJwt = await this.helpers.extractDataFromJwtString(jwt);
if (extractedJwt.data.refreshFrom < Date.now() && Date.now() < extractedJwt.data.validUntil) {
jwt = await this.refreshJwt();
} else if (Date.now() > extractedJwt.data.validUntil) {
this.deleteJwt();
}
return jwt;
}
public async refreshJwt(refreshTokenArg?: string): Promise<string> {
let extractedJwt: plugins.lointReception.data.IJwt;
if (!refreshTokenArg) {
extractedJwt = await this.helpers.extractDataFromJwtString(await this.getJwt());
}
const refreshJwtReq =
new plugins.typedrequest.TypedRequest<plugins.lointReception.request.IReq_RefreshJwt>(
`${this.receptionTrUrl}/typedrequest`,
'refreshJwt'
);
const response = await refreshJwtReq.fire({
refreshToken: refreshTokenArg || extractedJwt.data.refreshToken,
});
if (response.jwt) {
await this.storeJwt(response.jwt);
} else {
await this.deleteJwt();
}
this.statusObservable.next(response.status);
return await this.getJwt();
}
/**
* can be used to switch between pages
*/
public async getTransferToken(appDataArg?: plugins.lointReception.data.IApp): Promise<string> {
const jwt = await this.performJwtHousekeeping();
const extractedJwt = await this.helpers.extractDataFromJwtString(jwt);
const getTransferToken =
new plugins.typedrequest.TypedRequest<plugins.lointReception.request.IReq_ExchangeRefreshTokenAndTransferToken>(
`${this.receptionTrUrl}/typedrequest`,
'exchangeRefreshTokenAndTransferToken'
);
const response = await getTransferToken.fire({
refreshToken: extractedJwt.data.refreshToken,
appData: appDataArg || this.appData,
});
return response.transferToken;
}
/**
* gets a transfer token and switches to a location
*/
public async getTransferTokenAndSwitchToLocation(newLocationArg: string): Promise<void> {
const transferToken = await this.getTransferToken();
if (!transferToken) {
alert('failed to get transfer token!');
}
const urlInstance = plugins.smarturl.Smarturl.createFromUrl(newLocationArg, {
searchParams: {
transfertoken: transferToken,
},
});
const transferUrl = urlInstance.toString();
window.location.href = transferUrl;
return;
}
/**
* processes a transfer token
*/
public async processTransferToken(): Promise<boolean> {
const href = window.location.href;
const url = plugins.smarturl.Smarturl.createFromUrl(href);
const transferToken = url.searchParams['transfertoken'];
if (transferToken) {
const getTransferToken =
new plugins.typedrequest.TypedRequest<plugins.lointReception.request.IReq_ExchangeRefreshTokenAndTransferToken>(
`${this.receptionTrUrl}/typedrequest`,
'exchangeRefreshTokenAndTransferToken'
);
const response = await getTransferToken.fire({
transferToken,
appData: this.appData,
});
if (response.refreshToken) {
await this.refreshJwt(response.refreshToken);
} else {
globalThis.alert?.('transfer token invalid');
return false;
}
return true;
} else {
return false;
}
}
// Login Status stuff
public async checkJwtPresent() {
const jwt = await this.performJwtHousekeeping();
if (jwt) {
return true;
} else {
return false;
}
}
/**
* forces the current user to login
* @param requireLoginArg
* @returns
*/
public async determineLoginStatus(requireLoginArg: boolean = false): Promise<boolean> {
const jwtPresent = await this.checkJwtPresent();
if (jwtPresent) {
const jwt = await this.performJwtHousekeeping();
return !!jwt;
} else {
const transferTokenResult = await this.processTransferToken();
if (transferTokenResult) {
// we are in the clear
return true;
} else {
if (requireLoginArg) {
const urlInstance = plugins.smarturl.Smarturl.createFromUrl(
'https://sso.workspace.global/',
{
searchParams: {
appdata: plugins.smartjson.stringifyBase64(this.appData),
action: 'login',
},
}
);
if (!globalThis.location.href.startsWith('https://sso.workspace.global/')) {
globalThis.location.href = urlInstance.toString();
}
}
return false;
}
}
}
/**
* logs out the current user
*/
public async logout() {
const urlInstance = plugins.smarturl.Smarturl.createFromUrl('https://sso.workspace.global/', {
searchParams: {
appdata: plugins.smartjson.stringifyBase64(this.appData),
action: 'logout',
},
});
if (!globalThis.location.href.startsWith('https://sso.workspace.global/')) {
// we are somewhere in an app
await this.deleteJwt();
globalThis.location.href = urlInstance.toString();
} else {
// we are in the sso page
await this.enableTypedSocket();
console.log(`logging out against ${this.receptionTrUrl}`)
const logoutTr =
this.typedsocket.createTypedRequest<plugins.lointReception.request.ILogoutRequest>(
'logout'
);
await logoutTr.fire({
refreshToken: (await this.getJwtData()).data.refreshToken,
});
await this.deleteJwt();
const appData = await this.getAppDataOnSsoDomain();
if (appData) {
console.log(`redirecting to app after logout: ${appData.appUrl}`);
window.location.href = appData.appUrl;
} else {
console.error('no appData provided. Not redirecting after logout.');
}
}
}
public typedsocketDeferred = plugins.smartpromise.defer();
public async enableTypedSocket() {
if (this.typedsocketDeferred.claimed) {
return this.typedsocketDeferred.promise;
}
this.typedsocketDeferred.claim();
this.typedsocket = await plugins.typedsocket.TypedSocket.createClient(
this.typedrouter,
`${this.receptionTrUrl}/`
);
this.typedsocketDeferred.resolve(this.typedsocket);
return this.typedsocketDeferred.promise;
}
public async stop() {
await this.typedsocket?.stop();
}
// ==================================
// Organization and Settings stuff
// ==================================
public async createOrganization(
orgNameArg: string,
orgSlugArg: string,
modeArg: 'checkAvailability' | 'manifest'
) {
await this.typedsocketDeferred.promise;
const validateOrg =
this.typedsocket.createTypedRequest<plugins.lointReception.request.IReq_CreateOrganization>(
'createOrganization'
);
const response = await validateOrg.fire({
jwt: await this.getJwt(),
action: modeArg,
organizationName: orgNameArg,
organizationSlug: orgSlugArg,
userId: (await this.getJwtData()).id,
});
return response;
}
/**
* gets the current OrganizationRoles
*/
public async getRolesAndOrganizations() {
await this.typedsocketDeferred.promise;
const rolesAndOrganizationsForUserId =
this.typedsocket.createTypedRequest<plugins.lointReception.request.IReq_GetRolesAndOrganizationsForUserId>(
'getRolesAndOrganizationsForUserId'
);
const response = await rolesAndOrganizationsForUserId.fire({
jwt: await this.getJwt(),
userId: (await this.getJwtData()).id,
});
return response;
}
/**
* updates the PaddleCheckoutId for an organization.
*/
public async updatePaddleCheckoutId(orgIdArg: string, checkoutIdArg: string) {
await this.typedsocketDeferred.promise;
const updateBillingPlan =
this.typedsocket.createTypedRequest<plugins.lointReception.request.IReq_UpdatePaymentMethod>(
'updatePaymentMethod'
);
const response = await updateBillingPlan.fire({
jwtString: await this.getJwt(),
orgId: orgIdArg,
paddle: {
checkoutId: checkoutIdArg,
},
});
return response;
}
}
+62
View File
@@ -0,0 +1,62 @@
import * as plugins from './plugins.js';
import type { IdpClient } from "./classes.idpclient.js";
/**
* this class bundles all the typed requests that are used by the idp
*/
export class IdpRequests {
idpClientArg: IdpClient;
constructor(idpClientArg: IdpClient) {
this.idpClientArg = idpClientArg;
}
public get afterRegistrationEmailClicked () {
return new plugins.typedrequest.TypedRequest<plugins.lointReception.request.IReq_AfterRegistrationEmailClicked>(
this.idpClientArg.receptionTrUrl,
'afterRegistrationEmailClicked'
);
}
public get setData() {
return new plugins.typedrequest.TypedRequest<plugins.lointReception.request.IReq_SetDataForRegistration>(
this.idpClientArg.receptionTrUrl,
'setDataForRegistration'
);
}
public get mobileNumberVerification () {
return new plugins.typedrequest.TypedRequest<plugins.lointReception.request.IReq_MobileVerificationForRegistration>(
this.idpClientArg.receptionTrUrl,
'mobileVerificationForRegistration'
);
}
public get finishRegistration() {
return new plugins.typedrequest.TypedRequest<plugins.lointReception.request.IReq_FinishRegistration>(
this.idpClientArg.receptionTrUrl,
'finishRegistration'
);
}
public get loginWithUserNameAndPassword () {
return new plugins.typedrequest.TypedRequest<plugins.lointReception.request.IReq_LoginWithEmailOrUsernameAndPassword>(
this.idpClientArg.receptionTrUrl,
'loginWithEmailOrUsernameAndPassword'
);
}
public get obtainJwt () {
return new plugins.typedrequest.TypedRequest<plugins.lointReception.request.IReq_RefreshJwt>(
this.idpClientArg.receptionTrUrl,
'refreshJwt'
);
}
public get obtainOneTimeToken () {
return new plugins.typedrequest.TypedRequest<plugins.lointReception.request.IReq_ExchangeRefreshTokenAndTransferToken>(
this.idpClientArg.receptionTrUrl,
'exchangeRefreshTokenAndTransferToken'
);
}
}
+1
View File
@@ -0,0 +1 @@
export * from './classes.idpclient.js';
+26
View File
@@ -0,0 +1,26 @@
// losslessone_private 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 };
// pushrocks scope
import * as smartjson from '@push.rocks/smartjson';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrx from '@push.rocks/smartrx';
import * as smarttime from '@push.rocks/smarttime';
import * as smarturl from '@push.rocks/smarturl';
import * as webjwt from '@push.rocks/webjwt';
import * as webstore from '@push.rocks/webstore';
export { smartjson, smartpromise, smartrx, smarttime, smarturl, webjwt, webstore };
// @tsclass scope
import * as tsclass from '@tsclass/tsclass';
export { tsclass };
+8
View File
@@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@losslessone_private/loint-reception',
version: '1.0.122',
description: 'an interface package for the reception service at Lossless'
}
+9
View File
@@ -0,0 +1,9 @@
export * from './loint-reception.app.js';
export * from './loint-reception.billingplan.js';
export * from './loint-reception.device.js';
export * from './loint-reception.jwt.js';
export * from './loint-reception.loginsession.js';
export * from './loint-reception.organization.js';
export * from './loint-reception.paddlecheckoutdata.js';
export * from './loint-reception.role.js';
export * from './loint-reception.user.js';
+13
View File
@@ -0,0 +1,13 @@
export interface IApp {
/**
* must be unique
*/
id: string;
/**
* should be unique
*/
name: string;
description: string;
logoUrl: string;
appUrl: string;
}
@@ -0,0 +1,47 @@
import * as plugins from '../loint-reception.plugins.js';
export type TSupportedCurrency = 'EUR';
export interface IBillableItem {
name: string;
monthlyPrice: number;
currency: TSupportedCurrency;
from: number;
to: number;
factoredOn30DayMonth: number;
quantity: number;
}
export interface IBillingPlan {
id: string;
data: {
type: 'Paddle' | 'AppSumo' | 'FairUsageFree' | 'Enterprise' | 'Internal' | 'Testing';
proEnabled: boolean;
organizationId: string;
lastProcessed: number;
seats: number;
status: 'active' | 'activeOverdue' | 'pausedOverdue' | 'inactive' | 'suspended';
paddleData?: {
checkoutId: string;
};
alternativePaymentData?: {
enterprise: boolean;
appSumoCode: string;
};
nextBilling: {
items: Array<IBillableItem>;
method: 'paddle';
ontrack: boolean;
errorText?: string;
selectedBillingDate: number;
};
billingEvents: Array<{
timestamp: number;
amount: number;
currency: TSupportedCurrency;
billedItems: Array<IBillableItem>;
checkoutLink?: string;
}>;
communications: Array<any>;
};
}
@@ -0,0 +1,3 @@
import * as plugins from '../loint-reception.plugins.js';
export interface IDevice extends plugins.tsclass.network.IDevice {}
+38
View File
@@ -0,0 +1,38 @@
export type TLoginStatus = 'loggedIn' | 'loggedOut' | 'invalidated' | 'not found' | 'transfer';
export type TLoginAction = 'login' | 'logout' | 'manage';
export interface IJwt {
id: string;
blocked: boolean;
data: {
/**
* the user id of the jwt
*/
userId: string;
/**
* the latest point of
*/
validUntil: number;
/**
* hold off from refreshing before
*/
refreshFrom: number;
/**
* an interval in millis to recheck token invalidation
*/
refreshEvery: number;
/**
* the refresh token to obtain a new jwt for a session
*/
refreshToken: string;
/**
* just for looks/debugging
*/
justForLooks: {
validUntilIsoString: string;
};
};
}
@@ -0,0 +1,14 @@
export interface ILoginSession {
id: string;
data: {
userId: string;
validUntil: number;
invalidated: boolean;
refreshToken: string;
/**
* a device id that can be used to share the login session
* in different contexts on the same device
*/
deviceId: string;
};
}
@@ -0,0 +1,13 @@
import * as plugins from '../loint-reception.plugins.js';
import { type IBillingPlan } from './loint-reception.billingplan.js';
import { type IRole } from './loint-reception.role.js';
export interface IOrganization {
id: string;
data: {
name: string;
slug: string;
billingPlanId: string;
roleIds: string[];
};
}
@@ -0,0 +1,316 @@
export interface IPaddleCheckoutData<TPassthrough = null> {
checkout: {
created_at: string;
completed: boolean;
id: string;
coupon: {
coupon_code?: string;
};
passthrough?: TPassthrough;
prices: {
customer: {
currency: string;
unit: string;
unit_tax: string;
total: string;
total_tax: string;
items: Array<{
checkout_product_id: number;
product_id: number;
name: string;
custom_message: string;
quantity: number;
allow_quantity: false;
icon_url: string;
min_quantity: number;
max_quantity: number;
currency: string;
unit_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
line_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
discounts: [];
/**
* factorised, not percentage, so looks like 0.19 for Germany.
*/
tax_rate: number;
recurring: {
period: string;
interval: number;
trial_days: number;
currency: string;
unit_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
line_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
discounts: [];
tax_rate: number;
};
}>;
};
vendor: {
currency: string;
unit: string;
unit_tax: string;
total: string;
total_tax: string;
items: [
{
checkout_product_id: number;
product_id: number;
name: string;
custom_message: string;
quantity: number;
allow_quantity: false;
icon_url: string;
min_quantity: number;
max_quantity: number;
currency: string;
unit_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
line_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
discounts: [];
tax_rate: number;
recurring: {
period: string;
interval: number;
trial_days: number;
currency: string;
unit_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
line_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
discounts: [];
tax_rate: number;
};
}
];
};
};
redirect_url: null;
test_variant: 'newCheckout';
recurring_prices: {
customer: {
currency: string;
unit: string;
unit_tax: string;
total: string;
total_tax: string;
items: [
{
checkout_product_id: number;
product_id: number;
name: string;
custom_message: string;
quantity: number;
allow_quantity: false;
icon_url: string;
min_quantity: number;
max_quantity: number;
currency: string;
unit_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
line_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
discounts: [];
tax_rate: number;
recurring: {
period: string;
interval: number;
trial_days: number;
currency: string;
unit_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
line_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
discounts: [];
tax_rate: number;
};
}
];
};
interval: {
length: number;
type: string;
};
vendor: {
currency: string;
unit: string;
unit_tax: string;
total: string;
total_tax: string;
items: [
{
checkout_product_id: number;
product_id: number;
name: string;
custom_message: string;
quantity: number;
allow_quantity: false;
icon_url: string;
min_quantity: number;
max_quantity: number;
currency: string;
unit_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
line_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
discounts: [];
tax_rate: number;
recurring: {
period: string;
interval: number;
trial_days: number;
currency: string;
unit_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
line_price: {
net: number;
gross: number;
net_discount: number;
gross_discount: number;
net_after_discount: number;
gross_after_discount: number;
tax: number;
tax_after_discount: number;
};
discounts: [];
tax_rate: number;
};
}
];
};
};
};
product: {
quantity: number;
id: number;
name: string;
};
user: {
id: string;
email: string;
country: string;
};
}
@@ -0,0 +1,12 @@
import * as plugins from '../loint-reception.plugins.js';
import { type IRole } from './loint-reception.role.js';
export interface ISubOrgProperty {
name: string;
domain: string;
roles: IRole[];
/**
* contains the ids of all the apps that show the property
*/
attributedAppIds: string[];
}
@@ -0,0 +1,13 @@
import * as plugins from '../loint-reception.plugins.js';
/**
* a role describes a
*/
export interface IRole {
id: string;
data: {
userId: string;
organizationId: string;
role: 'owner' | 'admin' | 'editor' | 'guest' | 'viewer' | 'outlaw';
};
}
@@ -0,0 +1,30 @@
import * as plugins from '../loint-reception.plugins.js';
import { type IRole } from './loint-reception.role.js';
export interface IUser {
id: string;
data: {
name: string;
username: string;
email: string;
/**
* mobile number used for verification
*/
mobileNumber?: string;
/**
* only used during initial password setting
*/
password?: string;
/**
* used for validation of passwords
*/
passwordHash?: string;
status: 'new' | 'active' | 'deleted' | 'suspended';
/**
* a quick ref for which organizations might have roles for this user
* speeds up lookup
*/
connectedOrgs: string[];
};
}
+6
View File
@@ -0,0 +1,6 @@
// requests
import * as request from './request/index.js';
import * as data from './data/index.js';
import * as tags from './tags/index.js';
export { request, data, tags };
+9
View File
@@ -0,0 +1,9 @@
// @apiglobal scope
import * as typedRequestInterfaces from '@api.global/typedrequest-interfaces';
export { typedRequestInterfaces };
// @tsclass scope
import * as tsclass from '@tsclass/tsclass';
export { tsclass };
+9
View File
@@ -0,0 +1,9 @@
export * from './loint-reception.apitoken.js';
export * from './loint-reception.authorization.js';
export * from './loint-reception.billingplan.js';
export * from './loint-reception.jwt.js';
export * from './loint-reception.login.js';
export * from './loint-reception.organization.js';
export * from './loint-reception.plan.js';
export * from './loint-reception.registration.js';
export * from './loint-reception.user.js';
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,19 @@
import * as plugins from '../loint-reception.plugins.js';
import { type IUser, type IRole } from '../data/index.js';
export interface IReq_InternalAuthorization
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_InternalAuthorization
> {
method: '';
request: {
accountData: IUser;
jwt: string;
};
response: {
accountData: IUser;
jwt: string;
relevantRoles: IRole[];
};
}
@@ -0,0 +1,39 @@
import * as plugins from '../loint-reception.plugins.js';
import * as data from '../data/index.js';
export interface IReq_UpdatePaymentMethod
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_UpdatePaymentMethod
> {
method: 'updatePaymentMethod';
request: {
jwtString: string;
orgId: string;
paddle?: {
checkoutId: string;
};
};
response: {
billingPlan: plugins.tsclass.typeFest.PartialDeep<data.IBillingPlan>;
};
}
/**
* allows getting the billing plan for a user
*/
export interface IReq_GetBillingPlan
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_GetBillingPlan
> {
method: 'getBillingPlan';
request: {
jwtString: string;
orgId: string;
billingPlanId: string;
};
response: {
billingPlan: data.IBillingPlan;
};
}
@@ -0,0 +1,45 @@
import * as data from '../data/index.js';
import * as plugins from '../loint-reception.plugins.js';
export interface IReq_GetPublicKeyForValidation
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_GetPublicKeyForValidation
> {
method: 'getPublicKeyForValidation';
request: {
backendToken: string;
};
response: {
publicKeyPem: string;
};
}
export interface IReq_PushPublicKeyForValidation
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_PushPublicKeyForValidation
> {
method: 'pushPublicKeyForValidation';
request: {
publicKeyPem: string;
};
response: {};
}
/**
* allows getting or pushing a blocklist of jwt ids
*/
export interface IReq_PushOrGetJwtIdBlocklist
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_PushOrGetJwtIdBlocklist
> {
method: 'pushOrGetJwtIdBlocklist';
request: {
blockedJwtIds?: string[];
};
response: {
blockedJwtIds?: string[];
};
}
@@ -0,0 +1,180 @@
import * as plugins from '../loint-reception.plugins.js';
import * as data from '../data/index.js';
export interface IReq_LoginWithEmailOrUsernameAndPassword
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_LoginWithEmailOrUsernameAndPassword
> {
method: 'loginWithEmailOrUsernameAndPassword';
request: {
username: string;
password: string;
};
response: {
refreshToken?: string;
twoFaNeeded: boolean;
};
}
export interface IReq_LoginWithEmail
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_LoginWithEmailOrUsernameAndPassword
> {
method: 'loginWithEmail';
request: {
email: string;
};
response: {
status: 'ok' | 'not ok';
testOnlyToken?: string;
};
}
export interface IReq_LoginWithEmailAfterEmailTokenAquired
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_LoginWithEmailOrUsernameAndPassword
> {
method: 'loginWithEmailAfterEmailTokenAquired';
request: {
email: string;
token: string;
};
response: {
refreshToken: string;
};
}
/**
* in case you authenticate with a long lived api token
*/
export interface IReq_LoginWithApiToken
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_LoginWithApiToken
> {
method: 'loginWithApiToken';
request: {
apiToken: string;
};
response: {
jwt?: string;
};
}
export interface ILogoutRequest
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
ILogoutRequest
> {
method: 'logout';
request: {
refreshToken: string;
};
response: {};
}
export interface IReq_RefreshJwt
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_RefreshJwt
> {
method: 'refreshJwt';
request: {
refreshToken: string;
};
response: {
status: data.TLoginStatus;
jwt: string;
};
}
/**
* allows the exchange between refreshToken and transferTokens
*/
export interface IReq_ExchangeRefreshTokenAndTransferToken
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_ExchangeRefreshTokenAndTransferToken
> {
method: 'exchangeRefreshTokenAndTransferToken';
request: {
transferToken?: string;
refreshToken?: string;
appData: data.IApp;
};
response: {
refreshToken?: string;
transferToken?: string;
};
}
/**
* in case you authenticate with a long lived api token
*/
export interface IReq_ResetPassword
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_ResetPassword
> {
method: 'resetPassword';
request: {
email: string;
};
response: {
status: 'ok' | 'not ok';
};
}
/**
* in cse you authenticate with a long lived api token
*/
export interface IReq_SetNewPassword
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_SetNewPassword
> {
method: 'setNewPassword';
request: {
email: string;
oldPassword?: string;
tokenArg?: string;
newPassword: string;
};
response: {
status: 'ok' | 'not ok';
};
}
export interface IReq_ObtainDeviceId
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_ObtainDeviceId
> {
method: 'obtainDeviceId';
request: {};
response: {
deviceId: data.IDevice;
};
}
/**
* allows attaching a device id to a login session
* to share a login session across contexts
*/
export interface IReq_AttachDeviceId
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_AttachDeviceId
> {
method: 'attachDeviceId';
request: {
jwt: string;
deviceId: string;
};
response: {
ok: boolean;
};
}
@@ -0,0 +1,51 @@
import * as data from '../data/index.js';
import * as plugins from '../loint-reception.plugins.js';
export interface IReq_GetOrganizationById
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_GetOrganizationById
> {
method: 'getOrganizationById';
request: {
jwt: string;
id: string;
};
response: {
organization: data.IOrganization;
};
}
export interface IReq_CreateOrganization
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_CreateOrganization
> {
method: 'createOrganization';
request: {
jwt: string;
userId: string;
organizationName: string;
organizationSlug: string;
action: 'checkAvailability' | 'manifest';
};
response: {
nameAvailable: boolean;
resultingOrganization?: data.IOrganization;
role?: data.IRole;
};
}
export interface IReq_UpdateOrganization
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_UpdateOrganization
> {
method: 'updateOrganization';
request: {
organization: data.IOrganization;
};
response: {
organization: data.IOrganization;
};
}
@@ -0,0 +1,17 @@
import * as data from '../data/index.js';
import * as plugins from '../loint-reception.plugins.js';
export interface IReq_GetPlansForOrganizationId
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_GetPlansForOrganizationId
> {
method: 'getBillingPlansForOrganizationId';
request: {
jwt: string;
organizationId: string;
};
response: {
billingPlans: data.IBillingPlan[];
};
}
@@ -0,0 +1,90 @@
import * as plugins from '../loint-reception.plugins.js';
import { type IUser } from '../data/index.js';
export interface IReq_FirstRegistration
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_FirstRegistration
> {
method: 'firstRegistrationRequest';
request: {
email: string;
productSlugOfInterest: string;
};
response: {
status: 'ok' | 'not ok';
testOnlyToken?: string;
};
}
export interface IReq_AfterRegistrationEmailClicked
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_AfterRegistrationEmailClicked
> {
method: 'afterRegistrationEmailClicked';
request: {
/**
* the token that has been sent with the registation email to verify access
*/
token: string;
};
response: {
status: 'ok' | 'not ok';
/**
* the email thats associated with the given request token
*/
email: string;
};
}
export interface IReq_SetDataForRegistration
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_SetDataForRegistration
> {
method: 'setDataForRegistration';
request: {
token: string;
userData: IUser['data'];
};
response: {
status: 'ok' | 'not ok';
};
}
/**
* Should be used to verify a mobile number for an verifcation
*/
export interface IReq_MobileVerificationForRegistration
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_MobileVerificationForRegistration
> {
method: 'mobileVerificationForRegistration';
request: {
token: string;
mobileNumber?: string;
verificationCode?: string;
};
response: {
messageSent?: boolean;
verficationCodeOk?: boolean;
testOnlySmsCode?: string;
};
}
export interface IReq_FinishRegistration
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_FinishRegistration
> {
method: 'finishRegistration';
request: {
token: string;
};
response: {
status: 'ok' | 'not ok';
userData?: IUser['data'];
};
}
@@ -0,0 +1,76 @@
import * as data from '../data/index.js';
import * as plugins from '../loint-reception.plugins.js';
export interface IReq_GetUserData
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_GetUserData
> {
method: 'getUserData';
request: {
refreshToken: string;
};
response: {
jwt: string;
};
}
export interface IReq_SetUserData
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_SetUserData
> {
method: 'setUserData';
request: {
refreshToken: string;
};
response: {
oneTimeTransferCode: string;
};
}
export interface IReq_SuspendUser
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_SuspendUser
> {
method: 'suspendUser';
request: {
jwt: string;
userId: string;
};
response: {
publicKeyPem: string;
};
}
export interface IDeleteSuspendedUser
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IDeleteSuspendedUser
> {
method: 'deleteSuspendedUser';
request: {
backendToken: string;
};
response: {
ok: boolean;
errorText?: string;
};
}
export interface IReq_GetRolesAndOrganizationsForUserId
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_GetRolesAndOrganizationsForUserId
> {
method: 'getRolesAndOrganizationsForUserId';
request: {
jwt: string;
userId: string;
};
response: {
roles: data.IRole[];
organizations: data.IOrganization[];
};
}
+12
View File
@@ -0,0 +1,12 @@
import * as plugins from '../loint-reception.plugins.js';
export interface ITag_LolePubapi
extends plugins.typedRequestInterfaces.implementsTag<
plugins.typedRequestInterfaces.ITag,
ITag_LolePubapi
> {
name: 'lole-reception';
payload: {
backendToken: string;
};
}
@@ -0,0 +1,296 @@
import { IdpState } from '../idp.state.js';
import * as plugins from '../plugins.js';
import {
customElement,
DeesElement,
property,
state,
html,
cssManager,
unsafeCSS,
css,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('idp-registration-stepper')
export class IdpRegistrationStepper extends DeesElement {
public idpState = IdpState.getSingletonInstance();
@state()
private usedSubTemplate: TemplateResult;
@state()
private storedData = {
validationTokenUrlParam: 'string',
email: '',
refreshToken: '',
jwt: '',
oneTimeTransferToken: '',
};
constructor() {
super();
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100px;
color: ${cssManager.bdTheme('#333', '#fff')};
}
.main {
position: absolute;
top: 0px;
right: 0px;
left: 0px;
bottom: 0px;
display: flex;
justify-content: center;
align-items: center;
}
`,
];
public render(): TemplateResult {
return html`
<style></style>
<div class="main">
${this.usedSubTemplate
? this.usedSubTemplate
: html`<dees-spinner size="60"></dees-spinner>`}
</div>
`;
}
public async firstUpdated() {
await this.domtoolsPromise;
this.domtools.router.on(`/finishregistration`, async (routeArg) => {
this.storedData.validationTokenUrlParam = routeArg.queryParams.validationtoken;
if (!this.storedData.validationTokenUrlParam) {
this.usedSubTemplate = html`
You need a validation token, but we couldn't find one. Please contact workspace.global support.
`;
await this.domtools.convenience.smartdelay.delayFor(5000);
this.usedSubTemplate = html` Redirecting you to workspace.global support... `;
await this.domtools.convenience.smartdelay.delayFor(2000);
window.location.href = 'https://support.workspace.global';
return;
}
// lets verify the info;
let tokenErrorMessage: string;
const resAfterRegEmailClicked =
await this.idpState.idpClient.requests.afterRegistrationEmailClicked
.fire({
token: this.storedData.validationTokenUrlParam,
})
.catch(
(
err: typeof DeesElement['prototype']['domtools']['convenience']['typedrequest']['TypedResponseError']['prototype']
) => {
tokenErrorMessage = err.errorText;
return;
}
);
if (!resAfterRegEmailClicked || !resAfterRegEmailClicked.email) {
this.usedSubTemplate = html`
the supplied validation token does not match any registration sessions.<br />
${tokenErrorMessage ? html`Reason: ${tokenErrorMessage}` : null}
`;
await this.domtools.convenience.smartdelay.delayFor(5000);
this.usedSubTemplate = html`redirecting you for further support... `;
await this.domtools.convenience.smartdelay.delayFor(1000);
window.location.href = 'https://support.workspace.global';
return;
} else {
this.storedData.email = resAfterRegEmailClicked.email;
}
// lets continue with UI
this.usedSubTemplate = html`<dees-stepper
.steps=${[
{
title: 'What is your name?',
content: html`
<dees-form>
<dees-input-text
key="email"
label="Your Email"
value="${this.storedData.email}"
disabled
></dees-input-text>
<dees-input-text key="firstName" required label="First Name"></dees-input-text>
<dees-input-text key="lastName" required label="Last Name"></dees-input-text>
<dees-form-submit>Next</dees-form-submit>
</dees-form>
`,
validationFunc: async (stepperArg, elementArg) => {
const deesForm: plugins.deesCatalog.DeesForm = elementArg.querySelector('dees-form');
deesForm.addEventListener('formData', async (eventArg: CustomEvent) => {
const response = await this.idpState.idpClient.requests.setData
.fire({
token: this.storedData.validationTokenUrlParam,
userData: {
name: `${eventArg.detail.data.firstName} ${eventArg.detail.data.lastName}`,
connectedOrgs: null,
email: null,
status: null,
username: null,
},
})
.catch(
(
errArg: typeof DeesElement['prototype']['domtools']['convenience']['typedrequest']['TypedResponseError']['prototype']
) => {
deesForm.setStatus('error', errArg.errorText);
}
);
deesForm.setStatus('success', 'ok!');
stepperArg.goNext();
});
},
onReturnToStepFunc: async (stepperArg, stepElementArg) => {
const deesForm = stepElementArg.querySelector('dees-form');
deesForm.setStatus('normal', 'Edit and Next');
},
},
{
title: 'What is your mobile number?',
content: html`
<dees-form>
<dees-input-text
key="mobileNumber"
required
label="Your Mobile Number"
></dees-input-text>
<dees-form-submit>Next</dees-form-submit>
</dees-form>
`,
validationFunc: async (stepperArg, elementArg) => {
const deesForm: plugins.deesCatalog.DeesForm = elementArg.querySelector('dees-form');
deesForm.addEventListener('formData', async (eventArg: CustomEvent) => {
const response = await this.idpState.idpClient.requests.mobileNumberVerification
.fire({
token: this.storedData.validationTokenUrlParam,
mobileNumber: eventArg.detail.data.mobileNumber,
})
.catch(
(
errArg: typeof DeesElement['prototype']['domtools']['convenience']['typedrequest']['TypedResponseError']['prototype']
) => {
deesForm.setStatus('error', errArg.errorText);
}
);
deesForm.setStatus('success', 'ok!');
stepperArg.goNext();
});
},
onReturnToStepFunc: async (stepperArg, stepElementArg) => {
const deesForm = stepElementArg.querySelector('dees-form');
deesForm.setStatus('normal', 'Edit and Next');
},
},
{
title: 'What is the Verification Code?',
content: html`
<dees-form>
<dees-input-text
key="verificationCode"
required
label="Verification Code"
></dees-input-text>
<dees-form-submit>Next</dees-form-submit>
</dees-form>
`,
validationFunc: async (stepperArg, elementArg) => {
const deesForm: plugins.deesCatalog.DeesForm = elementArg.querySelector('dees-form');
deesForm.addEventListener('formData', async (eventArg: CustomEvent) => {
const response = await this.idpState.idpClient.requests.mobileNumberVerification.fire({
token: this.storedData.validationTokenUrlParam,
verificationCode: eventArg.detail.data.verificationCode,
});
if (response.verficationCodeOk) {
deesForm.setStatus('success', 'ok!');
stepperArg.goNext();
} else {
deesForm.setStatus('error', 'wrong code!');
await this.domtools.convenience.smartdelay.delayFor(3000);
deesForm.setStatus('normal', 'Retry And Next!');
}
});
},
onReturnToStepFunc: async (stepperArg, stepElementArg) => {
stepperArg.goBack();
const deesForm = stepElementArg.querySelector('dees-form');
deesForm.setStatus('normal', 'Next');
},
},
{
title: 'Create a secure password:',
content: html`
<dees-form>
<dees-input-text
key="password"
required
label="Your New Secure Password"
></dees-input-text>
<dees-form-submit>Next</dees-form-submit>
</dees-form>
`,
validationFunc: async (stepperArg, elementArg) => {
const deesForm: plugins.deesCatalog.DeesForm = elementArg.querySelector('dees-form');
deesForm.addEventListener('formData', async (eventArg: CustomEvent) => {
const response = await this.idpState.idpClient.requests.setData.fire({
token: this.storedData.validationTokenUrlParam,
userData: {
username: null,
email: null,
name: null,
connectedOrgs: null,
status: null,
password: eventArg.detail.data.password,
},
});
const finishRegistrationResponse =
await this.idpState.idpClient.requests.finishRegistration.fire({
token: this.storedData.validationTokenUrlParam,
});
deesForm.setStatus('pending', 'User created!');
await this.domtools.convenience.smartdelay.delayFor(500);
deesForm.setStatus('pending', 'Obtaining Refresh Token...');
const loginResponse = await this.idpState.idpClient.requests.loginWithUserNameAndPassword.fire(
{
username: this.storedData.email,
password: eventArg.detail.data.password,
}
);
this.storedData.refreshToken = loginResponse.refreshToken;
deesForm.setStatus('pending', 'Obtaining JWT...');
const jwtResponse = await this.idpState.idpClient.requests.obtainJwt.fire({
refreshToken: this.storedData.refreshToken,
});
deesForm.setStatus('pending', 'Obtaining Transfer Token...');
await this.idpState.idpClient.setJwt(jwtResponse.jwt);
await this.idpState.idpClient.getTransferTokenAndSwitchToLocation('https://sso.workspace.global/afterregistration');
});
},
},
] as plugins.deesCatalog.IStep[]}
></dees-stepper>`;
await this.domtools.convenience.smartdelay.delayFor(100);
});
this.domtools.router.on('/', async () => {
this.usedSubTemplate = html`Hm, this is app is not meant for what you are trying to do :) `;
await this.domtools.convenience.smartdelay.delayFor(2000);
this.usedSubTemplate = html`Redirecting you now...`;
window.location.href = `https://workspace.global`;
});
this.domtools.router._handleRouteState();
}
}
+264
View File
@@ -0,0 +1,264 @@
import * as plugins from '../plugins.js';
import {
customElement,
DeesElement,
property,
html,
type TemplateResult,
css,
cssManager,
query,
} from '@design.estate/dees-element';
import { commitinfo } from '../../dist_ts/00_commitinfo_data.js';
declare global {
interface HTMLElementTagNameMap {
'wg-logincontainer': IdpLogincontainer;
}
}
@customElement('idp-logincontainer')
export class IdpLogincontainer extends DeesElement {
public static demo = () => html`<wg-logincontainer></wg-logincontainer>`;
@query('.loginPromptContainer')
loginPromptContainer: HTMLDivElement;
@query('.loginManagerContainer')
loginManagerContainer: HTMLDivElement
@query('.transferManagerContainer')
transferManagerContainer: HTMLDivElement
public receptionClient = new plugins.idpClient.IdpClient('https://reception.lossless.one:443', {
appUrl: 'https://sso.workspace.global/',
description: 'the central sso app for workspace.global',
logoUrl: 'https://assetbroker.lossless.one/some',
name: 'sso.workspace.global',
id: null,
});
constructor() {
super();
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
position: absolute;
width: 100%;
height: 100%;
}
.mainContainer {
position: absolute;
top: 0px;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: all 0.2s;
transition-delay: 0.2s;
transform: translate3d(0px, 20px, 0px);
pointer-events: none;
}
.loginPromptContainer.show {
opacity: 1;
pointer-events: all;
transform: translate3d(0px, 0px, 0px);
}
.loginManagerContainer.show {
opacity: 1;
pointer-events: all;
transform: translate3d(0px, 0px, 0px);
}
.transferManagerContainer.show {
opacity: 1;
pointer-events: all;
transform: translate3d(0px, 0px, 0px);
}
.loginblock {
max-width: 520px;
flex-grow: 1;
transform: translate3d(0px, 0px, 0px);
transition: all 0.2s;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
background: ${cssManager.bdTheme('#ffffff', '#181818')};
border-top: 1px solid ${cssManager.bdTheme('#ffffff', '#333333')};
border-radius: 16px;
overflow: hidden;
}
img {
width: 130px;
min-height: 34.9px;
display: block;
margin: auto;
margin-top: 16px;
margin-bottom: 25px;
filter: ${cssManager.bdTheme('invert(1)', '')};
}
.legalinfo {
text-align: center;
margin: auto;
margin-top: 10px;
color: ${cssManager.bdTheme('#666', '#ccc')};
font-size: 12px;
line-height: 100%;
padding: 8px;
background: ${cssManager.bdTheme('#f5f5f5', '#111')};
border-top: 1px solid ${cssManager.bdTheme('#ccc', '#222222')};
font-family: 'Hubot Sans';
color: ${cssManager.bdTheme('#666', '#888')};
}
.legalinfo a {
color: ${cssManager.bdTheme('#666', '#ccc')};
text-decoration: none;
}
`,
];
render() {
return html`
<div class="mainContainer loginPromptContainer">
<div class="loginblock">
<img
src="https://assetbroker.lossless.one/brandfiles/00general/plain_workspaceglobal.svg"
/>
<wg-loginprompt></wg-loginprompt>
<div class="legalinfo">
<a href="https://legal.task.vc/" target="_blank">Legal Info</a>
| <a href="https://task.vc/" target="_blank">Company Website</a>
| <a href="https://support.task.vc/" target="_blank">Support</a>
| SSO v${commitinfo.version}
</div>
</div>
</div>
<div class="mainContainer loginManagerContainer">
<div class="loginblock">
<img
src="https://assetbroker.lossless.one/brandfiles/00general/plain_workspaceglobal.svg"
/>
<div class="legalinfo">
<a href="https://legal.task.vc/" target="_blank">Legal Info</a>
| <a href="https://task.vc/" target="_blank">Company Website</a>
| <a href="https://support.task.vc/" target="_blank">Support</a>
| SSO v${commitinfo.version}
</div>
</div>
</div>
<div class="mainContainer transferManagerContainer">
<div class="loginblock">
<img
src="https://assetbroker.lossless.one/brandfiles/00general/plain_workspaceglobal.svg"
/>
<wg-transfermanager></wg-transfermanager>
<div class="legalinfo">
<a href="https://legal.task.vc/" target="_blank">Legal Info</a>
| <a href="https://task.vc/" target="_blank">Company Website</a>
| <a href="https://support.task.vc/" target="_blank">Support</a>
| SSO v${commitinfo.version}
</div>
</div>
</div>
`;
}
public async showComponent(componentNameArg: 'loginPrompt' | 'loginManager' | 'transferManager') {
const domtoolsInstance = await this.domtoolsPromise;
const containerItems: HTMLDivElement[] = [
this.loginPromptContainer,
this.loginManagerContainer,
this.transferManagerContainer,
];
const show = async (itemArg: HTMLDivElement) => {
for (const containerItem of containerItems) {
if (containerItem !== itemArg) {
containerItem.classList.remove('show');
}
}
await domtoolsInstance.convenience.smartdelay.delayFor(200);
itemArg.classList.add('show');
await domtoolsInstance.convenience.smartdelay.delayFor(200);
};
switch (componentNameArg) {
case 'loginPrompt':
await show(this.loginPromptContainer);
break;
case 'loginManager':
await show(this.loginManagerContainer);
break;
case 'transferManager':
await show(this.transferManagerContainer);
break;
}
}
public async determineNextAction() {
const domtoolsInstance = await this.domtoolsPromise;
let action: plugins.idpInterfaces.data.TLoginAction;
if (domtoolsInstance.router.queryParams.getQueryParam('action')) {
action = domtoolsInstance.router.queryParams.getQueryParam('action');
}
if (window.location.pathname === '/afterregistration') {
await this.domtools.convenience.smartdelay.delayFor(1000);
await this.receptionClient.determineLoginStatus();
await this.receptionClient.getTransferTokenAndSwitchToLocation('https://account.workspace.global')
} else if (!(await this.receptionClient.determineLoginStatus()) && action === 'login') {
this.showComponent('loginPrompt');
} else if ((await this.receptionClient.determineLoginStatus()) && action === 'login') {
await this.showComponent('transferManager');
const wgTransferManager = this.shadowRoot.querySelector('wg-transfermanager');
await wgTransferManager.handleTransfer();
} else if ((await this.receptionClient.determineLoginStatus()) && action === 'manage') {
this.showComponent('loginManager');
} else if (action === 'logout') {
console.log('logging out, since requested action is "logout"');
await this.receptionClient.logout();
} else {
this.showComponent('loginPrompt');
}
}
public async firstUpdated() {
const domtoolsInstance = await this.domtoolsPromise;
await domtoolsInstance.convenience.smartdelay.delayFor(0);
console.log(`your are loggedin: ${await await this.receptionClient.determineLoginStatus()}`);
let appData: plugins.idpInterfaces.data.IApp;
if (domtoolsInstance.router.queryParams.getQueryParam('appdata')) {
appData = domtoolsInstance.convenience.smartjson.parseBase64(
domtoolsInstance.router.queryParams.getQueryParam('appdata')
);
}
const wgLogin = this.shadowRoot.querySelector('wg-loginprompt');
const wgTransferManager = this.shadowRoot.querySelector('wg-transfermanager');
wgLogin.appData = appData;
wgTransferManager.appData = appData;
await this.determineNextAction();
wgLogin.jwtObserable.subscribe({
next: async (jwtArg) => {
console.log('loggedIn');
await this.receptionClient.storeJwt(jwtArg);
await this.determineNextAction();
},
});
wgLogin.dispatchJwt();
}
}
+298
View File
@@ -0,0 +1,298 @@
import * as plugins from '../plugins.js';
import {
customElement,
DeesElement,
property,
html,
type TemplateResult,
css,
cssManager,
state,
domtools,
} from '@design.estate/dees-element';
// third party catalogs
import '@uptimelink/webwidget';
import '@design.estate/dees-catalog';
import { DeesForm, DeesFormSubmit, DeesInputText } from '@design.estate/dees-catalog';
declare global {
interface HTMLElementTagNameMap {
'wg-loginprompt': WgLogin;
}
}
@customElement('wg-loginprompt')
export class WgLogin extends DeesElement {
public static demo = () => html`<wg-loginprompt></wg-loginprompt>`;
public static receptionUrl = 'https://reception.lossless.one/typedrequest';
@property()
public activePane: 'login' | 'register' = 'login';
@property()
public productOfInterest: string;
@property()
jwt: string;
@property({
reflect: true,
})
appData: plugins.idpInterfaces.data.IApp;
public jwtObserable = new domtools.plugins.smartrx.rxjs.Subject<string>();
constructor() {
super();
domtools.elementBasic.setup();
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
color: ${cssManager.bdTheme('#333333', '#ffffff')};
}
.box {
opacity: 0;
cursor: pointer;
overflow: hidden;
transition: all 0.2s ease;
height: 0px;
}
.box.active {
opacity: 1 !important;
height: 360px;
cursor: auto;
}
.loginbox {
}
.registerbox {
}
.boxcontent {
margin: 0px 20px;
}
.info {
text-align: center;
padding: 32px;
line-height: 1.5em;
font-size: 12px;
font-weight: 600;
color: #999;
}
.registerButton {
display: block;
transition: all 0.2s ease;
will-change: transform;
cursor: pointer;
}
.registerButton:hover {
color: #fff;
transform: scale(1.02);
}
`,
];
public render(): TemplateResult {
return html`
<div class="loginbox box ${this.activePane === 'login' ? 'active' : ''}">
<div class="boxcontent">
<dees-form
id="loginForm"
@formData="${(eventArg) => {
this.login({
emailAddress: eventArg.detail.data.emailAddress,
passwordArg: eventArg.detail.data.password,
});
}}"
>
<dees-input-text
id="loginEmailInput"
.required=${true}
key="emailAddress"
label="Email-Address or Username"
></dees-input-text>
<dees-input-text
.id=${'loginPasswordInput'}
.key=${'password'}
.label=${'Password'}
.isPasswordBool=${true}
></dees-input-text>
<dees-form-submit id="loginSubmitButton"></dees-form-submit>
<div class="info">
You'll go here: ${this.appData ? html`${this.appData.appUrl}` : html``}
<p><span class="registerButton" @click=${() => {this.activePane = 'register'}}>You can also register for a new account.</span></p>
</div>
</dees-form>
</div>
</div>
<div class="registerbox box ${this.activePane === 'register' ? 'active' : ''}">
<div class="boxcontent">
<dees-form
id="registrationForm"
@formData="${(eventArg) => {
this.register({
emailAddress: eventArg.detail.data.emailAddress,
});
}}"
>
<dees-input-text
.required=${true}
key="emailAddress"
label="Email-Address"
></dees-input-text>
<dees-input-checkbox .label="${'Agree to the Terms and Conditions'}"></dees-input-checkbox>
<dees-form-submit>Send Verification Email</dees-form-submit>
<div class="info">
Already have an account?
<p><span class="registerButton" @click=${() => {this.activePane = 'login'}}>Login instead.</span></p>
</div>
</dees-form>
</div>
</div>
`;
}
public async firstUpdated() {
const domtoolsInstance = await this.domtoolsPromise;
const loginForm: DeesForm = this.shadowRoot.querySelector('#loginForm');
const loginPasswordInput: DeesInputText = loginForm.querySelector('#loginPasswordInput');
const loginSubmitButton: DeesFormSubmit = loginForm.querySelector('#loginSubmitButton');
const setButtonText = async () => {
if (loginPasswordInput.value) {
console.log('updating text of loginprompt.')
loginSubmitButton.text = 'Login';
} else {
loginSubmitButton.text = 'Send magic link (or enter password)';
}
};
loginForm.changeSubject.subscribe(() => {
console.log(`checking button text ${loginPasswordInput.value}`);
setButtonText();
});
setButtonText();
}
private login = async (valueArg: { emailAddress: string; passwordArg: string }) => {
// lets define the needed requests
const loginForm: DeesForm = this.shadowRoot.querySelector('#loginForm');
const loginRequestWithUsernameAndPassword =
new domtools.TypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmailOrUsernameAndPassword>(
WgLogin.receptionUrl,
'loginWithEmailOrUsernameAndPassword'
);
const loginRequestWithEmail =
new domtools.TypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmail>(
WgLogin.receptionUrl,
'loginWithEmail'
);
// lets do the actual logging in
if (valueArg.emailAddress && valueArg.passwordArg) {
loginForm.setStatus('pending', 'logging in...');
const response = await loginRequestWithUsernameAndPassword
.fire({
username: valueArg.emailAddress, // TODO: rename to emailAddress
password: valueArg.passwordArg,
})
.catch(() => {
loginForm.setStatus('error', 'could not log you in. Try Again!');
return;
});
if (!response) {
return;
}
if (response.refreshToken) {
loginForm.setStatus('pending', 'obtained refreshToken...');
const jwt = await this.handleRefreshToken(response.refreshToken, 0);
if (jwt) {
loginForm.setStatus('success', 'obtained jwt.');
} else {
loginForm.setStatus('error', 'something went wrong');
}
} else {
}
} else if (valueArg.emailAddress && !valueArg.passwordArg) {
loginForm.setStatus('pending', 'sending magic link...');
const response = await loginRequestWithEmail.fire({
email: valueArg.emailAddress,
});
if (response.status === 'ok') {
loginForm.setStatus('success', 'Please check your email!');
}
console.log(response);
}
};
private register = async (valueArg: { emailAddress: string }) => {
const registrationForm: DeesForm = this.shadowRoot.querySelector('#registrationForm');
registrationForm.setStatus('pending', 'registering...');
const firstSignupRequest =
new domtools.TypedRequest<plugins.idpInterfaces.request.IReq_FirstRegistration>(
WgLogin.receptionUrl,
'firstRegistrationRequest'
);
const response = await firstSignupRequest
.fire({
email: valueArg.emailAddress,
productSlugOfInterest: this.productOfInterest,
})
.catch((err) => {
registrationForm.setStatus('error', err.message);
return null;
});
if (response.status === 'ok') {
registrationForm.setStatus('success', 'Please check your email!');
}
console.log(response);
};
public async dispatchJwt(jwtArg?: string) {
if (jwtArg !== undefined) {
console.log(`dispatching jwt from loginprompt.`);
this.jwt = jwtArg;
await domtools.plugins.smartdelay.delayFor(200);
this.dispatchEvent(
new CustomEvent('leleLoginGotJwt', {
detail: {
jwt: this.jwt,
},
})
);
this.jwtObserable.next(this.jwt);
}
}
public async handleRefreshToken(refreshTokenArg: string, delayDispatchMillisArg = 0) {
// a refreshToken binds dierctly to a session.
// the refresh token is used on a continuous basis to get fresh and short-lived jwts
const refreshJwt = new domtools.TypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
WgLogin.receptionUrl,
'refreshJwt'
);
const responseJwt = await refreshJwt.fire({
refreshToken: refreshTokenArg,
});
if (responseJwt.jwt) {
this.domtools.convenience.smartdelay.delayFor(delayDispatchMillisArg).then(() => {
this.dispatchJwt(responseJwt.jwt);
});
return responseJwt.jwt;
} else {
return null;
}
}
}
+76
View File
@@ -0,0 +1,76 @@
import * as plugins from '../plugins.js';
import {
customElement,
DeesElement,
property,
html,
type TemplateResult,
css,
cssManager,
state,
domtools,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'wg-transfermanager': WgTransfermanager;
}
}
@customElement('wg-transfermanager')
export class WgTransfermanager extends DeesElement {
public appData: plugins.idpInterfaces.data.IApp;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
max-width: 520px;
margin-left: auto;
margin-right: auto;
color: ${cssManager.bdTheme('#333333', '#ffffff')};
}
.box {
cursor: pointer;
overflow: hidden;
transition: all 0.2s ease;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
border-radius: 3px;
margin-bottom: 20px;
height: 56px;
background: ${cssManager.bdTheme('#ffffff', '#181818')};
border-top: 1px solid ${cssManager.bdTheme('#ffffff', '#333333')};
text-align: center;
padding: 20px;
}
`,
];
public render() {
return html`
<div class="box">
transfering
</div>
`;
}
public async handleTransfer() {
const domtoolsInstance = await this.domtoolsPromise;
const box = this.shadowRoot.querySelector('.box');
const receptionClient = new plugins.idpClient.IdpClient(`https://reception.lossless.one:443`, this.appData);
const transferToken = await receptionClient.getTransferToken();
box.textContent = 'got transfer token...';
console.log(this.appData.appUrl);
const redirectUrl = domtoolsInstance.convenience.smarturl.Smarturl.createFromUrl(this.appData.appUrl, {
searchParams: {
'transfertoken': transferToken,
}
});
box.textContent = `redirecting to app...`;
window.location.href = redirectUrl.toString();
}
}
+73
View File
@@ -0,0 +1,73 @@
import * as plugins from '../plugins.js';
import {
customElement,
DeesElement,
property,
html,
cssManager,
unsafeCSS,
css,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('idp-welcome')
export class IdpWelcome extends DeesElement {
@property()
public someProperty = 'someProperty';
constructor() {
super();
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100px;
color: #fff;
font-family: 'Geist Sans';
}
:host([hidden]) {
font-family: 'Cal Sans';
display: none;
}
h1 {
text-align: center;
letter-spacing:0.0125em;
}
.textbox {
margin: 24px auto;
width: 500px;
background: #111111;
border-radius: 8px;
padding: 24px;
}
.textbox dees-button {
margin-top: 16px;
}
`,
];
public render(): TemplateResult {
return html`
<style></style>
<h1>idp.global</h1>
<div class="textbox">
idp.global is a Open Source identity provider for the world wide web. You can get the code if you want to improve it.
<dees-button @click=${() => {
window.location.href = 'https://code.foss.global/idp.global/idp.global';
}}>Get the code</dees-button>
</div>
<div class="textbox">
Do you want to sign in or register?
<dees-button @click=${() => {}}>Sign In</dees-button>
<dees-button @click=${() => {}}>Register</dees-button>
</div>
`;
}
}
+17
View File
@@ -0,0 +1,17 @@
import * as plugins from './plugins.js';
export class IdpState {
// STATIC
public static getSingletonInstance() {
if (!this.instance) {
this.instance = new IdpState();
}
return this.instance;
}
private static instance: IdpState;
// INSTANCE
public receptionUrl = 'https://reception.lossless.one/typedrequest';
public idpClient = new plugins.idpClient.IdpClient(this.receptionUrl);
}
+61
View File
@@ -0,0 +1,61 @@
import * as serviceworker from '@api.global/typedserver/web_serviceworker_client';
import * as domtools from '@design.estate/dees-domtools';
import { html, render } from '@design.estate/dees-element';
import { IdpWelcome } from './elements/idp-welcome.js';
const run = async () => {
const domtoolsInstance = await domtools.DomTools.setupDomTools();
domtools.elementBasic.setup();
domtoolsInstance.setWebsiteInfo({
metaObject: {
title: 'idp.global',
description:
'the code that runs idp.global',
canonicalDomain: 'https://idp.global',
ldCompany: {
name: 'Task Venture Capital GmbH',
status: 'active',
contact: {
address: {
name: 'Task Venture Capital GmbH',
city: 'Grasberg',
country: 'Germany',
houseNumber: '24',
postalCode: '28879',
streetName: 'Eickedorfer Vorweide',
},
description: 'work',
name: 'Task Venture Capital GmbH',
type: 'company',
facebookUrl: 'https://www.facebook.com/undefined variable',
twitterUrl: 'https://twitter.com/undefined variable',
website: 'https://Task Venture Capital GmbH',
phone: '+49 421 16767 548',
},
closedDate: null,
foundedDate: {
day: 1,
month: 1,
year: 2014,
},
},
},
});
const serviceWorker = await serviceworker.getServiceworkerClient();
const mainTemplate = html`
<style>
body {
margin: 0px;
--background-accent: #303f9f;
}
</style>
<idp-welcome></idp-welcome>
`;
render(mainTemplate, document.body);
};
run();
+17
View File
@@ -0,0 +1,17 @@
// node native
// project native
import * as idpInterfaces from '../dist_ts_interfaces/index.js';
import * as leleReceptionclient from '../dist_ts_idpclient/index.js';
export { idpInterfaces, leleReceptionclient as idpClient };
// @api.global scope
import * as typedrequest from '@api.global/typedrequest';
export { typedrequest };
// @design.estate scope
import * as deesCatalog from '@design.estate/dees-catalog';
export { deesCatalog };
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true
},
"exclude": [
"dist_*/**/*.d.ts"
]
}