mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 17:41:09 +02:00
✨ feat(auth): add organization management endpoints
Add missing organization features for Teams functionality: - PUT /auth/organizations/:id - update organization - DELETE /auth/organizations/:id - delete organization - PATCH /auth/organizations/:orgId/members/:memberId/role - update member role - GET /auth/organizations/:id/invitations - list org invitations - GET /auth/invitations - list user invitations - DELETE /auth/invitations/:id - cancel or reject invitation
This commit is contained in:
parent
9d618b107c
commit
5fe16b5eec
13 changed files with 1163 additions and 0 deletions
27
packages/shared-pwa/package.json
Normal file
27
packages/shared-pwa/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "@manacore/shared-pwa",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vite-pwa/sveltekit": ">=0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vite-pwa/sveltekit": "^0.6.5",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^6.0.0",
|
||||
"workbox-build": "^7.0.0"
|
||||
}
|
||||
}
|
||||
93
packages/shared-pwa/scripts/generate-icons.mjs
Normal file
93
packages/shared-pwa/scripts/generate-icons.mjs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* PWA Icon Generator Script
|
||||
*
|
||||
* Generates PWA icons from a source SVG or PNG file.
|
||||
* Creates: pwa-192x192.png, pwa-512x512.png, apple-touch-icon.png
|
||||
*
|
||||
* Usage:
|
||||
* node generate-icons.mjs <source-image> [output-dir]
|
||||
*
|
||||
* Requirements:
|
||||
* - sharp package (installed as devDependency)
|
||||
*
|
||||
* Example:
|
||||
* node generate-icons.mjs favicon.svg static/
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const ICON_SIZES = [
|
||||
{ name: 'pwa-192x192.png', size: 192 },
|
||||
{ name: 'pwa-512x512.png', size: 512 },
|
||||
{ name: 'apple-touch-icon.png', size: 180 },
|
||||
];
|
||||
|
||||
async function generateIcons(sourcePath, outputDir) {
|
||||
// Dynamic import of sharp (may not be installed in all contexts)
|
||||
let sharp;
|
||||
try {
|
||||
sharp = (await import('sharp')).default;
|
||||
} catch {
|
||||
console.error('Error: sharp package not installed.');
|
||||
console.error('Install it with: pnpm add -D sharp');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(sourcePath)) {
|
||||
console.error(`Error: Source file not found: ${sourcePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`Generating PWA icons from: ${sourcePath}`);
|
||||
console.log(`Output directory: ${outputDir}`);
|
||||
console.log('');
|
||||
|
||||
for (const icon of ICON_SIZES) {
|
||||
const outputPath = join(outputDir, icon.name);
|
||||
|
||||
try {
|
||||
await sharp(sourcePath).resize(icon.size, icon.size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(outputPath);
|
||||
|
||||
console.log(` ✓ ${icon.name} (${icon.size}x${icon.size})`);
|
||||
} catch (error) {
|
||||
console.error(` ✗ ${icon.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('Done! Icons generated successfully.');
|
||||
console.log('');
|
||||
console.log('Make sure these files are in your static/ directory.');
|
||||
}
|
||||
|
||||
// CLI execution
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 1) {
|
||||
console.log('PWA Icon Generator');
|
||||
console.log('');
|
||||
console.log('Usage: node generate-icons.mjs <source-image> [output-dir]');
|
||||
console.log('');
|
||||
console.log('Arguments:');
|
||||
console.log(' source-image Path to source SVG or PNG file');
|
||||
console.log(' output-dir Output directory (default: current directory)');
|
||||
console.log('');
|
||||
console.log('Example:');
|
||||
console.log(' node generate-icons.mjs favicon.svg static/');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const sourcePath = args[0];
|
||||
const outputDir = args[1] || '.';
|
||||
|
||||
generateIcons(sourcePath, outputDir);
|
||||
135
packages/shared-pwa/src/config.ts
Normal file
135
packages/shared-pwa/src/config.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* PWA Configuration Factory
|
||||
*
|
||||
* Creates a complete @vite-pwa/sveltekit configuration with sensible defaults
|
||||
* and preset-based caching strategies.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createPWAConfig } from '@manacore/shared-pwa';
|
||||
* import { SvelteKitPWA } from '@vite-pwa/sveltekit';
|
||||
*
|
||||
* export default defineConfig({
|
||||
* plugins: [
|
||||
* sveltekit(),
|
||||
* SvelteKitPWA(createPWAConfig({
|
||||
* name: 'Calendar - Kalender',
|
||||
* shortName: 'Calendar',
|
||||
* description: 'Kalender mit Offline-Unterstützung',
|
||||
* themeColor: '#3b82f6',
|
||||
* preset: 'standard',
|
||||
* })),
|
||||
* ],
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type { PWAConfigOptions, PWAConfig, ManifestConfig, WorkboxConfig } from './types.js';
|
||||
import {
|
||||
DEFAULT_BACKGROUND_COLOR,
|
||||
DEFAULT_CATEGORIES,
|
||||
DEFAULT_INCLUDE_ASSETS,
|
||||
DEFAULT_GLOB_PATTERNS,
|
||||
DEFAULT_GLOB_IGNORES,
|
||||
DEFAULT_NAVIGATE_FALLBACK_DENYLIST,
|
||||
DEFAULT_ICONS,
|
||||
} from './defaults.js';
|
||||
import { getPresetRuntimeCaching } from './presets.js';
|
||||
|
||||
/**
|
||||
* Create a complete PWA configuration for SvelteKit apps
|
||||
*/
|
||||
export function createPWAConfig(options: PWAConfigOptions): PWAConfig {
|
||||
const {
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
themeColor,
|
||||
backgroundColor = DEFAULT_BACKGROUND_COLOR,
|
||||
preset = 'standard',
|
||||
shortcuts = [],
|
||||
categories = DEFAULT_CATEGORIES,
|
||||
includeAssets = [],
|
||||
globIgnores = [],
|
||||
additionalRuntimeCaching = [],
|
||||
navigateFallback = '/offline',
|
||||
navigateFallbackDenylist = DEFAULT_NAVIGATE_FALLBACK_DENYLIST,
|
||||
devEnabled = true,
|
||||
registerType = 'autoUpdate',
|
||||
lang = 'de',
|
||||
startUrl = '/',
|
||||
} = options;
|
||||
|
||||
// Build manifest
|
||||
const manifest: ManifestConfig = {
|
||||
name,
|
||||
short_name: shortName,
|
||||
description,
|
||||
theme_color: themeColor,
|
||||
background_color: backgroundColor,
|
||||
display: 'standalone',
|
||||
orientation: 'any',
|
||||
scope: '/',
|
||||
start_url: startUrl,
|
||||
lang,
|
||||
categories,
|
||||
icons: DEFAULT_ICONS,
|
||||
};
|
||||
|
||||
// Add shortcuts if provided
|
||||
if (shortcuts.length > 0) {
|
||||
manifest.shortcuts = shortcuts.map((shortcut) => ({
|
||||
name: shortcut.name,
|
||||
short_name: shortcut.short_name,
|
||||
description: shortcut.description,
|
||||
url: shortcut.url,
|
||||
icons: [{ src: 'pwa-192x192.png', sizes: '192x192' }],
|
||||
}));
|
||||
}
|
||||
|
||||
// Build workbox config
|
||||
const workbox: WorkboxConfig = {
|
||||
globPatterns: DEFAULT_GLOB_PATTERNS,
|
||||
globIgnores: [...DEFAULT_GLOB_IGNORES, ...globIgnores],
|
||||
cleanupOutdatedCaches: true,
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
navigateFallback,
|
||||
navigateFallbackDenylist,
|
||||
runtimeCaching: [...getPresetRuntimeCaching(preset), ...additionalRuntimeCaching],
|
||||
};
|
||||
|
||||
// Return complete config
|
||||
return {
|
||||
registerType,
|
||||
devOptions: {
|
||||
enabled: devEnabled,
|
||||
},
|
||||
includeAssets: [...DEFAULT_INCLUDE_ASSETS, ...includeAssets],
|
||||
manifest,
|
||||
workbox,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create PWA config with SQLite WASM support (for offline-first apps)
|
||||
* Adds proper glob ignores and OPFS configuration
|
||||
*/
|
||||
export function createOfflineFirstPWAConfig(
|
||||
options: PWAConfigOptions & {
|
||||
/**
|
||||
* Additional packages to exclude from precaching
|
||||
*/
|
||||
excludePackages?: string[];
|
||||
}
|
||||
): PWAConfig {
|
||||
const { excludePackages = [], globIgnores = [], ...rest } = options;
|
||||
|
||||
// Add SQLite-specific ignores
|
||||
const allGlobIgnores = ['**/*sqlite*', '**/*wasm*', ...excludePackages.map((pkg) => `**/${pkg}/**`), ...globIgnores];
|
||||
|
||||
return createPWAConfig({
|
||||
...rest,
|
||||
globIgnores: allGlobIgnores,
|
||||
});
|
||||
}
|
||||
66
packages/shared-pwa/src/defaults.ts
Normal file
66
packages/shared-pwa/src/defaults.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Default PWA Configuration Values
|
||||
*/
|
||||
|
||||
import type { ManifestIcon } from './types.js';
|
||||
|
||||
/**
|
||||
* Default dark background color for ManaCore apps
|
||||
*/
|
||||
export const DEFAULT_BACKGROUND_COLOR = '#09090b';
|
||||
|
||||
/**
|
||||
* Default app categories
|
||||
*/
|
||||
export const DEFAULT_CATEGORIES = ['productivity', 'utilities'];
|
||||
|
||||
/**
|
||||
* Default assets to include in PWA
|
||||
*/
|
||||
export const DEFAULT_INCLUDE_ASSETS = ['favicon.png', 'favicon.svg'];
|
||||
|
||||
/**
|
||||
* Default glob patterns for precaching
|
||||
*/
|
||||
export const DEFAULT_GLOB_PATTERNS = ['**/*.{js,css,html,ico,png,svg,woff,woff2}'];
|
||||
|
||||
/**
|
||||
* Default URL patterns to exclude from navigate fallback
|
||||
*/
|
||||
export const DEFAULT_NAVIGATE_FALLBACK_DENYLIST = [/^\/api/, /^\/auth/];
|
||||
|
||||
/**
|
||||
* Default glob ignores (SQLite WASM for offline-first apps)
|
||||
*/
|
||||
export const DEFAULT_GLOB_IGNORES = ['**/*sqlite*'];
|
||||
|
||||
/**
|
||||
* Standard PWA icon configuration
|
||||
*/
|
||||
export const DEFAULT_ICONS: ManifestIcon[] = [
|
||||
{
|
||||
src: 'pwa-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'pwa-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'pwa-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Apple touch icon configuration
|
||||
*/
|
||||
export const APPLE_TOUCH_ICON = {
|
||||
src: 'apple-touch-icon.png',
|
||||
sizes: '180x180',
|
||||
type: 'image/png',
|
||||
};
|
||||
53
packages/shared-pwa/src/index.ts
Normal file
53
packages/shared-pwa/src/index.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* @manacore/shared-pwa
|
||||
*
|
||||
* Unified PWA configuration for all ManaCore SvelteKit apps.
|
||||
* Provides factory functions, presets, and defaults for consistent PWA setup.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createPWAConfig } from '@manacore/shared-pwa';
|
||||
* import { SvelteKitPWA } from '@vite-pwa/sveltekit';
|
||||
*
|
||||
* export default defineConfig({
|
||||
* plugins: [
|
||||
* sveltekit(),
|
||||
* SvelteKitPWA(createPWAConfig({
|
||||
* name: 'My App',
|
||||
* shortName: 'MyApp',
|
||||
* description: 'My awesome app',
|
||||
* themeColor: '#3b82f6',
|
||||
* })),
|
||||
* ],
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Main factory functions
|
||||
export { createPWAConfig, createOfflineFirstPWAConfig } from './config.js';
|
||||
|
||||
// Presets and cache strategies
|
||||
export { getPresetRuntimeCaching, cacheStrategies } from './presets.js';
|
||||
|
||||
// Default values
|
||||
export {
|
||||
DEFAULT_BACKGROUND_COLOR,
|
||||
DEFAULT_CATEGORIES,
|
||||
DEFAULT_INCLUDE_ASSETS,
|
||||
DEFAULT_GLOB_PATTERNS,
|
||||
DEFAULT_GLOB_IGNORES,
|
||||
DEFAULT_NAVIGATE_FALLBACK_DENYLIST,
|
||||
DEFAULT_ICONS,
|
||||
APPLE_TOUCH_ICON,
|
||||
} from './defaults.js';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
PWAConfigOptions,
|
||||
PWAConfig,
|
||||
PWAShortcut,
|
||||
WorkboxPreset,
|
||||
ManifestConfig,
|
||||
ManifestIcon,
|
||||
WorkboxConfig,
|
||||
} from './types.js';
|
||||
124
packages/shared-pwa/src/presets.ts
Normal file
124
packages/shared-pwa/src/presets.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* Workbox Runtime Caching Presets
|
||||
*
|
||||
* Provides pre-configured caching strategies for different app types:
|
||||
* - minimal: Static assets only
|
||||
* - standard: + API + Images
|
||||
* - full: + Fonts + External resources
|
||||
*/
|
||||
|
||||
import type { RuntimeCaching } from 'workbox-build';
|
||||
import type { WorkboxPreset } from './types.js';
|
||||
|
||||
/**
|
||||
* API caching strategy - NetworkFirst with fallback
|
||||
* Used for all *.mana.how API endpoints
|
||||
*/
|
||||
const API_CACHE: RuntimeCaching = {
|
||||
urlPattern: /^https:\/\/.*\.mana\.how\/api\/.*/i,
|
||||
handler: 'NetworkFirst',
|
||||
options: {
|
||||
cacheName: 'api-cache',
|
||||
expiration: {
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 60 * 60 * 24, // 24 hours
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Image caching strategy - CacheFirst for performance
|
||||
* Caches images for 30 days
|
||||
*/
|
||||
const IMAGE_CACHE: RuntimeCaching = {
|
||||
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/i,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'image-cache',
|
||||
expiration: {
|
||||
maxEntries: 200,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Font caching strategy - CacheFirst with long expiration
|
||||
* For Google Fonts and other web fonts
|
||||
*/
|
||||
const FONT_CACHE: RuntimeCaching = {
|
||||
urlPattern: /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'font-cache',
|
||||
expiration: {
|
||||
maxEntries: 30,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* External resources caching - StaleWhileRevalidate
|
||||
* For CDN resources and external APIs
|
||||
*/
|
||||
const EXTERNAL_CACHE: RuntimeCaching = {
|
||||
urlPattern: /^https:\/\/cdn\..*/i,
|
||||
handler: 'StaleWhileRevalidate',
|
||||
options: {
|
||||
cacheName: 'external-cache',
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 7, // 7 days
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Preset configurations for different caching strategies
|
||||
*/
|
||||
const PRESETS: Record<WorkboxPreset, RuntimeCaching[]> = {
|
||||
/**
|
||||
* Minimal preset - Only static assets (precached)
|
||||
* Use for simple apps without API calls
|
||||
*/
|
||||
minimal: [],
|
||||
|
||||
/**
|
||||
* Standard preset - Static + API + Images
|
||||
* Recommended for most apps
|
||||
*/
|
||||
standard: [API_CACHE, IMAGE_CACHE],
|
||||
|
||||
/**
|
||||
* Full preset - Standard + Fonts + External resources
|
||||
* Use for apps with custom fonts or external CDN resources
|
||||
*/
|
||||
full: [API_CACHE, IMAGE_CACHE, FONT_CACHE, EXTERNAL_CACHE],
|
||||
};
|
||||
|
||||
/**
|
||||
* Get runtime caching rules for a preset
|
||||
*/
|
||||
export function getPresetRuntimeCaching(preset: WorkboxPreset): RuntimeCaching[] {
|
||||
return PRESETS[preset] ?? PRESETS.standard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export individual cache strategies for custom configurations
|
||||
*/
|
||||
export const cacheStrategies = {
|
||||
api: API_CACHE,
|
||||
images: IMAGE_CACHE,
|
||||
fonts: FONT_CACHE,
|
||||
external: EXTERNAL_CACHE,
|
||||
};
|
||||
180
packages/shared-pwa/src/types.ts
Normal file
180
packages/shared-pwa/src/types.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/**
|
||||
* PWA Configuration Types for ManaCore Apps
|
||||
*/
|
||||
|
||||
import type { SvelteKitPWAOptions } from '@vite-pwa/sveltekit';
|
||||
import type { RuntimeCaching, ManifestEntry } from 'workbox-build';
|
||||
|
||||
/**
|
||||
* Workbox preset types for different caching strategies
|
||||
*/
|
||||
export type WorkboxPreset = 'minimal' | 'standard' | 'full';
|
||||
|
||||
/**
|
||||
* PWA manifest shortcut
|
||||
*/
|
||||
export interface PWAShortcut {
|
||||
name: string;
|
||||
short_name?: string;
|
||||
description?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for createPWAConfig
|
||||
*/
|
||||
export interface PWAConfigOptions {
|
||||
/**
|
||||
* Full name of the app (displayed in install prompts, app switcher)
|
||||
* @example "Calendar - Kalender"
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Short name for home screen icons (max ~12 chars)
|
||||
* @example "Calendar"
|
||||
*/
|
||||
shortName: string;
|
||||
|
||||
/**
|
||||
* App description for store listings
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Primary theme color (address bar, splash screen)
|
||||
* @example "#3b82f6"
|
||||
*/
|
||||
themeColor: string;
|
||||
|
||||
/**
|
||||
* Background color for splash screen
|
||||
* @default "#09090b"
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
|
||||
/**
|
||||
* Workbox caching preset
|
||||
* - minimal: Only static assets (simple apps without API)
|
||||
* - standard: + API (NetworkFirst) + Images (CacheFirst)
|
||||
* - full: + Fonts + External Resources
|
||||
* @default "standard"
|
||||
*/
|
||||
preset?: WorkboxPreset;
|
||||
|
||||
/**
|
||||
* App shortcuts for quick actions
|
||||
*/
|
||||
shortcuts?: PWAShortcut[];
|
||||
|
||||
/**
|
||||
* App categories for store listings
|
||||
* @default ["productivity", "utilities"]
|
||||
*/
|
||||
categories?: string[];
|
||||
|
||||
/**
|
||||
* Additional assets to include (besides default icons)
|
||||
*/
|
||||
includeAssets?: string[];
|
||||
|
||||
/**
|
||||
* Additional glob patterns to ignore in precaching
|
||||
*/
|
||||
globIgnores?: string[];
|
||||
|
||||
/**
|
||||
* Additional runtime caching rules
|
||||
*/
|
||||
additionalRuntimeCaching?: RuntimeCaching[];
|
||||
|
||||
/**
|
||||
* Custom navigate fallback path
|
||||
* @default "/offline"
|
||||
*/
|
||||
navigateFallback?: string;
|
||||
|
||||
/**
|
||||
* URL patterns to exclude from navigate fallback
|
||||
* @default [/^\/api/, /^\/auth/]
|
||||
*/
|
||||
navigateFallbackDenylist?: RegExp[];
|
||||
|
||||
/**
|
||||
* Enable PWA in development mode
|
||||
* @default true
|
||||
*/
|
||||
devEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* Service worker register type
|
||||
* @default "autoUpdate"
|
||||
*/
|
||||
registerType?: 'autoUpdate' | 'prompt';
|
||||
|
||||
/**
|
||||
* App language
|
||||
* @default "de"
|
||||
*/
|
||||
lang?: string;
|
||||
|
||||
/**
|
||||
* Start URL when app is launched
|
||||
* @default "/"
|
||||
*/
|
||||
startUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal manifest icon configuration
|
||||
*/
|
||||
export interface ManifestIcon {
|
||||
src: string;
|
||||
sizes: string;
|
||||
type: string;
|
||||
purpose?: 'any' | 'maskable' | 'monochrome';
|
||||
}
|
||||
|
||||
/**
|
||||
* Full manifest configuration
|
||||
*/
|
||||
export interface ManifestConfig {
|
||||
name: string;
|
||||
short_name: string;
|
||||
description: string;
|
||||
theme_color: string;
|
||||
background_color: string;
|
||||
display: 'standalone' | 'fullscreen' | 'minimal-ui' | 'browser';
|
||||
orientation: 'any' | 'portrait' | 'landscape';
|
||||
scope: string;
|
||||
start_url: string;
|
||||
lang: string;
|
||||
categories: string[];
|
||||
icons: ManifestIcon[];
|
||||
shortcuts?: Array<{
|
||||
name: string;
|
||||
short_name?: string;
|
||||
description?: string;
|
||||
url: string;
|
||||
icons?: Array<{ src: string; sizes: string }>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workbox configuration subset
|
||||
*/
|
||||
export interface WorkboxConfig {
|
||||
globPatterns: string[];
|
||||
globIgnores?: string[];
|
||||
cleanupOutdatedCaches: boolean;
|
||||
clientsClaim: boolean;
|
||||
skipWaiting: boolean;
|
||||
navigateFallback: string;
|
||||
navigateFallbackDenylist: RegExp[];
|
||||
runtimeCaching: RuntimeCaching[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete PWA configuration result
|
||||
*/
|
||||
export type PWAConfig = SvelteKitPWAOptions;
|
||||
19
packages/shared-pwa/tsconfig.json
Normal file
19
packages/shared-pwa/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ import {
|
|||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Put,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
|
|
@ -29,6 +31,8 @@ import { ResendVerificationDto } from './dto/resend-verification.dto';
|
|||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { ChangePasswordDto } from './dto/change-password.dto';
|
||||
import { DeleteAccountDto } from './dto/delete-account.dto';
|
||||
import { UpdateOrganizationDto } from './dto/update-organization.dto';
|
||||
import { UpdateMemberRoleDto } from './dto/update-member-role.dto';
|
||||
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||
import type { CurrentUserData } from '../common/decorators/current-user.decorator';
|
||||
|
|
@ -534,6 +538,162 @@ export class AuthController {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update organization
|
||||
*
|
||||
* Updates an organization's name, logo, or metadata.
|
||||
* Requires owner or admin role.
|
||||
*/
|
||||
@Put('organizations/:id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Update organization',
|
||||
description: 'Update organization name, logo, or metadata. Requires admin or owner role.',
|
||||
})
|
||||
@ApiBody({ type: UpdateOrganizationDto })
|
||||
@ApiResponse({ status: 200, description: 'Organization updated successfully' })
|
||||
@ApiResponse({ status: 401, description: 'Not authenticated' })
|
||||
@ApiResponse({ status: 403, description: 'No permission to update organization' })
|
||||
@ApiResponse({ status: 404, description: 'Organization not found' })
|
||||
async updateOrganization(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateOrganizationDto,
|
||||
@Headers('authorization') authorization: string
|
||||
) {
|
||||
const token = this.extractToken(authorization);
|
||||
return this.betterAuthService.updateOrganization(id, dto, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete organization
|
||||
*
|
||||
* Permanently deletes an organization and all its data.
|
||||
* Requires owner role.
|
||||
*/
|
||||
@Delete('organizations/:id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Delete organization',
|
||||
description: 'Permanently delete an organization. Only the owner can delete.',
|
||||
})
|
||||
@ApiResponse({ status: 204, description: 'Organization deleted successfully' })
|
||||
@ApiResponse({ status: 401, description: 'Not authenticated' })
|
||||
@ApiResponse({ status: 403, description: 'Only owner can delete organization' })
|
||||
@ApiResponse({ status: 404, description: 'Organization not found' })
|
||||
async deleteOrganization(
|
||||
@Param('id') id: string,
|
||||
@Headers('authorization') authorization: string
|
||||
) {
|
||||
const token = this.extractToken(authorization);
|
||||
await this.betterAuthService.deleteOrganization(id, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update member role
|
||||
*
|
||||
* Changes a member's role within an organization.
|
||||
* Requires owner or admin role.
|
||||
*/
|
||||
@Patch('organizations/:orgId/members/:memberId/role')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Update member role',
|
||||
description: "Change a member's role. Requires admin or owner role.",
|
||||
})
|
||||
@ApiBody({ type: UpdateMemberRoleDto })
|
||||
@ApiResponse({ status: 200, description: 'Member role updated successfully' })
|
||||
@ApiResponse({ status: 401, description: 'Not authenticated' })
|
||||
@ApiResponse({ status: 403, description: 'No permission to change roles' })
|
||||
@ApiResponse({ status: 404, description: 'Member not found' })
|
||||
async updateMemberRole(
|
||||
@Param('orgId') orgId: string,
|
||||
@Param('memberId') memberId: string,
|
||||
@Body() dto: UpdateMemberRoleDto,
|
||||
@Headers('authorization') authorization: string
|
||||
) {
|
||||
const token = this.extractToken(authorization);
|
||||
return this.betterAuthService.updateMemberRole(orgId, memberId, dto.role, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* List organization invitations
|
||||
*
|
||||
* Returns all pending invitations for an organization.
|
||||
* Requires owner or admin role.
|
||||
*/
|
||||
@Get('organizations/:id/invitations')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'List organization invitations',
|
||||
description: 'Get all pending invitations for an organization.',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Returns list of invitations' })
|
||||
@ApiResponse({ status: 401, description: 'Not authenticated' })
|
||||
async listOrganizationInvitations(
|
||||
@Param('id') id: string,
|
||||
@Headers('authorization') authorization: string
|
||||
) {
|
||||
const token = this.extractToken(authorization);
|
||||
return this.betterAuthService.listOrganizationInvitations(id, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* List user's pending invitations
|
||||
*
|
||||
* Returns all pending invitations for the authenticated user.
|
||||
*/
|
||||
@Get('invitations')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'List user invitations',
|
||||
description: 'Get all pending invitations for the current user.',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Returns list of invitations' })
|
||||
@ApiResponse({ status: 401, description: 'Not authenticated' })
|
||||
async listUserInvitations(@Headers('authorization') authorization: string) {
|
||||
const token = this.extractToken(authorization);
|
||||
return this.betterAuthService.listUserInvitations(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel or reject invitation
|
||||
*
|
||||
* Cancels an invitation (for org admins) or rejects it (for invitees).
|
||||
* The system automatically determines which action to take based on the user's role.
|
||||
*/
|
||||
@Delete('invitations/:id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Cancel or reject invitation',
|
||||
description:
|
||||
'Cancel (as org admin/owner) or reject (as invitee) a pending invitation.',
|
||||
})
|
||||
@ApiResponse({ status: 204, description: 'Invitation cancelled/rejected successfully' })
|
||||
@ApiResponse({ status: 401, description: 'Not authenticated' })
|
||||
@ApiResponse({ status: 404, description: 'Invitation not found' })
|
||||
async cancelOrRejectInvitation(
|
||||
@Param('id') id: string,
|
||||
@Headers('authorization') authorization: string
|
||||
) {
|
||||
const token = this.extractToken(authorization);
|
||||
// Try cancel first (for org owners/admins), if fails try reject (for invitees)
|
||||
try {
|
||||
await this.betterAuthService.cancelInvitation(id, token);
|
||||
} catch {
|
||||
await this.betterAuthService.rejectInvitation(id, token);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export { RegisterB2BDto } from './register-b2b.dto';
|
|||
export { InviteEmployeeDto } from './invite-employee.dto';
|
||||
export { AcceptInvitationDto } from './accept-invitation.dto';
|
||||
export { SetActiveOrganizationDto } from './set-active-organization.dto';
|
||||
export { UpdateOrganizationDto } from './update-organization.dto';
|
||||
export { UpdateMemberRoleDto } from './update-member-role.dto';
|
||||
|
||||
// Password management DTOs
|
||||
export { ForgotPasswordDto } from './forgot-password.dto';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { IsString, IsIn } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* DTO for updating a member's role within an organization
|
||||
*
|
||||
* Note: 'owner' role cannot be assigned via this endpoint.
|
||||
* To transfer ownership, use the dedicated transfer ownership endpoint.
|
||||
*/
|
||||
export class UpdateMemberRoleDto {
|
||||
@ApiProperty({
|
||||
description: 'New role for the member',
|
||||
enum: ['admin', 'member'],
|
||||
example: 'admin',
|
||||
})
|
||||
@IsString()
|
||||
@IsIn(['admin', 'member'])
|
||||
role: 'admin' | 'member';
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { IsString, IsOptional, MaxLength, MinLength } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* DTO for updating an organization
|
||||
*
|
||||
* All fields are optional - only provided fields will be updated.
|
||||
*/
|
||||
export class UpdateOrganizationDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'New name for the organization',
|
||||
minLength: 2,
|
||||
maxLength: 255,
|
||||
example: 'Acme Corporation',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MinLength(2)
|
||||
@MaxLength(255)
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'URL to organization logo',
|
||||
maxLength: 500,
|
||||
example: 'https://example.com/logo.png',
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(500)
|
||||
logo?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Additional metadata for the organization',
|
||||
example: { industry: 'Technology', size: 'Enterprise' },
|
||||
})
|
||||
@IsOptional()
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
|
@ -58,6 +58,7 @@ import type {
|
|||
ValidateTokenResult,
|
||||
TokenPayload,
|
||||
OrganizationMember,
|
||||
OrganizationInvitation,
|
||||
Organization,
|
||||
BetterAuthAPI,
|
||||
SignUpResponse,
|
||||
|
|
@ -721,6 +722,252 @@ export class BetterAuthService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update organization
|
||||
*
|
||||
* Updates an organization's name, logo, or metadata.
|
||||
* Requires owner or admin role.
|
||||
*
|
||||
* @param organizationId - Organization ID
|
||||
* @param data - Fields to update (name, logo, metadata)
|
||||
* @param token - User's authentication token
|
||||
* @returns Updated organization
|
||||
* @throws ForbiddenException if user lacks permission
|
||||
* @throws NotFoundException if organization not found
|
||||
*/
|
||||
async updateOrganization(
|
||||
organizationId: string,
|
||||
data: { name?: string; logo?: string; metadata?: Record<string, unknown> },
|
||||
token: string
|
||||
): Promise<Organization> {
|
||||
try {
|
||||
const result = await (this.orgApi as any).updateOrganization({
|
||||
body: {
|
||||
organizationId,
|
||||
data: {
|
||||
...(data.name !== undefined && { name: data.name }),
|
||||
...(data.logo !== undefined && { logo: data.logo }),
|
||||
...(data.metadata !== undefined && { metadata: data.metadata }),
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message?.includes('not found')) {
|
||||
throw new NotFoundException('Organization not found');
|
||||
}
|
||||
if (error.message?.includes('permission') || error.message?.includes('unauthorized')) {
|
||||
throw new ForbiddenException('You do not have permission to update this organization');
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete organization
|
||||
*
|
||||
* Deletes an organization and all its data.
|
||||
* Requires owner role.
|
||||
*
|
||||
* @param organizationId - Organization ID
|
||||
* @param token - User's authentication token
|
||||
* @throws ForbiddenException if user is not the owner
|
||||
* @throws NotFoundException if organization not found
|
||||
*/
|
||||
async deleteOrganization(organizationId: string, token: string): Promise<void> {
|
||||
try {
|
||||
await (this.orgApi as any).deleteOrganization({
|
||||
body: { organizationId },
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message?.includes('not found')) {
|
||||
throw new NotFoundException('Organization not found');
|
||||
}
|
||||
if (error.message?.includes('permission') || error.message?.includes('unauthorized')) {
|
||||
throw new ForbiddenException('Only the owner can delete the organization');
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update member role
|
||||
*
|
||||
* Changes a member's role within an organization.
|
||||
* Requires owner or admin role.
|
||||
*
|
||||
* @param organizationId - Organization ID
|
||||
* @param memberId - Member ID to update
|
||||
* @param role - New role ('admin' or 'member')
|
||||
* @param token - User's authentication token
|
||||
* @returns Updated member
|
||||
* @throws ForbiddenException if user lacks permission
|
||||
* @throws NotFoundException if member not found
|
||||
*/
|
||||
async updateMemberRole(
|
||||
organizationId: string,
|
||||
memberId: string,
|
||||
role: 'admin' | 'member',
|
||||
token: string
|
||||
): Promise<OrganizationMember> {
|
||||
try {
|
||||
const result = await (this.orgApi as any).updateMemberRole({
|
||||
body: {
|
||||
organizationId,
|
||||
memberId,
|
||||
role,
|
||||
},
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return result?.member || result;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message?.includes('not found')) {
|
||||
throw new NotFoundException('Member not found');
|
||||
}
|
||||
if (error.message?.includes('permission') || error.message?.includes('unauthorized')) {
|
||||
throw new ForbiddenException('You do not have permission to change member roles');
|
||||
}
|
||||
if (error.message?.includes('owner')) {
|
||||
throw new ForbiddenException("Cannot change the owner's role");
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List organization invitations
|
||||
*
|
||||
* Returns all pending invitations for an organization.
|
||||
* Requires owner or admin role.
|
||||
*
|
||||
* @param organizationId - Organization ID
|
||||
* @param token - User's authentication token
|
||||
* @returns List of invitations
|
||||
*/
|
||||
async listOrganizationInvitations(
|
||||
organizationId: string,
|
||||
token: string
|
||||
): Promise<OrganizationInvitation[]> {
|
||||
try {
|
||||
const result = await (this.orgApi as any).listInvitations({
|
||||
query: { organizationId },
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return result?.invitations || result || [];
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(
|
||||
'Failed to list organization invitations',
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List user's pending invitations
|
||||
*
|
||||
* Returns all pending invitations for the authenticated user.
|
||||
*
|
||||
* @param token - User's authentication token
|
||||
* @returns List of invitations
|
||||
*/
|
||||
async listUserInvitations(token: string): Promise<OrganizationInvitation[]> {
|
||||
try {
|
||||
const result = (await (this.orgApi as any).getInvitation)
|
||||
? await (this.orgApi as any).listUserInvitations({
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
return result?.invitations || result || [];
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(
|
||||
'Failed to list user invitations',
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an invitation
|
||||
*
|
||||
* Cancels a pending invitation. Used by organization admins/owners.
|
||||
*
|
||||
* @param invitationId - Invitation ID
|
||||
* @param token - User's authentication token
|
||||
* @throws ForbiddenException if user lacks permission
|
||||
* @throws NotFoundException if invitation not found
|
||||
*/
|
||||
async cancelInvitation(invitationId: string, token: string): Promise<void> {
|
||||
try {
|
||||
await (this.orgApi as any).cancelInvitation({
|
||||
body: { invitationId },
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message?.includes('not found')) {
|
||||
throw new NotFoundException('Invitation not found');
|
||||
}
|
||||
if (error.message?.includes('permission') || error.message?.includes('unauthorized')) {
|
||||
throw new ForbiddenException('You do not have permission to cancel this invitation');
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject an invitation
|
||||
*
|
||||
* Rejects a pending invitation. Used by the invited user.
|
||||
*
|
||||
* @param invitationId - Invitation ID
|
||||
* @param token - User's authentication token
|
||||
* @throws NotFoundException if invitation not found
|
||||
*/
|
||||
async rejectInvitation(invitationId: string, token: string): Promise<void> {
|
||||
try {
|
||||
await (this.orgApi as any).rejectInvitation({
|
||||
body: { invitationId },
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message?.includes('not found')) {
|
||||
throw new NotFoundException('Invitation not found');
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Token Management Methods
|
||||
// =========================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue