feat: auth system, scrip scheduler, UI widgets, and new API routes
- Add session-based authentication (login page, middleware, auth context) - Add cron-like scrip scheduler for time-based conditions - Add layout builder, scrip wizard, searchable select components - Add trend chart widget for dashboards - Add notifications, attachments, queue-permissions API routes - Add seed-users script - Update schema with 10 new migrations (0008-0017) - Apply redesign: Linear-inspired dark theme, conversation-centric UI - Gitignore runtime data directory Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,13 +5,17 @@ export interface LifecycleDefinition {
|
||||
inactive: string[];
|
||||
};
|
||||
transitions: Record<string, string[]>;
|
||||
transition_rights?: Record<string, string>; // "from→to" → rightName
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
requiredRight?: string; // Named right required for this transition, if any
|
||||
}
|
||||
|
||||
const FALLBACK_RIGHT = 'ticket.modify';
|
||||
|
||||
export class LifecycleValidator {
|
||||
validateTransition(
|
||||
lifecycleDef: LifecycleDefinition,
|
||||
@@ -35,13 +39,15 @@ export class LifecycleValidator {
|
||||
const allowedTransitions = this.getAllowedTransitions(lifecycleDef, fromStatus);
|
||||
|
||||
if (allowedTransitions.includes(toStatus)) {
|
||||
return { valid: true };
|
||||
const right = this.getRequiredRight(lifecycleDef, fromStatus, toStatus);
|
||||
return { valid: true, requiredRight: right ?? FALLBACK_RIGHT };
|
||||
}
|
||||
|
||||
// Also handle wildcard "*" -> any transition
|
||||
const wildcardTransitions = this.getAllowedTransitions(lifecycleDef, '*');
|
||||
if (wildcardTransitions.includes(toStatus)) {
|
||||
return { valid: true };
|
||||
const right = this.getRequiredRight(lifecycleDef, fromStatus, toStatus);
|
||||
return { valid: true, requiredRight: right ?? FALLBACK_RIGHT };
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -50,6 +56,37 @@ export class LifecycleValidator {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required right for a transition using RT's 4-level priority:
|
||||
* 1. exact "from→to"
|
||||
* 2. wildcard from "*→to"
|
||||
* 3. wildcard to "from→*"
|
||||
* 4. full wildcard "*→*"
|
||||
* 5. fallback: ticket.modify
|
||||
*/
|
||||
getRequiredRight(
|
||||
lifecycleDef: LifecycleDefinition,
|
||||
fromStatus: string,
|
||||
toStatus: string,
|
||||
): string | null {
|
||||
const rights = lifecycleDef.transition_rights ?? {};
|
||||
|
||||
// Priority 1: exact match
|
||||
if (rights[`${fromStatus}→${toStatus}`]) return rights[`${fromStatus}→${toStatus}`];
|
||||
|
||||
// Priority 2: wildcard from
|
||||
if (rights[`*→${toStatus}`]) return rights[`*→${toStatus}`];
|
||||
|
||||
// Priority 3: wildcard to
|
||||
if (rights[`${fromStatus}→*`]) return rights[`${fromStatus}→*`];
|
||||
|
||||
// Priority 4: full wildcard
|
||||
if (rights['*→*']) return rights['*→*'];
|
||||
|
||||
// Priority 5: fallback
|
||||
return null;
|
||||
}
|
||||
|
||||
isResolvedStatus(lifecycleDef: LifecycleDefinition, status: string): boolean {
|
||||
return lifecycleDef.statuses.inactive.includes(status);
|
||||
}
|
||||
@@ -58,16 +95,12 @@ export class LifecycleValidator {
|
||||
lifecycleDef: LifecycleDefinition,
|
||||
fromStatus: string,
|
||||
): string[] {
|
||||
// Direct transition
|
||||
if (lifecycleDef.transitions[fromStatus]) {
|
||||
return lifecycleDef.transitions[fromStatus]!;
|
||||
}
|
||||
|
||||
// Wildcard transitions
|
||||
if (lifecycleDef.transitions['*']) {
|
||||
return lifecycleDef.transitions['*']!;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user