Refactor code structure for improved readability and maintainability
This commit is contained in:
BIN
.playwright-mcp/stack-gallery-login.png
Normal file
BIN
.playwright-mcp/stack-gallery-login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
22
deno.json
22
deno.json
@@ -2,6 +2,7 @@
|
|||||||
"name": "@stack.gallery/registry",
|
"name": "@stack.gallery/registry",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
|
"nodeModulesDir": "auto",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"start": "deno run --allow-all mod.ts server",
|
"start": "deno run --allow-all mod.ts server",
|
||||||
"dev": "deno run --allow-all --watch mod.ts server --ephemeral",
|
"dev": "deno run --allow-all --watch mod.ts server --ephemeral",
|
||||||
@@ -10,29 +11,28 @@
|
|||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@push.rocks/smartregistry": "npm:@push.rocks/smartregistry@^2.5.0",
|
"@push.rocks/smartregistry": "npm:@push.rocks/smartregistry@^2.5.0",
|
||||||
"@push.rocks/smartdata": "npm:@push.rocks/smartdata@^5.0.0",
|
"@push.rocks/smartdata": "npm:@push.rocks/smartdata@^7.0.0",
|
||||||
"@push.rocks/smartbucket": "npm:@push.rocks/smartbucket@^4.3.0",
|
"@push.rocks/smartbucket": "npm:@push.rocks/smartbucket@^4.3.0",
|
||||||
"@push.rocks/smartlog": "npm:@push.rocks/smartlog@^3.0.0",
|
"@push.rocks/smartlog": "npm:@push.rocks/smartlog@^3.1.0",
|
||||||
"@push.rocks/smartenv": "npm:@push.rocks/smartenv@^5.0.0",
|
"@push.rocks/smartenv": "npm:@push.rocks/smartenv@^6.0.0",
|
||||||
"@push.rocks/smartpath": "npm:@push.rocks/smartpath@^5.0.0",
|
"@push.rocks/smartpath": "npm:@push.rocks/smartpath@^6.0.0",
|
||||||
"@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.0.0",
|
"@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.2.0",
|
||||||
"@push.rocks/smartstring": "npm:@push.rocks/smartstring@^4.0.0",
|
"@push.rocks/smartstring": "npm:@push.rocks/smartstring@^4.1.0",
|
||||||
"@push.rocks/smartcrypto": "npm:@push.rocks/smartcrypto@^2.0.0",
|
"@push.rocks/smartcrypto": "npm:@push.rocks/smartcrypto@^2.0.0",
|
||||||
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.0.0",
|
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.0",
|
||||||
"@push.rocks/smartunique": "npm:@push.rocks/smartunique@^3.0.0",
|
"@push.rocks/smartunique": "npm:@push.rocks/smartunique@^3.0.0",
|
||||||
"@push.rocks/smartdelay": "npm:@push.rocks/smartdelay@^3.0.0",
|
"@push.rocks/smartdelay": "npm:@push.rocks/smartdelay@^3.0.0",
|
||||||
"@push.rocks/smartrx": "npm:@push.rocks/smartrx@^3.0.0",
|
"@push.rocks/smartrx": "npm:@push.rocks/smartrx@^3.0.0",
|
||||||
"@push.rocks/smartcli": "npm:@push.rocks/smartcli@^4.0.0",
|
"@push.rocks/smartcli": "npm:@push.rocks/smartcli@^4.0.0",
|
||||||
"@tsclass/tsclass": "npm:@tsclass/tsclass@^9.0.0",
|
"@push.rocks/smartarchive": "npm:@push.rocks/smartarchive@^5.0.0",
|
||||||
|
"@tsclass/tsclass": "npm:@tsclass/tsclass@^9.3.0",
|
||||||
"@std/path": "jsr:@std/path@^1.0.0",
|
"@std/path": "jsr:@std/path@^1.0.0",
|
||||||
"@std/fs": "jsr:@std/fs@^1.0.0",
|
"@std/fs": "jsr:@std/fs@^1.0.0",
|
||||||
"@std/http": "jsr:@std/http@^1.0.0"
|
"@std/http": "jsr:@std/http@^1.0.0"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": ["deno.window", "dom"],
|
"lib": ["deno.window"]
|
||||||
"jsx": "react-jsx",
|
|
||||||
"jsxImportSource": "npm:react"
|
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"rules": {
|
"rules": {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "deno run --allow-all mod.ts server",
|
"start": "deno run --allow-all mod.ts server",
|
||||||
"dev": "deno run --allow-all --watch mod.ts server --ephemeral",
|
"dev": "deno run --allow-all --watch mod.ts server --ephemeral",
|
||||||
|
"watch": "concurrently --kill-others --names \"BACKEND,UI\" --prefix-colors \"cyan,magenta\" \"deno run --allow-all --watch mod.ts server --ephemeral\" \"cd ui && pnpm run watch\"",
|
||||||
"build": "cd ui && pnpm run build",
|
"build": "cd ui && pnpm run build",
|
||||||
"test": "deno test --allow-all"
|
"test": "deno test --allow-all"
|
||||||
},
|
},
|
||||||
@@ -22,5 +23,8 @@
|
|||||||
"composer"
|
"composer"
|
||||||
],
|
],
|
||||||
"author": "Stack.Gallery",
|
"author": "Stack.Gallery",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^9.1.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
206
pnpm-lock.yaml
generated
Normal file
206
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
devDependencies:
|
||||||
|
concurrently:
|
||||||
|
specifier: ^9.1.2
|
||||||
|
version: 9.2.1
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
ansi-regex@5.0.1:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
chalk@4.1.2:
|
||||||
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
cliui@8.0.1:
|
||||||
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
||||||
|
color-name@1.1.4:
|
||||||
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
concurrently@9.2.1:
|
||||||
|
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
emoji-regex@8.0.0:
|
||||||
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
|
escalade@3.2.0:
|
||||||
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
get-caller-file@2.0.5:
|
||||||
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
|
|
||||||
|
has-flag@4.0.0:
|
||||||
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0:
|
||||||
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
require-directory@2.1.1:
|
||||||
|
resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||||
|
|
||||||
|
shell-quote@1.8.3:
|
||||||
|
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
supports-color@8.1.1:
|
||||||
|
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
tree-kill@1.2.2:
|
||||||
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
y18n@5.0.8:
|
||||||
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
yargs-parser@21.1.1:
|
||||||
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
yargs@17.7.2:
|
||||||
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 2.0.1
|
||||||
|
|
||||||
|
chalk@4.1.2:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
cliui@8.0.1:
|
||||||
|
dependencies:
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wrap-ansi: 7.0.0
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
|
||||||
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
concurrently@9.2.1:
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
rxjs: 7.8.2
|
||||||
|
shell-quote: 1.8.3
|
||||||
|
supports-color: 8.1.1
|
||||||
|
tree-kill: 1.2.2
|
||||||
|
yargs: 17.7.2
|
||||||
|
|
||||||
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0: {}
|
||||||
|
|
||||||
|
require-directory@2.1.1: {}
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
shell-quote@1.8.3: {}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 8.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 4.0.0
|
||||||
|
|
||||||
|
supports-color@8.1.1:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 4.0.0
|
||||||
|
|
||||||
|
tree-kill@1.2.2: {}
|
||||||
|
|
||||||
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
|
yargs-parser@21.1.1: {}
|
||||||
|
|
||||||
|
yargs@17.7.2:
|
||||||
|
dependencies:
|
||||||
|
cliui: 8.0.1
|
||||||
|
escalade: 3.2.0
|
||||||
|
get-caller-file: 2.0.5
|
||||||
|
require-directory: 2.1.1
|
||||||
|
string-width: 4.2.3
|
||||||
|
y18n: 5.0.8
|
||||||
|
yargs-parser: 21.1.1
|
||||||
14
ts/cli.ts
14
ts/cli.ts
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as plugins from './plugins.ts';
|
import * as plugins from './plugins.ts';
|
||||||
import { StackGalleryRegistry, createRegistryFromEnv } from './registry.ts';
|
import { StackGalleryRegistry, createRegistryFromEnv, createRegistryFromEnvFile } from './registry.ts';
|
||||||
import { initDb } from './models/db.ts';
|
import { initDb } from './models/db.ts';
|
||||||
import { User, Organization, OrganizationMember, Repository } from './models/index.ts';
|
import { User, Organization, OrganizationMember, Repository } from './models/index.ts';
|
||||||
import { AuthService } from './services/auth.service.ts';
|
import { AuthService } from './services/auth.service.ts';
|
||||||
@@ -13,9 +13,17 @@ export async function runCli(): Promise<void> {
|
|||||||
|
|
||||||
// Server command
|
// Server command
|
||||||
smartcliInstance.addCommand('server').subscribe(async (argsParsed) => {
|
smartcliInstance.addCommand('server').subscribe(async (argsParsed) => {
|
||||||
console.log('Starting Stack.Gallery Registry...');
|
const isEphemeral = argsParsed.ephemeral || argsParsed.e;
|
||||||
|
|
||||||
const registry = createRegistryFromEnv();
|
console.log('Starting Stack.Gallery Registry...');
|
||||||
|
if (isEphemeral) {
|
||||||
|
console.log('[CLI] Ephemeral mode enabled - loading config from .nogit/env.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use env file in ephemeral/dev mode, otherwise use environment variables
|
||||||
|
const registry = isEphemeral
|
||||||
|
? await createRegistryFromEnvFile()
|
||||||
|
: createRegistryFromEnv();
|
||||||
await registry.start();
|
await registry.start();
|
||||||
|
|
||||||
// Handle shutdown gracefully
|
// Handle shutdown gracefully
|
||||||
|
|||||||
@@ -4,13 +4,10 @@
|
|||||||
|
|
||||||
import * as plugins from '../plugins.ts';
|
import * as plugins from '../plugins.ts';
|
||||||
import type { IApiToken, ITokenScope, TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
|
import type { IApiToken, ITokenScope, TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
|
||||||
import { getDb } from './db.ts';
|
import { db } from './db.ts';
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => db)
|
||||||
export class ApiToken
|
export class ApiToken extends plugins.smartdata.SmartDataDbDoc<ApiToken, ApiToken> implements IApiToken {
|
||||||
extends plugins.smartdata.SmartDataDbDoc<ApiToken, ApiToken>
|
|
||||||
implements IApiToken
|
|
||||||
{
|
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string = '';
|
public id: string = '';
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,10 @@
|
|||||||
|
|
||||||
import * as plugins from '../plugins.ts';
|
import * as plugins from '../plugins.ts';
|
||||||
import type { IAuditLog, TAuditAction, TAuditResourceType } from '../interfaces/audit.interfaces.ts';
|
import type { IAuditLog, TAuditAction, TAuditResourceType } from '../interfaces/audit.interfaces.ts';
|
||||||
import { getDb } from './db.ts';
|
import { db } from './db.ts';
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => db)
|
||||||
export class AuditLog
|
export class AuditLog extends plugins.smartdata.SmartDataDbDoc<AuditLog, AuditLog> implements IAuditLog {
|
||||||
extends plugins.smartdata.SmartDataDbDoc<AuditLog, AuditLog>
|
|
||||||
implements IAuditLog
|
|
||||||
{
|
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string = '';
|
public id: string = '';
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,65 @@
|
|||||||
/**
|
/**
|
||||||
* Database connection singleton
|
* Database connection singleton
|
||||||
|
*
|
||||||
|
* SmartData models need a db reference at class definition time via lazy getter.
|
||||||
|
* The actual .init() is called later when the server starts.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as plugins from '../plugins.ts';
|
import * as plugins from '../plugins.ts';
|
||||||
|
import { User } from './user.ts';
|
||||||
|
|
||||||
let dbInstance: plugins.smartdata.SmartdataDb | null = null;
|
// Database instance - created lazily in initDb()
|
||||||
|
// The @Collection(() => db) decorator uses a lazy getter, so db can be undefined
|
||||||
|
// until initDb() is called. Default admin is seeded after db.init() completes.
|
||||||
|
export let db: plugins.smartdata.SmartdataDb;
|
||||||
|
|
||||||
|
let isInitialized = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize database connection
|
* Initialize database connection
|
||||||
*/
|
*/
|
||||||
export async function initDb(config: {
|
export async function initDb(
|
||||||
mongoDbUrl: string;
|
mongoDbUrl: string,
|
||||||
mongoDbName?: string;
|
mongoDbName?: string
|
||||||
}): Promise<plugins.smartdata.SmartdataDb> {
|
): Promise<plugins.smartdata.SmartdataDb> {
|
||||||
if (dbInstance) {
|
if (isInitialized && db) {
|
||||||
return dbInstance;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
dbInstance = new plugins.smartdata.SmartdataDb({
|
// Create the database instance with actual configuration
|
||||||
mongoDbUrl: config.mongoDbUrl,
|
db = new plugins.smartdata.SmartdataDb({
|
||||||
mongoDbName: config.mongoDbName || 'stackregistry',
|
mongoDbUrl: mongoDbUrl,
|
||||||
|
mongoDbName: mongoDbName || 'stackregistry',
|
||||||
});
|
});
|
||||||
|
|
||||||
await dbInstance.init();
|
await db.init();
|
||||||
|
isInitialized = true;
|
||||||
console.log('Database connected successfully');
|
console.log('Database connected successfully');
|
||||||
|
|
||||||
return dbInstance;
|
// Seed default admin user if none exists
|
||||||
|
try {
|
||||||
|
await User.seedDefaultAdmin();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[Database] Failed to seed default admin:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get database instance (must call initDb first)
|
* Get database instance (for backward compatibility)
|
||||||
*/
|
*/
|
||||||
export function getDb(): plugins.smartdata.SmartdataDb {
|
export function getDb(): plugins.smartdata.SmartdataDb {
|
||||||
if (!dbInstance) {
|
return db;
|
||||||
throw new Error('Database not initialized. Call initDb() first.');
|
|
||||||
}
|
|
||||||
return dbInstance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close database connection
|
* Close database connection
|
||||||
*/
|
*/
|
||||||
export async function closeDb(): Promise<void> {
|
export async function closeDb(): Promise<void> {
|
||||||
if (dbInstance) {
|
if (db && isInitialized) {
|
||||||
await dbInstance.close();
|
await db.close();
|
||||||
dbInstance = null;
|
isInitialized = false;
|
||||||
console.log('Database connection closed');
|
console.log('Database connection closed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,5 +68,5 @@ export async function closeDb(): Promise<void> {
|
|||||||
* Check if database is connected
|
* Check if database is connected
|
||||||
*/
|
*/
|
||||||
export function isDbConnected(): boolean {
|
export function isDbConnected(): boolean {
|
||||||
return dbInstance !== null;
|
return isInitialized;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,10 @@
|
|||||||
|
|
||||||
import * as plugins from '../plugins.ts';
|
import * as plugins from '../plugins.ts';
|
||||||
import type { IOrganizationMember, TOrganizationRole } from '../interfaces/auth.interfaces.ts';
|
import type { IOrganizationMember, TOrganizationRole } from '../interfaces/auth.interfaces.ts';
|
||||||
import { getDb } from './db.ts';
|
import { db } from './db.ts';
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => db)
|
||||||
export class OrganizationMember
|
export class OrganizationMember extends plugins.smartdata.SmartDataDbDoc<OrganizationMember, OrganizationMember> implements IOrganizationMember {
|
||||||
extends plugins.smartdata.SmartDataDbDoc<OrganizationMember, OrganizationMember>
|
|
||||||
implements IOrganizationMember
|
|
||||||
{
|
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string = '';
|
public id: string = '';
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
IOrganizationSettings,
|
IOrganizationSettings,
|
||||||
TOrganizationPlan,
|
TOrganizationPlan,
|
||||||
} from '../interfaces/auth.interfaces.ts';
|
} from '../interfaces/auth.interfaces.ts';
|
||||||
import { getDb } from './db.ts';
|
import { db } from './db.ts';
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: IOrganizationSettings = {
|
const DEFAULT_SETTINGS: IOrganizationSettings = {
|
||||||
requireMfa: false,
|
requireMfa: false,
|
||||||
@@ -17,11 +17,8 @@ const DEFAULT_SETTINGS: IOrganizationSettings = {
|
|||||||
allowedProtocols: ['oci', 'npm', 'maven', 'cargo', 'composer', 'pypi', 'rubygems'],
|
allowedProtocols: ['oci', 'npm', 'maven', 'cargo', 'composer', 'pypi', 'rubygems'],
|
||||||
};
|
};
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => db)
|
||||||
export class Organization
|
export class Organization extends plugins.smartdata.SmartDataDbDoc<Organization, Organization> implements IOrganization {
|
||||||
extends plugins.smartdata.SmartDataDbDoc<Organization, Organization>
|
|
||||||
implements IOrganization
|
|
||||||
{
|
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string = '';
|
public id: string = '';
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import type {
|
|||||||
IProtocolMetadata,
|
IProtocolMetadata,
|
||||||
} from '../interfaces/package.interfaces.ts';
|
} from '../interfaces/package.interfaces.ts';
|
||||||
import type { TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
|
import type { TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
|
||||||
import { getDb } from './db.ts';
|
import { db } from './db.ts';
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => db)
|
||||||
export class Package extends plugins.smartdata.SmartDataDbDoc<Package, Package> implements IPackage {
|
export class Package extends plugins.smartdata.SmartDataDbDoc<Package, Package> implements IPackage {
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string = ''; // {protocol}:{org}:{name}
|
public id: string = ''; // {protocol}:{org}:{name}
|
||||||
|
|||||||
@@ -4,13 +4,10 @@
|
|||||||
|
|
||||||
import * as plugins from '../plugins.ts';
|
import * as plugins from '../plugins.ts';
|
||||||
import type { IRepositoryPermission, TRepositoryRole } from '../interfaces/auth.interfaces.ts';
|
import type { IRepositoryPermission, TRepositoryRole } from '../interfaces/auth.interfaces.ts';
|
||||||
import { getDb } from './db.ts';
|
import { db } from './db.ts';
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => db)
|
||||||
export class RepositoryPermission
|
export class RepositoryPermission extends plugins.smartdata.SmartDataDbDoc<RepositoryPermission, RepositoryPermission> implements IRepositoryPermission {
|
||||||
extends plugins.smartdata.SmartDataDbDoc<RepositoryPermission, RepositoryPermission>
|
|
||||||
implements IRepositoryPermission
|
|
||||||
{
|
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string = '';
|
public id: string = '';
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,10 @@
|
|||||||
|
|
||||||
import * as plugins from '../plugins.ts';
|
import * as plugins from '../plugins.ts';
|
||||||
import type { IRepository, TRepositoryVisibility, TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
|
import type { IRepository, TRepositoryVisibility, TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
|
||||||
import { getDb } from './db.ts';
|
import { db } from './db.ts';
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => db)
|
||||||
export class Repository
|
export class Repository extends plugins.smartdata.SmartDataDbDoc<Repository, Repository> implements IRepository {
|
||||||
extends plugins.smartdata.SmartDataDbDoc<Repository, Repository>
|
|
||||||
implements IRepository
|
|
||||||
{
|
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string = '';
|
public id: string = '';
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,10 @@
|
|||||||
|
|
||||||
import * as plugins from '../plugins.ts';
|
import * as plugins from '../plugins.ts';
|
||||||
import type { ISession } from '../interfaces/auth.interfaces.ts';
|
import type { ISession } from '../interfaces/auth.interfaces.ts';
|
||||||
import { getDb } from './db.ts';
|
import { db } from './db.ts';
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => db)
|
||||||
export class Session
|
export class Session extends plugins.smartdata.SmartDataDbDoc<Session, Session> implements ISession {
|
||||||
extends plugins.smartdata.SmartDataDbDoc<Session, Session>
|
|
||||||
implements ISession
|
|
||||||
{
|
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string = '';
|
public id: string = '';
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,10 @@
|
|||||||
|
|
||||||
import * as plugins from '../plugins.ts';
|
import * as plugins from '../plugins.ts';
|
||||||
import type { ITeamMember, TTeamRole } from '../interfaces/auth.interfaces.ts';
|
import type { ITeamMember, TTeamRole } from '../interfaces/auth.interfaces.ts';
|
||||||
import { getDb } from './db.ts';
|
import { db } from './db.ts';
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => db)
|
||||||
export class TeamMember
|
export class TeamMember extends plugins.smartdata.SmartDataDbDoc<TeamMember, TeamMember> implements ITeamMember {
|
||||||
extends plugins.smartdata.SmartDataDbDoc<TeamMember, TeamMember>
|
|
||||||
implements ITeamMember
|
|
||||||
{
|
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string = '';
|
public id: string = '';
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
import * as plugins from '../plugins.ts';
|
import * as plugins from '../plugins.ts';
|
||||||
import type { ITeam } from '../interfaces/auth.interfaces.ts';
|
import type { ITeam } from '../interfaces/auth.interfaces.ts';
|
||||||
import { getDb } from './db.ts';
|
import { db } from './db.ts';
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => db)
|
||||||
export class Team extends plugins.smartdata.SmartDataDbDoc<Team, Team> implements ITeam {
|
export class Team extends plugins.smartdata.SmartDataDbDoc<Team, Team> implements ITeam {
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string = '';
|
public id: string = '';
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
import * as plugins from '../plugins.ts';
|
import * as plugins from '../plugins.ts';
|
||||||
import type { IUser, TUserStatus } from '../interfaces/auth.interfaces.ts';
|
import type { IUser, TUserStatus } from '../interfaces/auth.interfaces.ts';
|
||||||
import { getDb } from './db.ts';
|
import { db } from './db.ts';
|
||||||
|
|
||||||
@plugins.smartdata.Collection(() => getDb())
|
@plugins.smartdata.Collection(() => db)
|
||||||
export class User extends plugins.smartdata.SmartDataDbDoc<User, User> implements IUser {
|
export class User extends plugins.smartdata.SmartDataDbDoc<User, User> implements IUser {
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string = '';
|
public id: string = '';
|
||||||
@@ -112,4 +112,119 @@ export class User extends plugins.smartdata.SmartDataDbDoc<User, User> implement
|
|||||||
this.id = await User.getNewId();
|
this.id = await User.getNewId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is active (status === 'active')
|
||||||
|
*/
|
||||||
|
public get isActive(): boolean {
|
||||||
|
return this.status === 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for isPlatformAdmin for backward compatibility
|
||||||
|
*/
|
||||||
|
public get isSystemAdmin(): boolean {
|
||||||
|
return this.isPlatformAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find user by ID
|
||||||
|
*/
|
||||||
|
public static async findById(id: string): Promise<User | null> {
|
||||||
|
return await User.getInstance({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify password against stored hash
|
||||||
|
*/
|
||||||
|
public async verifyPassword(password: string): Promise<boolean> {
|
||||||
|
if (!this.passwordHash) return false;
|
||||||
|
|
||||||
|
const [saltHex, expectedHash] = this.passwordHash.split(':');
|
||||||
|
if (!saltHex || !expectedHash) return false;
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(saltHex + password);
|
||||||
|
|
||||||
|
let hash = data;
|
||||||
|
for (let i = 0; i < 10000; i++) {
|
||||||
|
hash = new Uint8Array(await crypto.subtle.digest('SHA-256', hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashHex = Array.from(hash)
|
||||||
|
.map((b) => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return hashHex === expectedHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a password for storage
|
||||||
|
*/
|
||||||
|
public static async hashPassword(password: string): Promise<string> {
|
||||||
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
const saltHex = Array.from(salt)
|
||||||
|
.map((b) => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(saltHex + password);
|
||||||
|
|
||||||
|
let hash = data;
|
||||||
|
for (let i = 0; i < 10000; i++) {
|
||||||
|
hash = new Uint8Array(await crypto.subtle.digest('SHA-256', hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashHex = Array.from(hash)
|
||||||
|
.map((b) => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return `${saltHex}:${hashHex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the default admin user if no admin exists
|
||||||
|
*/
|
||||||
|
public static async seedDefaultAdmin(): Promise<User | null> {
|
||||||
|
const adminEmail = Deno.env.get('ADMIN_EMAIL') || 'admin@stack.gallery';
|
||||||
|
const adminPassword = Deno.env.get('ADMIN_PASSWORD') || 'admin';
|
||||||
|
|
||||||
|
// Check if any platform admin exists
|
||||||
|
const existingAdmin = await User.getInstance({ isPlatformAdmin: true });
|
||||||
|
if (existingAdmin) {
|
||||||
|
console.log('[User] Platform admin already exists, skipping seed');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if admin email already exists
|
||||||
|
const existingUser = await User.findByEmail(adminEmail);
|
||||||
|
if (existingUser) {
|
||||||
|
console.log('[User] User with admin email already exists, promoting to admin');
|
||||||
|
existingUser.isPlatformAdmin = true;
|
||||||
|
existingUser.status = 'active';
|
||||||
|
await existingUser.save();
|
||||||
|
return existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new admin user
|
||||||
|
console.log('[User] Creating default admin user:', adminEmail);
|
||||||
|
const passwordHash = await User.hashPassword(adminPassword);
|
||||||
|
|
||||||
|
const admin = new User();
|
||||||
|
admin.id = await User.getNewId();
|
||||||
|
admin.email = adminEmail.toLowerCase();
|
||||||
|
admin.username = 'admin';
|
||||||
|
admin.passwordHash = passwordHash;
|
||||||
|
admin.displayName = 'System Administrator';
|
||||||
|
admin.status = 'active';
|
||||||
|
admin.emailVerified = true;
|
||||||
|
admin.isPlatformAdmin = true;
|
||||||
|
admin.createdAt = new Date();
|
||||||
|
admin.updatedAt = new Date();
|
||||||
|
|
||||||
|
await admin.save();
|
||||||
|
console.log('[User] Default admin user created successfully');
|
||||||
|
|
||||||
|
return admin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
107
ts/registry.ts
107
ts/registry.ts
@@ -154,18 +154,22 @@ export class StackGalleryRegistry {
|
|||||||
return await this.handleApiRequest(request);
|
return await this.handleApiRequest(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry protocol endpoints
|
// Registry protocol endpoints (handled by smartregistry)
|
||||||
// NPM: /-/..., /@scope/package, /package
|
// NPM: /-/..., /@scope/package (but not /packages which is UI route)
|
||||||
// OCI: /v2/...
|
// OCI: /v2/...
|
||||||
// Maven: /maven2/...
|
// Maven: /maven2/...
|
||||||
// PyPI: /simple/..., /pypi/...
|
// PyPI: /simple/..., /pypi/...
|
||||||
// Cargo: /api/v1/crates/...
|
// Cargo: /api/v1/crates/...
|
||||||
// Composer: /packages.json, /p/...
|
// Composer: /packages.json, /p/...
|
||||||
// RubyGems: /api/v1/gems/..., /gems/...
|
// RubyGems: /api/v1/gems/..., /gems/...
|
||||||
|
const registryPaths = ['/-/', '/v2/', '/maven2/', '/simple/', '/pypi/', '/api/v1/crates/', '/packages.json', '/p/', '/api/v1/gems/', '/gems/'];
|
||||||
|
const isRegistryPath = registryPaths.some(p => path.startsWith(p)) ||
|
||||||
|
(path.startsWith('/@') && !path.startsWith('/@stack'));
|
||||||
|
|
||||||
if (this.smartRegistry) {
|
if (this.smartRegistry && isRegistryPath) {
|
||||||
try {
|
try {
|
||||||
return await this.smartRegistry.handleRequest(request);
|
const response = await this.smartRegistry.handleRequest(request);
|
||||||
|
if (response) return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[StackGalleryRegistry] Request error:', error);
|
console.error('[StackGalleryRegistry] Request error:', error);
|
||||||
return new Response(
|
return new Response(
|
||||||
@@ -178,7 +182,56 @@ export class StackGalleryRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response('Not Found', { status: 404 });
|
// Serve static UI files
|
||||||
|
return await this.serveStaticFile(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve static files from UI dist
|
||||||
|
*/
|
||||||
|
private async serveStaticFile(path: string): Promise<Response> {
|
||||||
|
const uiDistPath = './ui/dist/registry-ui/browser';
|
||||||
|
|
||||||
|
// Map path to file
|
||||||
|
let filePath = path === '/' ? '/index.html' : path;
|
||||||
|
|
||||||
|
// Content type mapping
|
||||||
|
const contentTypes: Record<string, string> = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.js': 'application/javascript',
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.woff': 'font/woff',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
'.ttf': 'font/ttf',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fullPath = `${uiDistPath}${filePath}`;
|
||||||
|
const file = await Deno.readFile(fullPath);
|
||||||
|
const ext = filePath.substring(filePath.lastIndexOf('.'));
|
||||||
|
const contentType = contentTypes[ext] || 'application/octet-stream';
|
||||||
|
|
||||||
|
return new Response(file, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': contentType },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// For SPA routing, serve index.html for unknown paths
|
||||||
|
try {
|
||||||
|
const indexFile = await Deno.readFile(`${uiDistPath}/index.html`);
|
||||||
|
return new Response(indexFile, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'text/html' },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -274,3 +327,47 @@ export function createRegistryFromEnv(): StackGalleryRegistry {
|
|||||||
|
|
||||||
return new StackGalleryRegistry(config);
|
return new StackGalleryRegistry(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create registry from .nogit/env.json file (for local development)
|
||||||
|
* Falls back to environment variables if file doesn't exist
|
||||||
|
*/
|
||||||
|
export async function createRegistryFromEnvFile(): Promise<StackGalleryRegistry> {
|
||||||
|
const envPath = '.nogit/env.json';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const envText = await Deno.readTextFile(envPath);
|
||||||
|
const env = JSON.parse(envText);
|
||||||
|
|
||||||
|
console.log('[StackGalleryRegistry] Loading config from .nogit/env.json');
|
||||||
|
|
||||||
|
// Build S3 endpoint from host/port/ssl settings
|
||||||
|
const s3Protocol = env.S3_USESSL ? 'https' : 'http';
|
||||||
|
const s3Endpoint = `${s3Protocol}://${env.S3_HOST || 'localhost'}:${env.S3_PORT || '9000'}`;
|
||||||
|
|
||||||
|
const config: IRegistryConfig = {
|
||||||
|
mongoUrl: env.MONGODB_URL || `mongodb://${env.MONGODB_USER}:${env.MONGODB_PASS}@${env.MONGODB_HOST || 'localhost'}:${env.MONGODB_PORT || '27017'}/${env.MONGODB_NAME}?authSource=admin`,
|
||||||
|
mongoDb: env.MONGODB_NAME || 'stackgallery',
|
||||||
|
s3Endpoint: s3Endpoint,
|
||||||
|
s3AccessKey: env.S3_ACCESSKEY || env.S3_ACCESS_KEY || 'minioadmin',
|
||||||
|
s3SecretKey: env.S3_SECRETKEY || env.S3_SECRET_KEY || 'minioadmin',
|
||||||
|
s3Bucket: env.S3_BUCKET || 'registry',
|
||||||
|
s3Region: env.S3_REGION,
|
||||||
|
host: env.HOST || '0.0.0.0',
|
||||||
|
port: parseInt(env.PORT || '3000', 10),
|
||||||
|
storagePath: env.STORAGE_PATH || 'packages',
|
||||||
|
enableUpstreamCache: env.ENABLE_UPSTREAM_CACHE !== false,
|
||||||
|
upstreamCacheExpiry: parseInt(env.UPSTREAM_CACHE_EXPIRY || '24', 10),
|
||||||
|
jwtSecret: env.JWT_SECRET,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new StackGalleryRegistry(config);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Deno.errors.NotFound) {
|
||||||
|
console.log('[StackGalleryRegistry] No .nogit/env.json found, using environment variables');
|
||||||
|
} else {
|
||||||
|
console.warn('[StackGalleryRegistry] Error reading .nogit/env.json, falling back to env vars:', error);
|
||||||
|
}
|
||||||
|
return createRegistryFromEnv();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,7 +61,17 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Find user by email
|
// Find user by email
|
||||||
const user = await User.findByEmail(email);
|
let user: User | null = null;
|
||||||
|
try {
|
||||||
|
user = await User.findByEmail(email);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[AuthService] Database error finding user:', err);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errorCode: 'DATABASE_ERROR',
|
||||||
|
errorMessage: 'Unable to verify credentials. Please try again.',
|
||||||
|
};
|
||||||
|
}
|
||||||
if (!user) {
|
if (!user) {
|
||||||
await auditContext.logUserLogin('', false, 'User not found');
|
await auditContext.logUserLogin('', false, 'User not found');
|
||||||
return {
|
return {
|
||||||
|
|||||||
8529
ui/pnpm-lock.yaml
generated
Normal file
8529
ui/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ import {
|
|||||||
HttpHandlerFn,
|
HttpHandlerFn,
|
||||||
HttpErrorResponse,
|
HttpErrorResponse,
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import { catchError, switchMap, throwError } from 'rxjs';
|
import { catchError, switchMap, throwError, from, Observable } from 'rxjs';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@@ -33,9 +33,9 @@ export const authInterceptor: HttpInterceptorFn = (
|
|||||||
return next(req).pipe(
|
return next(req).pipe(
|
||||||
catchError((error: HttpErrorResponse) => {
|
catchError((error: HttpErrorResponse) => {
|
||||||
if (error.status === 401) {
|
if (error.status === 401) {
|
||||||
// Try to refresh the token
|
// Try to refresh the token using from() to convert Promise to Observable
|
||||||
return new Promise((resolve) => {
|
return from(authService.refreshAccessToken()).pipe(
|
||||||
authService.refreshAccessToken().then((success) => {
|
switchMap((success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
// Retry the request with new token
|
// Retry the request with new token
|
||||||
const newToken = authService.accessToken;
|
const newToken = authService.accessToken;
|
||||||
@@ -44,14 +44,14 @@ export const authInterceptor: HttpInterceptorFn = (
|
|||||||
Authorization: `Bearer ${newToken}`,
|
Authorization: `Bearer ${newToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
resolve(next(retryReq));
|
return next(retryReq);
|
||||||
} else {
|
} else {
|
||||||
// Redirect to login
|
// Redirect to login
|
||||||
router.navigate(['/login']);
|
router.navigate(['/login']);
|
||||||
resolve(throwError(() => error));
|
return throwError(() => error);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}).then((result) => result as ReturnType<HttpHandlerFn>);
|
);
|
||||||
}
|
}
|
||||||
return throwError(() => error);
|
return throwError(() => error);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component, inject, signal, OnInit } from '@angular/core';
|
import { Component, inject, signal, OnInit } from '@angular/core';
|
||||||
|
import { NgClass } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ApiService, type IToken } from '../../core/services/api.service';
|
import { ApiService, type IToken } from '../../core/services/api.service';
|
||||||
import { ToastService } from '../../core/services/toast.service';
|
import { ToastService } from '../../core/services/toast.service';
|
||||||
@@ -6,7 +7,7 @@ import { ToastService } from '../../core/services/toast.service';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tokens',
|
selector: 'app-tokens',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [FormsModule],
|
imports: [NgClass, FormsModule],
|
||||||
template: `
|
template: `
|
||||||
<div class="p-6 max-w-4xl mx-auto">
|
<div class="p-6 max-w-4xl mx-auto">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
@@ -105,10 +106,12 @@ import { ToastService } from '../../core/services/toast.service';
|
|||||||
<label class="label block mb-1.5">Protocols</label>
|
<label class="label block mb-1.5">Protocols</label>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
@for (protocol of availableProtocols; track protocol) {
|
@for (protocol of availableProtocols; track protocol) {
|
||||||
<label class="flex items-center gap-2 px-3 py-1.5 rounded-md border border-gray-300 dark:border-gray-600 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
<label
|
||||||
[class.bg-primary-50]="newToken.protocols.includes(protocol)"
|
class="flex items-center gap-2 px-3 py-1.5 rounded-md border cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
[class.border-primary-300]="newToken.protocols.includes(protocol)"
|
[ngClass]="{
|
||||||
[class.dark:bg-primary-900/20]="newToken.protocols.includes(protocol)">
|
'bg-primary-50 border-primary-300 dark:bg-primary-900/20': newToken.protocols.includes(protocol),
|
||||||
|
'border-gray-300 dark:border-gray-600': !newToken.protocols.includes(protocol)
|
||||||
|
}">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
[checked]="newToken.protocols.includes(protocol)"
|
[checked]="newToken.protocols.includes(protocol)"
|
||||||
|
|||||||
Reference in New Issue
Block a user