feat(sdk): add initial browser and server authentication SDK exports
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
.nogit/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
|
||||
# builds
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
# caches
|
||||
.cache/
|
||||
.rpt2_cache
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"@git.zone/cli": {
|
||||
"schemaVersion": 2,
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "idp.global",
|
||||
"gitrepo": "sdk",
|
||||
"description": "Reusable browser and server SDK for idp.global authentication.",
|
||||
"npmPackagename": "@idp.global/sdk",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"idp.global",
|
||||
"sdk",
|
||||
"authentication",
|
||||
"oidc",
|
||||
"smartdata"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"targets": {
|
||||
"git": {
|
||||
"enabled": true,
|
||||
"remote": "origin"
|
||||
},
|
||||
"npm": {
|
||||
"enabled": false,
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
},
|
||||
"docker": {
|
||||
"enabled": false,
|
||||
"engine": "tsdocker"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ship.zone/szci": {}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## Pending
|
||||
|
||||
- Initial SDK package with browser and server exports.
|
||||
|
||||
### Features
|
||||
|
||||
- add initial browser and server authentication SDK exports (sdk)
|
||||
- introduces browser-side IdP client and typed request wrappers for authentication and organization workflows
|
||||
- adds server-side account store, password hashing, and authentication services with local and idp.global auth support
|
||||
- includes tests covering explicit account creation, local password authentication, and IdP email mismatch rejection
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Task Venture Capital GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "@idp.global/sdk",
|
||||
"version": "1.0.0",
|
||||
"private": false,
|
||||
"description": "Reusable browser and server SDK for idp.global authentication.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./dist_ts/index.js",
|
||||
"./browser": "./dist_ts_browser/index.js",
|
||||
"./server": "./dist_ts_server/index.js"
|
||||
},
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "pnpm run build && tstest test/ --verbose --logfile --timeout 60",
|
||||
"build": "tsbuild tsfolders --web --allowimplicitany",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.3.0",
|
||||
"@api.global/typedsocket": "^4.1.2",
|
||||
"@idp.global/interfaces": "file:../interfaces",
|
||||
"@push.rocks/smartdata": "^7.1.7",
|
||||
"@push.rocks/smartjson": "^6.0.1",
|
||||
"@push.rocks/smartpromise": "^4.2.4",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smarttime": "^4.2.3",
|
||||
"@push.rocks/smarturl": "^3.1.0",
|
||||
"@push.rocks/webjwt": "^1.0.10",
|
||||
"@push.rocks/webstore": "^2.0.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsdoc": "^2.0.3",
|
||||
"@git.zone/tsrun": "^2.0.3",
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@push.rocks/smartdb": "^2.10.0",
|
||||
"@types/node": "^25.6.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"ts_browser/**/*",
|
||||
"ts_server/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"readme.md",
|
||||
"changelog.md",
|
||||
"license"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@code.foss.global:29419/idp.global/sdk.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://code.foss.global/idp.global/sdk/issues"
|
||||
},
|
||||
"homepage": "https://code.foss.global/idp.global/sdk#readme",
|
||||
"keywords": [
|
||||
"idp.global",
|
||||
"sdk",
|
||||
"authentication",
|
||||
"oidc",
|
||||
"smartdata"
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 Chrome versions"
|
||||
],
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
}
|
||||
Generated
+9699
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
# @idp.global/sdk
|
||||
|
||||
Reusable SDK for idp.global browser clients and server-side account authentication.
|
||||
|
||||
Use explicit runtime exports:
|
||||
|
||||
- `@idp.global/sdk/browser`
|
||||
- `@idp.global/sdk/server`
|
||||
|
||||
The server account store never creates accounts automatically. Consumers must explicitly create persisted accounts.
|
||||
@@ -0,0 +1,109 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartdata from '@push.rocks/smartdata';
|
||||
import * as smartdb from '@push.rocks/smartdb';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import { AccountAuthService, SmartdataAccountStore } from '../ts_server/index.js';
|
||||
|
||||
const createTestStore = async () => {
|
||||
const storagePath = path.join(os.tmpdir(), `idp-sdk-account-store-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
||||
const localSmartDb = new smartdb.LocalSmartDb({ folderPath: storagePath });
|
||||
const connectionInfo = await localSmartDb.start();
|
||||
const smartdataDb = new smartdata.SmartdataDb({
|
||||
mongoDbUrl: connectionInfo.connectionUri,
|
||||
mongoDbName: `idp-sdk-test-${Date.now()}`,
|
||||
});
|
||||
await smartdataDb.init();
|
||||
const store = new SmartdataAccountStore({ smartdataDb });
|
||||
await smartdataDb.mongoDb.createCollection('__test_init');
|
||||
return {
|
||||
store,
|
||||
async cleanup() {
|
||||
await smartdataDb.close();
|
||||
await localSmartDb.stop();
|
||||
await fs.promises.rm(storagePath, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
let testStore: Awaited<ReturnType<typeof createTestStore>>;
|
||||
|
||||
tap.test('setup account store test db', async () => {
|
||||
testStore = await createTestStore();
|
||||
});
|
||||
|
||||
tap.test('SmartdataAccountStore only persists explicitly created accounts', async () => {
|
||||
expect(await testStore.store.hasActiveAdminAccount()).toEqual(false);
|
||||
expect(await testStore.store.getAccountByEmail('admin@example.com')).toBeNull();
|
||||
|
||||
const account = await testStore.store.createAccount({
|
||||
email: 'Admin@Example.com',
|
||||
name: 'Admin User',
|
||||
role: 'admin',
|
||||
authSources: ['local'],
|
||||
password: 'correct horse battery staple',
|
||||
});
|
||||
|
||||
expect(account.emailNormalized).toEqual('admin@example.com');
|
||||
expect(await testStore.store.hasActiveAdminAccount()).toEqual(true);
|
||||
expect(account.passwordHash).not.toEqual('correct horse battery staple');
|
||||
});
|
||||
|
||||
tap.test('AccountAuthService authenticates local accounts only with valid password', async () => {
|
||||
const authService = new AccountAuthService({ store: testStore.store });
|
||||
|
||||
expect(await authService.authenticate({
|
||||
email: 'admin@example.com',
|
||||
password: 'wrong',
|
||||
authSource: 'local',
|
||||
})).toBeNull();
|
||||
|
||||
const result = await authService.authenticate({
|
||||
email: 'ADMIN@example.com',
|
||||
password: 'correct horse battery staple',
|
||||
authSource: 'local',
|
||||
});
|
||||
expect(result?.account.role).toEqual('admin');
|
||||
expect(result?.authSource).toEqual('local');
|
||||
});
|
||||
|
||||
tap.test('AccountAuthService rejects IdP email mismatches', async () => {
|
||||
await testStore.store.createAccount({
|
||||
email: 'idp-admin@example.com',
|
||||
name: 'IdP Admin User',
|
||||
role: 'admin',
|
||||
authSources: ['idp.global'],
|
||||
});
|
||||
const authService = new AccountAuthService({
|
||||
store: testStore.store,
|
||||
idpClient: {
|
||||
loginWithEmailAndPassword: async () => ({
|
||||
jwt: 'jwt',
|
||||
refreshToken: 'refresh',
|
||||
user: {
|
||||
id: 'idp-user-1',
|
||||
data: {
|
||||
name: 'Other User',
|
||||
username: 'other@example.com',
|
||||
email: 'other@example.com',
|
||||
status: 'active',
|
||||
connectedOrgs: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(await authService.authenticate({
|
||||
email: 'idp-admin@example.com',
|
||||
password: 'idp-password',
|
||||
authSource: 'idp.global',
|
||||
})).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('cleanup account store test db', async () => {
|
||||
await testStore.cleanup();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,318 @@
|
||||
import { IdpRequests } from './classes.idprequests.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export class IdpClient {
|
||||
private helpers = {
|
||||
async extractDataFromJwtString(jwtString: string): Promise<plugins.idpInterfaces.data.IJwt> {
|
||||
return plugins.webjwt.getDataFromJwtString(jwtString);
|
||||
},
|
||||
};
|
||||
|
||||
public appData: plugins.idpInterfaces.data.IAppLegacy;
|
||||
public rolesReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1);
|
||||
public organizationsReplaySubject = new plugins.smartrx.rxjs.ReplaySubject(1);
|
||||
|
||||
public parsedReceptionUrl: plugins.smarturl.Smarturl;
|
||||
|
||||
constructor(receptionBaseUrlArg: string, appDataArg?: plugins.idpInterfaces.data.IAppLegacy) {
|
||||
if (receptionBaseUrlArg.endsWith('/')) {
|
||||
receptionBaseUrlArg = receptionBaseUrlArg.slice(0, -1);
|
||||
}
|
||||
if (!receptionBaseUrlArg.endsWith('/typedrequest')) {
|
||||
receptionBaseUrlArg = `${receptionBaseUrlArg}/typedrequest`;
|
||||
}
|
||||
this.parsedReceptionUrl = plugins.smarturl.Smarturl.createFromUrl(receptionBaseUrlArg);
|
||||
if (!appDataArg) {
|
||||
appDataArg = {
|
||||
id: '',
|
||||
appUrl: typeof window !== 'undefined' ? `https://${window.location.host}/` : '',
|
||||
description: '',
|
||||
logoUrl: '',
|
||||
name: '',
|
||||
};
|
||||
}
|
||||
this.appData = appDataArg;
|
||||
}
|
||||
|
||||
public requests = new IdpRequests(this);
|
||||
|
||||
public checkWetherOnReceptionDomain() {
|
||||
return plugins.smarturl.Smarturl.createFromUrl(window.location.href).hostname ===
|
||||
this.parsedReceptionUrl.hostname;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return plugins.smartjson.parseBase64(appDataString);
|
||||
}
|
||||
|
||||
public async setJwt(jwtStringArg: string) {
|
||||
await this.storeJwt(jwtStringArg);
|
||||
}
|
||||
|
||||
public async setRefreshToken(refreshTokenArg: string) {
|
||||
await this.storeRefreshToken(refreshTokenArg);
|
||||
}
|
||||
|
||||
public typedsocket!: plugins.typedsocket.TypedSocket;
|
||||
public typedrouter: any = new plugins.typedrequest.TypedRouter();
|
||||
public statusObservable = new plugins.smartrx.rxjs.Subject<plugins.idpInterfaces.data.TLoginStatus>();
|
||||
|
||||
public ssoStore = new plugins.webstore.WebStore({
|
||||
storeName: 'idpglobalStore',
|
||||
dbName: 'main',
|
||||
});
|
||||
|
||||
public async storeJwt(jwtString: string) {
|
||||
await this.ssoStore.set('idpJwt', jwtString);
|
||||
}
|
||||
|
||||
public async storeRefreshToken(refreshToken: string) {
|
||||
await this.ssoStore.set('idpRefreshToken', refreshToken);
|
||||
}
|
||||
|
||||
public async getJwt(): Promise<string> {
|
||||
return await this.ssoStore.get('idpJwt');
|
||||
}
|
||||
|
||||
public async getRefreshToken(): Promise<string> {
|
||||
return await this.ssoStore.get('idpRefreshToken');
|
||||
}
|
||||
|
||||
public async getJwtData(): Promise<plugins.idpInterfaces.data.IJwt> {
|
||||
return this.helpers.extractDataFromJwtString(await this.getJwt());
|
||||
}
|
||||
|
||||
public async deleteJwt() {
|
||||
await this.ssoStore.delete('idpJwt');
|
||||
}
|
||||
|
||||
public async deleteRefreshToken() {
|
||||
await this.ssoStore.delete('idpRefreshToken');
|
||||
}
|
||||
|
||||
public async clearAuthState() {
|
||||
await Promise.all([this.deleteJwt(), this.deleteRefreshToken()]);
|
||||
}
|
||||
|
||||
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) {
|
||||
await this.deleteJwt();
|
||||
jwt = await this.refreshJwt();
|
||||
}
|
||||
return jwt;
|
||||
}
|
||||
|
||||
public async refreshJwt(refreshTokenArg?: string): Promise<string | null> {
|
||||
const refreshToken = refreshTokenArg || (await this.getRefreshToken());
|
||||
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.typedsocketDeferred.promise;
|
||||
const refreshJwtReq = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>('refreshJwt');
|
||||
const response = await refreshJwtReq
|
||||
.fire({ refreshToken })
|
||||
.catch(async () => {
|
||||
await this.clearAuthState();
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!response?.jwt) {
|
||||
await this.clearAuthState();
|
||||
this.statusObservable.next(response?.status || 'loggedOut');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.refreshToken) {
|
||||
await this.storeRefreshToken(response.refreshToken);
|
||||
}
|
||||
await this.storeJwt(response.jwt);
|
||||
this.statusObservable.next(response.status);
|
||||
return response.jwt;
|
||||
}
|
||||
|
||||
public async getTransferToken(appDataArg?: plugins.idpInterfaces.data.IAppLegacy): Promise<string | null> {
|
||||
await this.performJwtHousekeeping();
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
await this.typedsocketDeferred.promise;
|
||||
const getTransferToken = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ExchangeRefreshTokenAndTransferToken>('exchangeRefreshTokenAndTransferToken');
|
||||
const response = await getTransferToken.fire({
|
||||
refreshToken,
|
||||
appData: appDataArg || this.appData,
|
||||
});
|
||||
return response.transferToken;
|
||||
}
|
||||
|
||||
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 },
|
||||
});
|
||||
window.location.href = urlInstance.toString();
|
||||
}
|
||||
|
||||
public async processTransferToken(): Promise<boolean> {
|
||||
const href = window.location.href;
|
||||
const url = plugins.smarturl.Smarturl.createFromUrl(href);
|
||||
const transferToken = url.searchParams['transfertoken'];
|
||||
if (transferToken) {
|
||||
await this.typedsocketDeferred.promise;
|
||||
const getTransferToken = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ExchangeRefreshTokenAndTransferToken>('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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async checkJwtPresent() {
|
||||
const jwt = await this.performJwtHousekeeping();
|
||||
return !!jwt;
|
||||
}
|
||||
|
||||
public async determineLoginStatus(requireLoginArg: boolean = false): Promise<boolean> {
|
||||
const jwtPresent = await this.checkJwtPresent();
|
||||
if (jwtPresent) {
|
||||
const jwt = await this.performJwtHousekeeping();
|
||||
return !!jwt;
|
||||
}
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (refreshToken) {
|
||||
const jwt = await this.refreshJwt(refreshToken);
|
||||
if (jwt) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const transferTokenResult = await this.processTransferToken();
|
||||
if (transferTokenResult) {
|
||||
return true;
|
||||
}
|
||||
if (requireLoginArg) {
|
||||
const urlInstance = plugins.smarturl.Smarturl.createFromUrl(
|
||||
this.parsedReceptionUrl.clone().set('path', '/login').toString(),
|
||||
{
|
||||
searchParams: {
|
||||
appdata: plugins.smartjson.stringifyBase64(this.appData),
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!globalThis.location.href.startsWith(this.parsedReceptionUrl.toString())) {
|
||||
globalThis.location.href = urlInstance.toString();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
const idpLogoutUrl = this.parsedReceptionUrl.clone().set('path', '/logout');
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (!globalThis.location.href.startsWith(idpLogoutUrl.origin)) {
|
||||
await this.clearAuthState();
|
||||
globalThis.location.href = idpLogoutUrl.toString();
|
||||
return;
|
||||
}
|
||||
if (!refreshToken) {
|
||||
await this.clearAuthState();
|
||||
window.location.href = this.parsedReceptionUrl.origin;
|
||||
return;
|
||||
}
|
||||
await this.enableTypedSocket();
|
||||
const logoutTr = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.ILogoutRequest>('logout');
|
||||
await logoutTr.fire({ refreshToken });
|
||||
await this.clearAuthState();
|
||||
const appData = await this.getAppDataOnSsoDomain();
|
||||
if (appData) {
|
||||
window.location.href = appData.appUrl;
|
||||
} else if (window.location.href.startsWith(idpLogoutUrl.origin)) {
|
||||
window.location.href = this.parsedReceptionUrl.origin;
|
||||
}
|
||||
}
|
||||
|
||||
public typedsocketDeferred = plugins.smartpromise.defer<plugins.typedsocket.TypedSocket>();
|
||||
|
||||
public async enableTypedSocket() {
|
||||
if (this.typedsocketDeferred.claimed) {
|
||||
return this.typedsocketDeferred.promise;
|
||||
}
|
||||
this.typedsocketDeferred.claim();
|
||||
this.typedsocket = await plugins.typedsocket.TypedSocket.createClient(
|
||||
this.typedrouter,
|
||||
this.parsedReceptionUrl.toString(),
|
||||
);
|
||||
this.typedsocketDeferred.resolve(this.typedsocket);
|
||||
return this.typedsocketDeferred.promise;
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
await this.typedsocket?.stop();
|
||||
}
|
||||
|
||||
public async createOrganization(orgNameArg: string, orgSlugArg: string, modeArg: 'checkAvailability' | 'manifest') {
|
||||
await this.typedsocketDeferred.promise;
|
||||
const validateOrg = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CreateOrganization>('createOrganization');
|
||||
return validateOrg.fire({
|
||||
jwt: await this.getJwt(),
|
||||
action: modeArg,
|
||||
organizationName: orgNameArg,
|
||||
organizationSlug: orgSlugArg,
|
||||
userId: (await this.getJwtData()).id,
|
||||
});
|
||||
}
|
||||
|
||||
public async getRolesAndOrganizations() {
|
||||
await this.typedsocketDeferred.promise;
|
||||
const rolesAndOrganizationsForUserId = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetRolesAndOrganizationsForUserId>('getRolesAndOrganizationsForUserId');
|
||||
return rolesAndOrganizationsForUserId.fire({
|
||||
jwt: await this.getJwt(),
|
||||
userId: (await this.getJwtData()).id,
|
||||
});
|
||||
}
|
||||
|
||||
public async updatePaddleCheckoutId(orgIdArg: string, checkoutIdArg: string) {
|
||||
await this.typedsocketDeferred.promise;
|
||||
const updateBillingPlan = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdatePaymentMethod>('updatePaymentMethod');
|
||||
return updateBillingPlan.fire({
|
||||
jwtString: await this.getJwt(),
|
||||
orgId: orgIdArg,
|
||||
paddle: { checkoutId: checkoutIdArg },
|
||||
});
|
||||
}
|
||||
|
||||
public async whoIs() {
|
||||
await this.typedsocketDeferred.promise;
|
||||
const whoIs = this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_WhoIs>('whoIs');
|
||||
return whoIs.fire({ jwt: await this.getJwt() });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import type { IdpClient } from './classes.idpclient.js';
|
||||
|
||||
export class IdpRequests {
|
||||
constructor(private idpClientArg: IdpClient) {}
|
||||
|
||||
public get afterRegistrationEmailClicked() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_AfterRegistrationEmailClicked>('afterRegistrationEmailClicked');
|
||||
}
|
||||
|
||||
public get setData() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_SetDataForRegistration>('setDataForRegistration');
|
||||
}
|
||||
|
||||
public get mobileNumberVerification() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_MobileVerificationForRegistration>('mobileVerificationForRegistration');
|
||||
}
|
||||
|
||||
public get finishRegistration() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_FinishRegistration>('finishRegistration');
|
||||
}
|
||||
|
||||
public get loginWithUserNameAndPassword() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmailOrUsernameAndPassword>('loginWithEmailOrUsernameAndPassword');
|
||||
}
|
||||
|
||||
public get obtainJwt() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>('refreshJwt');
|
||||
}
|
||||
|
||||
public get obtainOneTimeToken() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ExchangeRefreshTokenAndTransferToken>('exchangeRefreshTokenAndTransferToken');
|
||||
}
|
||||
|
||||
public get loginWithEmail() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmail>('loginWithEmail');
|
||||
}
|
||||
|
||||
public get loginWithEmailAfterToken() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmailAfterEmailTokenAquired>('loginWithEmailAfterEmailTokenAquired');
|
||||
}
|
||||
|
||||
public get loginWithApiToken() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_LoginWithApiToken>('loginWithApiToken');
|
||||
}
|
||||
|
||||
public get completeOidcAuthorization() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CompleteOidcAuthorization>('completeOidcAuthorization');
|
||||
}
|
||||
|
||||
public get prepareOidcAuthorization() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_PrepareOidcAuthorization>('prepareOidcAuthorization');
|
||||
}
|
||||
|
||||
public get resetPassword() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ResetPassword>('resetPassword');
|
||||
}
|
||||
|
||||
public get setNewPassword() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_SetNewPassword>('setNewPassword');
|
||||
}
|
||||
|
||||
public get obtainDeviceId() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ObtainDeviceId>('obtainDeviceId');
|
||||
}
|
||||
|
||||
public get attachDeviceId() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_AttachDeviceId>('attachDeviceId');
|
||||
}
|
||||
|
||||
public get firstRegistration() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_FirstRegistration>('firstRegistrationRequest');
|
||||
}
|
||||
|
||||
public get getUserData() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetUserData>('getUserData');
|
||||
}
|
||||
|
||||
public get setUserData() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_SetUserData>('setUserData');
|
||||
}
|
||||
|
||||
public get getUserSessions() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetUserSessions>('getUserSessions');
|
||||
}
|
||||
|
||||
public get revokeSession() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RevokeSession>('revokeSession');
|
||||
}
|
||||
|
||||
public get getUserActivity() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetUserActivity>('getUserActivity');
|
||||
}
|
||||
|
||||
public get getOrganizationById() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetOrganizationById>('getOrganizationById');
|
||||
}
|
||||
|
||||
public get updateOrganization() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateOrganization>('updateOrganization');
|
||||
}
|
||||
|
||||
public get deleteOrganization() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_DeleteOrganization>('deleteOrganization');
|
||||
}
|
||||
|
||||
public get getOrgRoleDefinitions() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetOrgRoleDefinitions>('getOrgRoleDefinitions');
|
||||
}
|
||||
|
||||
public get upsertOrgRoleDefinition() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpsertOrgRoleDefinition>('upsertOrgRoleDefinition');
|
||||
}
|
||||
|
||||
public get deleteOrgRoleDefinition() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_DeleteOrgRoleDefinition>('deleteOrgRoleDefinition');
|
||||
}
|
||||
|
||||
public get createInvitation() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CreateInvitation>('createInvitation');
|
||||
}
|
||||
|
||||
public get getOrgInvitations() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetOrgInvitations>('getOrgInvitations');
|
||||
}
|
||||
|
||||
public get getOrgMembers() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetOrgMembers>('getOrgMembers');
|
||||
}
|
||||
|
||||
public get cancelInvitation() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CancelInvitation>('cancelInvitation');
|
||||
}
|
||||
|
||||
public get resendInvitation() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ResendInvitation>('resendInvitation');
|
||||
}
|
||||
|
||||
public get removeMember() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RemoveMember>('removeMember');
|
||||
}
|
||||
|
||||
public get updateMemberRoles() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateMemberRoles>('updateMemberRoles');
|
||||
}
|
||||
|
||||
public get transferOwnership() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_TransferOwnership>('transferOwnership');
|
||||
}
|
||||
|
||||
public get getInvitationByToken() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetInvitationByToken>('getInvitationByToken');
|
||||
}
|
||||
|
||||
public get acceptInvitation() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_AcceptInvitation>('acceptInvitation');
|
||||
}
|
||||
|
||||
public get bulkCreateInvitations() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_BulkCreateInvitations>('bulkCreateInvitations');
|
||||
}
|
||||
|
||||
public get updateAppRoleMappings() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateAppRoleMappings>('updateAppRoleMappings');
|
||||
}
|
||||
|
||||
public get getBillingPlan() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetBillingPlan>('getBillingPlan');
|
||||
}
|
||||
|
||||
public get getPaddleConfig() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetPaddleConfig>('getPaddleConfig');
|
||||
}
|
||||
|
||||
public get getPublicKeyForValidation() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetPublicKeyForValidation>('getPublicKeyForValidation');
|
||||
}
|
||||
|
||||
public get pushPublicKeyForValidation() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_PushPublicKeyForValidation>('pushPublicKeyForValidation');
|
||||
}
|
||||
|
||||
public get pushOrGetJwtIdBlocklist() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_PushOrGetJwtIdBlocklist>('pushOrGetJwtIdBlocklist');
|
||||
}
|
||||
|
||||
public get suspendUser() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_SuspendUser>('suspendUser');
|
||||
}
|
||||
|
||||
public get deleteSuspendedUser() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IDeleteSuspendedUser>('deleteSuspendedUser');
|
||||
}
|
||||
|
||||
public get checkGlobalAdmin() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CheckGlobalAdmin>('checkGlobalAdmin');
|
||||
}
|
||||
|
||||
public get getGlobalAppStats() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_GetGlobalAppStats>('getGlobalAppStats');
|
||||
}
|
||||
|
||||
public get createGlobalApp() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_CreateGlobalApp>('createGlobalApp');
|
||||
}
|
||||
|
||||
public get updateGlobalApp() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_UpdateGlobalApp>('updateGlobalApp');
|
||||
}
|
||||
|
||||
public get deleteGlobalApp() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_DeleteGlobalApp>('deleteGlobalApp');
|
||||
}
|
||||
|
||||
public get regenerateAppCredentials() {
|
||||
return this.idpClientArg.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RegenerateAppCredentials>('regenerateAppCredentials');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './classes.idpclient.js';
|
||||
export * from './classes.idprequests.js';
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as idpInterfaces from '@idp.global/interfaces';
|
||||
|
||||
export { idpInterfaces };
|
||||
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import * as typedsocket from '@api.global/typedsocket';
|
||||
|
||||
export { typedrequest, typedsocket };
|
||||
|
||||
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 };
|
||||
@@ -0,0 +1,61 @@
|
||||
import { IdpGlobalServerClient } from './classes.idp-global-server-client.js';
|
||||
import { SmartdataAccountStore } from './classes.account-store.js';
|
||||
import type { IAuthenticateAccountOptions, IAuthenticatedAccountResult, IIdpSdkAccount } from './interfaces.js';
|
||||
|
||||
export class AccountAuthService {
|
||||
constructor(private optionsArg: {
|
||||
store: SmartdataAccountStore;
|
||||
idpClient?: IdpGlobalServerClient;
|
||||
}) {}
|
||||
|
||||
public async authenticate(optionsArg: IAuthenticateAccountOptions): Promise<IAuthenticatedAccountResult | null> {
|
||||
const account = await this.optionsArg.store.getAccountByEmail(optionsArg.email);
|
||||
if (!account || account.status !== 'active') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const authSource = optionsArg.authSource || 'auto';
|
||||
if ((authSource === 'local' || authSource === 'auto') && account.authSources.includes('local')) {
|
||||
const localOk = await this.optionsArg.store.verifyLocalPassword(account, optionsArg.password);
|
||||
if (localOk) {
|
||||
const updatedAccount = await this.optionsArg.store.updateLoginState(account.id, {});
|
||||
return { account: updatedAccount || account, authSource: 'local' };
|
||||
}
|
||||
if (authSource === 'local') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ((authSource === 'idp.global' || authSource === 'auto') && account.authSources.includes('idp.global')) {
|
||||
return this.authenticateWithIdp(account, optionsArg.password);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async authenticateWithIdp(accountArg: IIdpSdkAccount, passwordArg: string): Promise<IAuthenticatedAccountResult | null> {
|
||||
if (!this.optionsArg.idpClient) {
|
||||
return null;
|
||||
}
|
||||
const idpResult = await this.optionsArg.idpClient.loginWithEmailAndPassword({
|
||||
email: accountArg.email,
|
||||
password: passwordArg,
|
||||
});
|
||||
const idpEmail = this.optionsArg.store.normalizeEmail(idpResult.user.data.email);
|
||||
if (idpEmail !== accountArg.emailNormalized) {
|
||||
return null;
|
||||
}
|
||||
if (accountArg.idpSubject && accountArg.idpSubject !== idpResult.user.id) {
|
||||
return null;
|
||||
}
|
||||
const updatedAccount = await this.optionsArg.store.updateLoginState(accountArg.id, {
|
||||
idpSubject: accountArg.idpSubject || idpResult.user.id,
|
||||
});
|
||||
return {
|
||||
account: updatedAccount || accountArg,
|
||||
authSource: 'idp.global',
|
||||
idpJwt: idpResult.jwt,
|
||||
idpRefreshToken: idpResult.refreshToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import type { IIdpSdkAccount, TIdpAccountAuthSource, TIdpAccountRole, TIdpAccountStatus } from './interfaces.js';
|
||||
|
||||
let activeSmartdataDb: plugins.smartdata.SmartdataDb | null = null;
|
||||
|
||||
export const setAccountDocSmartdataDb = (smartdataDbArg: plugins.smartdata.SmartdataDb) => {
|
||||
activeSmartdataDb = smartdataDbArg;
|
||||
};
|
||||
|
||||
const getDb = () => {
|
||||
if (!activeSmartdataDb) {
|
||||
throw new Error('IdpSdkAccountDoc has no SmartdataDb configured');
|
||||
}
|
||||
return activeSmartdataDb;
|
||||
};
|
||||
|
||||
@plugins.smartdata.Collection(() => getDb())
|
||||
export class IdpSdkAccountDoc extends plugins.smartdata.SmartDataDbDoc<IdpSdkAccountDoc, IdpSdkAccountDoc> {
|
||||
@plugins.smartdata.unI()
|
||||
@plugins.smartdata.svDb()
|
||||
public id!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public email!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public emailNormalized!: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public name: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public role!: TIdpAccountRole;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public status!: TIdpAccountStatus;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public authSources!: TIdpAccountAuthSource[];
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public passwordHash?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public idpSubject?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public createdAt!: number;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public updatedAt!: number;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public lastLoginAt?: number;
|
||||
|
||||
public toAccount(): IIdpSdkAccount {
|
||||
return {
|
||||
id: this.id,
|
||||
email: this.email,
|
||||
emailNormalized: this.emailNormalized,
|
||||
name: this.name,
|
||||
role: this.role,
|
||||
status: this.status,
|
||||
authSources: this.authSources || [],
|
||||
passwordHash: this.passwordHash,
|
||||
idpSubject: this.idpSubject,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
lastLoginAt: this.lastLoginAt,
|
||||
};
|
||||
}
|
||||
|
||||
public static async findById(idArg: string): Promise<IdpSdkAccountDoc | null> {
|
||||
return IdpSdkAccountDoc.getInstance({ id: idArg });
|
||||
}
|
||||
|
||||
public static async findByEmailNormalized(emailNormalizedArg: string): Promise<IdpSdkAccountDoc | null> {
|
||||
return IdpSdkAccountDoc.getInstance({ emailNormalized: emailNormalizedArg });
|
||||
}
|
||||
|
||||
public static async findAdmins(): Promise<IdpSdkAccountDoc[]> {
|
||||
return IdpSdkAccountDoc.getInstances({ role: 'admin', status: 'active' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { IdpSdkAccountDoc, setAccountDocSmartdataDb } from './classes.account-doc.js';
|
||||
import { PasswordHasher } from './classes.password-hasher.js';
|
||||
import type { ICreateIdpSdkAccountOptions, IIdpSdkAccount, TIdpAccountAuthSource } from './interfaces.js';
|
||||
|
||||
export class SmartdataAccountStore {
|
||||
constructor(private optionsArg: { smartdataDb: plugins.smartdata.SmartdataDb }) {
|
||||
setAccountDocSmartdataDb(optionsArg.smartdataDb);
|
||||
}
|
||||
|
||||
public normalizeEmail(emailArg: string): string {
|
||||
return emailArg.trim().toLowerCase();
|
||||
}
|
||||
|
||||
public async createAccount(optionsArg: ICreateIdpSdkAccountOptions): Promise<IIdpSdkAccount> {
|
||||
const emailNormalized = this.normalizeEmail(optionsArg.email);
|
||||
if (!emailNormalized || !emailNormalized.includes('@')) {
|
||||
throw new Error('A valid account email is required');
|
||||
}
|
||||
const existing = await IdpSdkAccountDoc.findByEmailNormalized(emailNormalized);
|
||||
if (existing) {
|
||||
throw new Error(`Account already exists for ${emailNormalized}`);
|
||||
}
|
||||
|
||||
const authSources = this.normalizeAuthSources(optionsArg.authSources);
|
||||
if (authSources.length === 0) {
|
||||
throw new Error('At least one auth source is required');
|
||||
}
|
||||
if (authSources.includes('local') && !optionsArg.password) {
|
||||
throw new Error('A local password is required for local auth');
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const doc = new IdpSdkAccountDoc();
|
||||
doc.id = plugins.crypto.randomUUID();
|
||||
doc.email = optionsArg.email.trim();
|
||||
doc.emailNormalized = emailNormalized;
|
||||
doc.name = optionsArg.name.trim() || doc.email;
|
||||
doc.role = optionsArg.role;
|
||||
doc.status = optionsArg.status || 'active';
|
||||
doc.authSources = authSources;
|
||||
doc.passwordHash = optionsArg.password ? await PasswordHasher.hashPassword(optionsArg.password) : undefined;
|
||||
doc.idpSubject = optionsArg.idpSubject;
|
||||
doc.createdAt = now;
|
||||
doc.updatedAt = now;
|
||||
await doc.save();
|
||||
return doc.toAccount();
|
||||
}
|
||||
|
||||
public async getAccountByEmail(emailArg: string): Promise<IIdpSdkAccount | null> {
|
||||
const doc = await IdpSdkAccountDoc.findByEmailNormalized(this.normalizeEmail(emailArg));
|
||||
return doc?.toAccount() || null;
|
||||
}
|
||||
|
||||
public async getAccountById(idArg: string): Promise<IIdpSdkAccount | null> {
|
||||
const doc = await IdpSdkAccountDoc.findById(idArg);
|
||||
return doc?.toAccount() || null;
|
||||
}
|
||||
|
||||
public async listAccounts(): Promise<IIdpSdkAccount[]> {
|
||||
const docs = await IdpSdkAccountDoc.getInstances({});
|
||||
return docs.map((docArg) => docArg.toAccount());
|
||||
}
|
||||
|
||||
public async hasActiveAdminAccount(): Promise<boolean> {
|
||||
const admins = await IdpSdkAccountDoc.findAdmins();
|
||||
return admins.length > 0;
|
||||
}
|
||||
|
||||
public async verifyLocalPassword(accountArg: IIdpSdkAccount, passwordArg: string): Promise<boolean> {
|
||||
if (accountArg.status !== 'active' || !accountArg.authSources.includes('local')) {
|
||||
return false;
|
||||
}
|
||||
return PasswordHasher.verifyPassword(passwordArg, accountArg.passwordHash);
|
||||
}
|
||||
|
||||
public async updateLoginState(accountIdArg: string, patchArg: { idpSubject?: string }): Promise<IIdpSdkAccount | null> {
|
||||
const doc = await IdpSdkAccountDoc.findById(accountIdArg);
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
if (patchArg.idpSubject !== undefined) {
|
||||
doc.idpSubject = patchArg.idpSubject;
|
||||
}
|
||||
doc.lastLoginAt = Date.now();
|
||||
doc.updatedAt = Date.now();
|
||||
await doc.save();
|
||||
return doc.toAccount();
|
||||
}
|
||||
|
||||
private normalizeAuthSources(authSourcesArg: TIdpAccountAuthSource[]): TIdpAccountAuthSource[] {
|
||||
return [...new Set(authSourcesArg.filter((sourceArg) => sourceArg === 'local' || sourceArg === 'idp.global'))];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export interface IIdpGlobalServerClientOptions {
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
export interface IIdpPasswordAuthResult {
|
||||
user: plugins.idpInterfaces.data.IUser;
|
||||
jwt: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export class IdpGlobalServerClient {
|
||||
private typedrouter: any = new plugins.typedrequest.TypedRouter();
|
||||
private typedsocket?: plugins.typedsocket.TypedSocket;
|
||||
private typedsocketDeferred = plugins.smartpromise.defer<plugins.typedsocket.TypedSocket>();
|
||||
|
||||
constructor(private optionsArg: IIdpGlobalServerClientOptions) {}
|
||||
|
||||
public async connect(): Promise<plugins.typedsocket.TypedSocket> {
|
||||
if (this.typedsocketDeferred.claimed) {
|
||||
return this.typedsocketDeferred.promise;
|
||||
}
|
||||
this.typedsocketDeferred.claim();
|
||||
this.typedsocket = await plugins.typedsocket.TypedSocket.createClient(
|
||||
this.typedrouter,
|
||||
this.getTypedRequestUrl(),
|
||||
);
|
||||
this.typedsocketDeferred.resolve(this.typedsocket);
|
||||
return this.typedsocketDeferred.promise;
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
await this.typedsocket?.stop();
|
||||
this.typedsocket = undefined;
|
||||
}
|
||||
|
||||
public async loginWithEmailAndPassword(optionsArg: { email: string; password: string }): Promise<IIdpPasswordAuthResult> {
|
||||
const socket = await this.connect();
|
||||
const loginRequest = socket.createTypedRequest<plugins.idpInterfaces.request.IReq_LoginWithEmailOrUsernameAndPassword>('loginWithEmailOrUsernameAndPassword');
|
||||
const loginResponse = await loginRequest.fire({
|
||||
username: optionsArg.email,
|
||||
password: optionsArg.password,
|
||||
});
|
||||
if (!loginResponse.refreshToken || loginResponse.twoFaNeeded) {
|
||||
throw new Error(loginResponse.twoFaNeeded ? 'Two-factor authentication is required' : 'IdP login failed');
|
||||
}
|
||||
|
||||
const refreshRequest = socket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>('refreshJwt');
|
||||
const refreshResponse = await refreshRequest.fire({ refreshToken: loginResponse.refreshToken });
|
||||
if (!refreshResponse.jwt) {
|
||||
throw new Error('IdP did not return a JWT');
|
||||
}
|
||||
|
||||
const whoIsRequest = socket.createTypedRequest<plugins.idpInterfaces.request.IReq_WhoIs>('whoIs');
|
||||
const whoIsResponse = await whoIsRequest.fire({ jwt: refreshResponse.jwt });
|
||||
return {
|
||||
user: whoIsResponse.user,
|
||||
jwt: refreshResponse.jwt,
|
||||
refreshToken: refreshResponse.refreshToken || loginResponse.refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
private getTypedRequestUrl(): string {
|
||||
let baseUrl = this.optionsArg.baseUrl.trim();
|
||||
if (baseUrl.endsWith('/')) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
if (!baseUrl.endsWith('/typedrequest')) {
|
||||
baseUrl = `${baseUrl}/typedrequest`;
|
||||
}
|
||||
return baseUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
const HASH_PREFIX = 'scrypt:v1';
|
||||
|
||||
export class PasswordHasher {
|
||||
public static async hashPassword(passwordArg: string): Promise<string> {
|
||||
const salt = plugins.crypto.randomBytes(16).toString('base64url');
|
||||
const key = await this.scrypt(passwordArg, salt);
|
||||
return `${HASH_PREFIX}:${salt}:${key.toString('base64url')}`;
|
||||
}
|
||||
|
||||
public static async verifyPassword(passwordArg: string, passwordHashArg?: string): Promise<boolean> {
|
||||
if (!passwordHashArg) {
|
||||
return false;
|
||||
}
|
||||
const [prefix, version, salt, storedKey] = passwordHashArg.split(':');
|
||||
if (`${prefix}:${version}` !== HASH_PREFIX || !salt || !storedKey) {
|
||||
return false;
|
||||
}
|
||||
const candidate = await this.scrypt(passwordArg, salt);
|
||||
const stored = Buffer.from(storedKey, 'base64url');
|
||||
if (candidate.byteLength !== stored.byteLength) {
|
||||
return false;
|
||||
}
|
||||
return plugins.crypto.timingSafeEqual(candidate, stored);
|
||||
}
|
||||
|
||||
private static async scrypt(passwordArg: string, saltArg: string): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
plugins.crypto.scrypt(passwordArg, saltArg, 64, (error, derivedKey) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(derivedKey as Buffer);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './interfaces.js';
|
||||
export * from './classes.account-auth-service.js';
|
||||
export * from './classes.account-doc.js';
|
||||
export * from './classes.account-store.js';
|
||||
export * from './classes.idp-global-server-client.js';
|
||||
export * from './classes.password-hasher.js';
|
||||
@@ -0,0 +1,41 @@
|
||||
export type TIdpAccountAuthSource = 'local' | 'idp.global';
|
||||
export type TIdpAccountRole = 'admin' | 'user';
|
||||
export type TIdpAccountStatus = 'active' | 'disabled';
|
||||
|
||||
export interface IIdpSdkAccount {
|
||||
id: string;
|
||||
email: string;
|
||||
emailNormalized: string;
|
||||
name: string;
|
||||
role: TIdpAccountRole;
|
||||
status: TIdpAccountStatus;
|
||||
authSources: TIdpAccountAuthSource[];
|
||||
passwordHash?: string;
|
||||
idpSubject?: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
lastLoginAt?: number;
|
||||
}
|
||||
|
||||
export interface ICreateIdpSdkAccountOptions {
|
||||
email: string;
|
||||
name: string;
|
||||
role: TIdpAccountRole;
|
||||
status?: TIdpAccountStatus;
|
||||
authSources: TIdpAccountAuthSource[];
|
||||
password?: string;
|
||||
idpSubject?: string;
|
||||
}
|
||||
|
||||
export interface IAuthenticateAccountOptions {
|
||||
email: string;
|
||||
password: string;
|
||||
authSource?: TIdpAccountAuthSource | 'auto';
|
||||
}
|
||||
|
||||
export interface IAuthenticatedAccountResult {
|
||||
account: IIdpSdkAccount;
|
||||
authSource: TIdpAccountAuthSource;
|
||||
idpJwt?: string;
|
||||
idpRefreshToken?: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export { crypto };
|
||||
|
||||
import * as idpInterfaces from '@idp.global/interfaces';
|
||||
|
||||
export { idpInterfaces };
|
||||
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import * as typedsocket from '@api.global/typedsocket';
|
||||
|
||||
export { typedrequest, typedsocket };
|
||||
|
||||
import * as smartdata from '@push.rocks/smartdata';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
|
||||
export { smartdata, smartpromise };
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["node"],
|
||||
"strict": false
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user