hazo_auth 10.2.2 → 10.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-src/lib/auth/deny_permission.server.ts +101 -0
- package/cli-src/lib/auth/index.ts +8 -0
- package/cli-src/lib/auth/with_permission_issue_capture.server.ts +222 -0
- package/cli-src/lib/services/find_roles_with_permission.ts +47 -0
- package/dist/admin-issues/permission_denied_provider.client.d.ts +14 -0
- package/dist/admin-issues/permission_denied_provider.client.d.ts.map +1 -0
- package/dist/admin-issues/permission_denied_provider.client.js +80 -0
- package/dist/admin-issues/plugin.client.d.ts +34 -0
- package/dist/admin-issues/plugin.client.d.ts.map +1 -0
- package/dist/admin-issues/plugin.client.js +64 -0
- package/dist/admin-issues/plugin.server.d.ts +59 -0
- package/dist/admin-issues/plugin.server.d.ts.map +1 -0
- package/dist/admin-issues/plugin.server.js +168 -0
- package/dist/admin-issues/wire.server.d.ts +23 -0
- package/dist/admin-issues/wire.server.d.ts.map +1 -0
- package/dist/admin-issues/wire.server.js +30 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +6 -0
- package/dist/components/layouts/create_firm/index.d.ts +2 -1
- package/dist/components/layouts/create_firm/index.d.ts.map +1 -1
- package/dist/components/layouts/dev_lock/index.d.ts +1 -1
- package/dist/components/layouts/dev_lock/index.d.ts.map +1 -1
- package/dist/components/layouts/email_verification/index.d.ts +1 -1
- package/dist/components/layouts/email_verification/index.d.ts.map +1 -1
- package/dist/components/layouts/forgot_password/index.d.ts +1 -1
- package/dist/components/layouts/forgot_password/index.d.ts.map +1 -1
- package/dist/components/layouts/google_token_test/index.d.ts +1 -1
- package/dist/components/layouts/google_token_test/index.d.ts.map +1 -1
- package/dist/components/layouts/legal/legal_acceptance_gate.d.ts +1 -1
- package/dist/components/layouts/legal/legal_acceptance_gate.d.ts.map +1 -1
- package/dist/components/layouts/legal/legal_doc_checkbox_list.d.ts +1 -1
- package/dist/components/layouts/legal/legal_doc_checkbox_list.d.ts.map +1 -1
- package/dist/components/layouts/legal/legal_doc_combined_view.d.ts +1 -1
- package/dist/components/layouts/legal/legal_doc_combined_view.d.ts.map +1 -1
- package/dist/components/layouts/legal/legal_doc_drawer.d.ts +1 -1
- package/dist/components/layouts/legal/legal_doc_drawer.d.ts.map +1 -1
- package/dist/components/layouts/login/index.d.ts +1 -1
- package/dist/components/layouts/login/index.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/connected_accounts_section.d.ts +1 -1
- package/dist/components/layouts/my_settings/components/connected_accounts_section.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/editable_field.d.ts +1 -1
- package/dist/components/layouts/my_settings/components/editable_field.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/password_change_dialog.d.ts +1 -1
- package/dist/components/layouts/my_settings/components/password_change_dialog.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_dialog.d.ts +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_dialog.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_display.d.ts +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_display.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_gravatar_tab.d.ts +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_gravatar_tab.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_library_tab.d.ts +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_library_tab.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_upload_tab.d.ts +7 -1
- package/dist/components/layouts/my_settings/components/profile_picture_upload_tab.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_upload_tab.js +46 -15
- package/dist/components/layouts/my_settings/components/set_password_section.d.ts +1 -1
- package/dist/components/layouts/my_settings/components/set_password_section.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/index.d.ts +1 -1
- package/dist/components/layouts/my_settings/index.d.ts.map +1 -1
- package/dist/components/layouts/register/index.d.ts +1 -1
- package/dist/components/layouts/register/index.d.ts.map +1 -1
- package/dist/components/layouts/reset_password/index.d.ts +1 -1
- package/dist/components/layouts/reset_password/index.d.ts.map +1 -1
- package/dist/components/layouts/scope_management/components/branding_editor.d.ts +1 -1
- package/dist/components/layouts/scope_management/components/branding_editor.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/already_logged_in_guard.d.ts +1 -1
- package/dist/components/layouts/shared/components/already_logged_in_guard.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/auth_navbar.d.ts +1 -1
- package/dist/components/layouts/shared/components/auth_navbar.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/auth_page_shell.d.ts +1 -1
- package/dist/components/layouts/shared/components/auth_page_shell.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts +1 -1
- package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/field_error_message.d.ts +1 -1
- package/dist/components/layouts/shared/components/field_error_message.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/floating_home_link.d.ts +1 -1
- package/dist/components/layouts/shared/components/floating_home_link.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/form_action_buttons.d.ts +1 -1
- package/dist/components/layouts/shared/components/form_action_buttons.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/form_field_wrapper.d.ts +1 -1
- package/dist/components/layouts/shared/components/form_field_wrapper.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/form_header.d.ts +1 -1
- package/dist/components/layouts/shared/components/form_header.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/google_icon.d.ts +1 -1
- package/dist/components/layouts/shared/components/google_icon.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/google_sign_in_button.d.ts +1 -1
- package/dist/components/layouts/shared/components/google_sign_in_button.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/logout_button.d.ts +1 -1
- package/dist/components/layouts/shared/components/logout_button.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/oauth_divider.d.ts +1 -1
- package/dist/components/layouts/shared/components/oauth_divider.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/password_field.d.ts +1 -1
- package/dist/components/layouts/shared/components/password_field.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/profile_pic_menu.d.ts +1 -1
- package/dist/components/layouts/shared/components/profile_pic_menu.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/profile_pic_menu_wrapper.d.ts +1 -1
- package/dist/components/layouts/shared/components/profile_pic_menu_wrapper.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/profile_stamp.d.ts +1 -1
- package/dist/components/layouts/shared/components/profile_stamp.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts +1 -1
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/standalone_layout_wrapper.d.ts +1 -1
- package/dist/components/layouts/shared/components/standalone_layout_wrapper.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/two_column_auth_layout.d.ts +1 -1
- package/dist/components/layouts/shared/components/two_column_auth_layout.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/unauthorized_guard.d.ts +1 -1
- package/dist/components/layouts/shared/components/unauthorized_guard.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/visual_panel.d.ts +1 -1
- package/dist/components/layouts/shared/components/visual_panel.d.ts.map +1 -1
- package/dist/components/layouts/user_management/components/app_user_data_editor.d.ts +1 -1
- package/dist/components/layouts/user_management/components/app_user_data_editor.d.ts.map +1 -1
- package/dist/components/layouts/user_management/components/roles_matrix.d.ts +1 -1
- package/dist/components/layouts/user_management/components/roles_matrix.d.ts.map +1 -1
- package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts +1 -1
- package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts.map +1 -1
- package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts +1 -1
- package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts.map +1 -1
- package/dist/components/layouts/user_management/index.d.ts +1 -1
- package/dist/components/layouts/user_management/index.d.ts.map +1 -1
- package/dist/components/permission_denied_dialog.client.d.ts +19 -0
- package/dist/components/permission_denied_dialog.client.d.ts.map +1 -0
- package/dist/components/permission_denied_dialog.client.js +112 -0
- package/dist/components/ui/alert-dialog.d.ts +2 -2
- package/dist/components/ui/dialog.d.ts +2 -2
- package/dist/components/ui/dropdown-menu.d.ts +1 -1
- package/dist/components/ui/hazo_ui_tooltip.d.ts +1 -1
- package/dist/components/ui/hazo_ui_tooltip.d.ts.map +1 -1
- package/dist/components/ui/sheet.d.ts +2 -2
- package/dist/components/ui/skeleton.d.ts +1 -1
- package/dist/components/ui/skeleton.d.ts.map +1 -1
- package/dist/components/ui/sonner.d.ts +1 -1
- package/dist/components/ui/sonner.d.ts.map +1 -1
- package/dist/components/ui/tree-view.d.ts +1 -1
- package/dist/components/ui/tree-view.d.ts.map +1 -1
- package/dist/components/ui/user-type-badge.d.ts +1 -1
- package/dist/components/ui/user-type-badge.d.ts.map +1 -1
- package/dist/consent/cookie_consent_banner.d.ts +1 -1
- package/dist/consent/cookie_consent_banner.d.ts.map +1 -1
- package/dist/consent/manage_modal.d.ts +1 -1
- package/dist/consent/manage_modal.d.ts.map +1 -1
- package/dist/contexts/hazo_auth_provider.d.ts +2 -2
- package/dist/contexts/hazo_auth_provider.d.ts.map +1 -1
- package/dist/lib/auth/deny_permission.server.d.ts +18 -0
- package/dist/lib/auth/deny_permission.server.d.ts.map +1 -0
- package/dist/lib/auth/deny_permission.server.js +66 -0
- package/dist/lib/auth/index.d.ts +2 -0
- package/dist/lib/auth/index.d.ts.map +1 -1
- package/dist/lib/auth/index.js +2 -0
- package/dist/lib/auth/with_permission_issue_capture.server.d.ts +49 -0
- package/dist/lib/auth/with_permission_issue_capture.server.d.ts.map +1 -0
- package/dist/lib/auth/with_permission_issue_capture.server.js +152 -0
- package/dist/lib/services/find_roles_with_permission.d.ts +12 -0
- package/dist/lib/services/find_roles_with_permission.d.ts.map +1 -0
- package/dist/lib/services/find_roles_with_permission.js +38 -0
- package/dist/page_components/create_firm.d.ts +1 -1
- package/dist/page_components/create_firm.d.ts.map +1 -1
- package/dist/page_components/dev_lock.d.ts +1 -1
- package/dist/page_components/dev_lock.d.ts.map +1 -1
- package/dist/page_components/my_settings.d.ts +1 -1
- package/dist/page_components/my_settings.d.ts.map +1 -1
- package/dist/server-lib.d.ts +7 -0
- package/dist/server-lib.d.ts.map +1 -1
- package/dist/server-lib.js +7 -0
- package/dist/server_pages/forgot_password.d.ts +1 -1
- package/dist/server_pages/forgot_password.d.ts.map +1 -1
- package/dist/server_pages/forgot_password_client_wrapper.d.ts +1 -1
- package/dist/server_pages/forgot_password_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/login.d.ts +1 -1
- package/dist/server_pages/login.d.ts.map +1 -1
- package/dist/server_pages/login_client_wrapper.d.ts +1 -1
- package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/my_settings.d.ts +1 -1
- package/dist/server_pages/my_settings.d.ts.map +1 -1
- package/dist/server_pages/register.d.ts +1 -1
- package/dist/server_pages/register.d.ts.map +1 -1
- package/dist/server_pages/register_client_wrapper.d.ts +1 -1
- package/dist/server_pages/register_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/reset_password.d.ts +1 -1
- package/dist/server_pages/reset_password.d.ts.map +1 -1
- package/dist/server_pages/reset_password_client_wrapper.d.ts +1 -1
- package/dist/server_pages/reset_password_client_wrapper.d.ts.map +1 -1
- package/dist/server_pages/verify_email.d.ts +1 -1
- package/dist/server_pages/verify_email.d.ts.map +1 -1
- package/dist/server_pages/verify_email_client_wrapper.d.ts +1 -1
- package/dist/server_pages/verify_email_client_wrapper.d.ts.map +1 -1
- package/dist/strings/strings_provider.d.ts +1 -1
- package/dist/strings/strings_provider.d.ts.map +1 -1
- package/dist/theme/theme_provider.d.ts +1 -1
- package/dist/theme/theme_provider.d.ts.map +1 -1
- package/package.json +19 -11
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// file_description: standalone helper to return a 403 FORBIDDEN response and fire the permission-denied handler
|
|
2
|
+
import 'server-only';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildDenialPayloadFromRequest,
|
|
6
|
+
buildForbiddenResponse,
|
|
7
|
+
getPermissionDeniedHandler,
|
|
8
|
+
type PermissionDeniedPayload,
|
|
9
|
+
} from './with_permission_issue_capture.server.js';
|
|
10
|
+
|
|
11
|
+
export interface DenyPermissionInput {
|
|
12
|
+
request: Request;
|
|
13
|
+
/** HazoAuthResult | TenantAuthResult | any auth-like shape */
|
|
14
|
+
auth: any;
|
|
15
|
+
missing_permissions: string[];
|
|
16
|
+
/** Defaults to missing_permissions */
|
|
17
|
+
required_permissions?: string[];
|
|
18
|
+
permission_descriptions?: Record<string, string>;
|
|
19
|
+
/** Falls back to X-Hazo-Action-Label header, then permission join */
|
|
20
|
+
action_label?: string;
|
|
21
|
+
/** Falls back to default message */
|
|
22
|
+
user_friendly_message?: string;
|
|
23
|
+
/** Falls back to new URL(request.url).pathname */
|
|
24
|
+
api_route?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractUserId(auth: any): string {
|
|
28
|
+
return (
|
|
29
|
+
auth?.user?.id ||
|
|
30
|
+
auth?.tenant?.user_id ||
|
|
31
|
+
auth?.user_id ||
|
|
32
|
+
''
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractScopeId(auth: any): string | undefined {
|
|
37
|
+
return (
|
|
38
|
+
auth?.tenant?.scope_id ??
|
|
39
|
+
auth?.scope_id ??
|
|
40
|
+
auth?.organization?.id ??
|
|
41
|
+
auth?.organization_id ??
|
|
42
|
+
undefined
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function denyPermission(input: DenyPermissionInput): Response {
|
|
47
|
+
const user_id = extractUserId(input.auth);
|
|
48
|
+
const scope_id = extractScopeId(input.auth);
|
|
49
|
+
|
|
50
|
+
const payload: PermissionDeniedPayload = buildDenialPayloadFromRequest(input.request, {
|
|
51
|
+
user_id,
|
|
52
|
+
scope_id,
|
|
53
|
+
missing_permissions: input.missing_permissions,
|
|
54
|
+
required_permissions: input.required_permissions,
|
|
55
|
+
permission_descriptions: input.permission_descriptions,
|
|
56
|
+
action_label: input.action_label,
|
|
57
|
+
user_friendly_message: input.user_friendly_message,
|
|
58
|
+
api_route: input.api_route,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Fire handler best-effort — do not await, must not delay the 403
|
|
62
|
+
Promise.resolve(getPermissionDeniedHandler()?.(payload)).catch((e) => {
|
|
63
|
+
console.warn('[hazo_auth] denyPermission: permission denied handler failed', e);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// buildForbiddenResponse is async (dynamic import of hazo_api), but denyPermission must be
|
|
67
|
+
// synchronous to match the function signature. We build the fallback synchronously here
|
|
68
|
+
// and fire the async hazo_api path only when available. In practice, the caller immediately
|
|
69
|
+
// returns the Response so we use the sync fallback — consumers that need the hazo_api
|
|
70
|
+
// envelope can await a wrapping async function or use withPermissionIssueCapture instead.
|
|
71
|
+
const bodyJson = JSON.stringify({
|
|
72
|
+
ok: false,
|
|
73
|
+
error: {
|
|
74
|
+
code: 'FORBIDDEN',
|
|
75
|
+
message: payload.user_friendly_message,
|
|
76
|
+
details: {
|
|
77
|
+
missing_permissions: payload.missing_permissions,
|
|
78
|
+
permission_descriptions: payload.permission_descriptions,
|
|
79
|
+
action_label: payload.action_label,
|
|
80
|
+
origin_url: payload.origin_url,
|
|
81
|
+
user_friendly_message: payload.user_friendly_message,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Kick off async hazo_api enhancement but return the sync response immediately.
|
|
87
|
+
// The returned Response object is the sync fallback; hazo_api is decorative for
|
|
88
|
+
// middleware use cases that don't need the exact envelope.
|
|
89
|
+
void import('hazo_api')
|
|
90
|
+
.then((m: any) => {
|
|
91
|
+
if (typeof m?.fail === 'function') {
|
|
92
|
+
// No-op: we already returned — this is a fire-and-forget; nothing to do with the result.
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
.catch(() => undefined);
|
|
96
|
+
|
|
97
|
+
return new Response(bodyJson, {
|
|
98
|
+
status: 403,
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -60,3 +60,11 @@ export { get_auth_cache, reset_auth_cache } from "./auth_cache.js";
|
|
|
60
60
|
// section: rate_limiter_exports
|
|
61
61
|
export { get_rate_limiter, reset_rate_limiter } from "./auth_rate_limiter.js";
|
|
62
62
|
|
|
63
|
+
// section: permission_issue_capture_exports
|
|
64
|
+
export {
|
|
65
|
+
withPermissionIssueCapture,
|
|
66
|
+
setPermissionDeniedHandler,
|
|
67
|
+
getPermissionDeniedHandler,
|
|
68
|
+
} from "./with_permission_issue_capture.server.js";
|
|
69
|
+
export type { PermissionDeniedPayload } from "./with_permission_issue_capture.server";
|
|
70
|
+
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// file_description: Route handler wrapper that catches PermissionError and returns a structured hazo_api fail() 403
|
|
2
|
+
// section: server-only-guard
|
|
3
|
+
import 'server-only';
|
|
4
|
+
|
|
5
|
+
// section: imports
|
|
6
|
+
import { type NextRequest } from 'next/server';
|
|
7
|
+
import { createLogger } from 'hazo_core';
|
|
8
|
+
import { PermissionError } from './auth_types.js';
|
|
9
|
+
import { hazo_get_auth } from './hazo_get_auth.server.js';
|
|
10
|
+
|
|
11
|
+
const log = createLogger('hazo_auth:permission_capture');
|
|
12
|
+
|
|
13
|
+
// section: types
|
|
14
|
+
|
|
15
|
+
export interface PermissionDeniedPayload {
|
|
16
|
+
user_id: string;
|
|
17
|
+
scope_id?: string;
|
|
18
|
+
missing_permissions: string[];
|
|
19
|
+
required_permissions: string[];
|
|
20
|
+
user_permissions: string[];
|
|
21
|
+
permission_descriptions: Record<string, string>;
|
|
22
|
+
user_friendly_message: string;
|
|
23
|
+
action_label: string;
|
|
24
|
+
origin_url: string;
|
|
25
|
+
api_route: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// section: global_callback
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Global callback, set once at app boot by the consuming app.
|
|
32
|
+
* DI: hazo_auth does NOT import hazo_admin; the app passes createOrBumpIssue here.
|
|
33
|
+
*/
|
|
34
|
+
let _onPermissionDenied: ((payload: PermissionDeniedPayload) => void | Promise<void>) | null = null;
|
|
35
|
+
|
|
36
|
+
export function setPermissionDeniedHandler(
|
|
37
|
+
fn: (payload: PermissionDeniedPayload) => void | Promise<void>,
|
|
38
|
+
): void {
|
|
39
|
+
_onPermissionDenied = fn;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getPermissionDeniedHandler() {
|
|
43
|
+
return _onPermissionDenied;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// section: build_payload_helper
|
|
47
|
+
|
|
48
|
+
export interface BuildDenialPayloadParts {
|
|
49
|
+
user_id: string;
|
|
50
|
+
scope_id?: string;
|
|
51
|
+
missing_permissions: string[];
|
|
52
|
+
required_permissions?: string[];
|
|
53
|
+
permission_descriptions?: Record<string, string>;
|
|
54
|
+
action_label?: string;
|
|
55
|
+
user_friendly_message?: string;
|
|
56
|
+
api_route?: string;
|
|
57
|
+
user_permissions?: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Shared helper: builds a PermissionDeniedPayload from a request + explicit parts.
|
|
62
|
+
* Called by both withPermissionIssueCapture and denyPermission.
|
|
63
|
+
*/
|
|
64
|
+
export function buildDenialPayloadFromRequest(
|
|
65
|
+
request: Request,
|
|
66
|
+
parts: BuildDenialPayloadParts,
|
|
67
|
+
): PermissionDeniedPayload {
|
|
68
|
+
const originUrl =
|
|
69
|
+
request.headers.get('x-hazo-origin-url') ||
|
|
70
|
+
request.headers.get('referer') ||
|
|
71
|
+
'';
|
|
72
|
+
|
|
73
|
+
const actionLabelFromHeader = request.headers.get('x-hazo-action-label') || '';
|
|
74
|
+
const descriptions = parts.permission_descriptions ?? {};
|
|
75
|
+
|
|
76
|
+
const actionLabel =
|
|
77
|
+
parts.action_label ||
|
|
78
|
+
actionLabelFromHeader ||
|
|
79
|
+
parts.missing_permissions.map((p) => descriptions[p] || p).join(', ');
|
|
80
|
+
|
|
81
|
+
let apiRoute = parts.api_route ?? '';
|
|
82
|
+
if (!apiRoute) {
|
|
83
|
+
try {
|
|
84
|
+
apiRoute = new URL(request.url).pathname;
|
|
85
|
+
} catch {
|
|
86
|
+
/* ignore */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
user_id: parts.user_id,
|
|
92
|
+
scope_id: parts.scope_id,
|
|
93
|
+
missing_permissions: parts.missing_permissions,
|
|
94
|
+
required_permissions: parts.required_permissions ?? parts.missing_permissions,
|
|
95
|
+
user_permissions: parts.user_permissions ?? [],
|
|
96
|
+
permission_descriptions: descriptions,
|
|
97
|
+
user_friendly_message:
|
|
98
|
+
parts.user_friendly_message ?? "You don't have permission to perform this action.",
|
|
99
|
+
action_label: actionLabel,
|
|
100
|
+
origin_url: originUrl,
|
|
101
|
+
api_route: apiRoute,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// section: response_helper
|
|
106
|
+
|
|
107
|
+
/** Builds the 403 Response, using hazo_api.fail() if available, otherwise hand-built. */
|
|
108
|
+
export async function buildForbiddenResponse(payload: PermissionDeniedPayload): Promise<Response> {
|
|
109
|
+
const apiMod = (await import('hazo_api').catch(() => null)) as {
|
|
110
|
+
fail?: (
|
|
111
|
+
code: string,
|
|
112
|
+
message: string,
|
|
113
|
+
opts?: { details?: unknown; status?: number },
|
|
114
|
+
) => Response;
|
|
115
|
+
} | null;
|
|
116
|
+
|
|
117
|
+
if (apiMod?.fail) {
|
|
118
|
+
return apiMod.fail('FORBIDDEN', payload.user_friendly_message, {
|
|
119
|
+
status: 403,
|
|
120
|
+
details: {
|
|
121
|
+
missing_permissions: payload.missing_permissions,
|
|
122
|
+
user_friendly_message: payload.user_friendly_message,
|
|
123
|
+
permission_descriptions: payload.permission_descriptions,
|
|
124
|
+
action_label: payload.action_label,
|
|
125
|
+
origin_url: payload.origin_url,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return new Response(
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
ok: false,
|
|
133
|
+
error: {
|
|
134
|
+
code: 'FORBIDDEN',
|
|
135
|
+
message: payload.user_friendly_message,
|
|
136
|
+
details: {
|
|
137
|
+
missing_permissions: payload.missing_permissions,
|
|
138
|
+
permission_descriptions: payload.permission_descriptions,
|
|
139
|
+
action_label: payload.action_label,
|
|
140
|
+
origin_url: payload.origin_url,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
{ status: 403, headers: { 'Content-Type': 'application/json' } },
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// section: wrapper
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Wraps a Next.js route handler. On PermissionError, returns a structured
|
|
152
|
+
* hazo_api fail() 403 with code FORBIDDEN and fires the registered
|
|
153
|
+
* onPermissionDenied callback best-effort.
|
|
154
|
+
*
|
|
155
|
+
* Usage:
|
|
156
|
+
* export const POST = withPermissionIssueCapture(
|
|
157
|
+
* async (request) => { ... },
|
|
158
|
+
* { required_permissions: ['edit_config'] }
|
|
159
|
+
* );
|
|
160
|
+
*/
|
|
161
|
+
export function withPermissionIssueCapture(
|
|
162
|
+
handler: (request: NextRequest) => Promise<Response>,
|
|
163
|
+
opts: { required_permissions: string[] },
|
|
164
|
+
): (request: NextRequest) => Promise<Response> {
|
|
165
|
+
return async (request: NextRequest): Promise<Response> => {
|
|
166
|
+
try {
|
|
167
|
+
// Run hazo_get_auth with strict=true so PermissionError is thrown on denial
|
|
168
|
+
await hazo_get_auth(request, {
|
|
169
|
+
required_permissions: opts.required_permissions,
|
|
170
|
+
strict: true,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// On success, hand off to the original handler
|
|
174
|
+
return await handler(request);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (err instanceof PermissionError) {
|
|
177
|
+
// Convert PermissionError.permission_descriptions Map → plain object
|
|
178
|
+
const descriptions: Record<string, string> = {};
|
|
179
|
+
if (err.permission_descriptions) {
|
|
180
|
+
err.permission_descriptions.forEach((v, k) => {
|
|
181
|
+
descriptions[k] = v;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Try to extract user_id from session — best-effort, non-strict re-call
|
|
186
|
+
let userId = '';
|
|
187
|
+
try {
|
|
188
|
+
const lax = await hazo_get_auth(request, {
|
|
189
|
+
required_permissions: [],
|
|
190
|
+
strict: false,
|
|
191
|
+
});
|
|
192
|
+
if (lax.authenticated) {
|
|
193
|
+
userId = lax.user.id;
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
/* ignore — user_id stays '' */
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const payload = buildDenialPayloadFromRequest(request, {
|
|
200
|
+
user_id: userId,
|
|
201
|
+
missing_permissions: err.missing_permissions,
|
|
202
|
+
required_permissions: err.required_permissions,
|
|
203
|
+
user_permissions: err.user_permissions,
|
|
204
|
+
permission_descriptions: descriptions,
|
|
205
|
+
user_friendly_message: err.user_friendly_message,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Fire callback best-effort (do NOT await — must not delay the 403 response)
|
|
209
|
+
if (_onPermissionDenied) {
|
|
210
|
+
Promise.resolve(_onPermissionDenied(payload)).catch((cbErr) => {
|
|
211
|
+
log.warn('onPermissionDenied callback failed', { error: cbErr });
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return buildForbiddenResponse(payload);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Re-throw non-permission errors
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// file_description: helper to find roles that have a given permission and are actively used in a scope
|
|
2
|
+
import 'server-only';
|
|
3
|
+
|
|
4
|
+
import { create_app_logger } from '../app_logger.js';
|
|
5
|
+
|
|
6
|
+
export interface RoleWithPermission {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns all roles that (a) have the given permission and (b) are actively
|
|
13
|
+
* used in the given scope (at least one user assigned to that scope via that role).
|
|
14
|
+
* Returns empty array if permission doesn't exist or no matching roles.
|
|
15
|
+
*/
|
|
16
|
+
export async function find_roles_with_permission(
|
|
17
|
+
adapter: any,
|
|
18
|
+
scope_id: string,
|
|
19
|
+
permission_name: string,
|
|
20
|
+
): Promise<RoleWithPermission[]> {
|
|
21
|
+
const logger = create_app_logger();
|
|
22
|
+
try {
|
|
23
|
+
const sql = `
|
|
24
|
+
SELECT DISTINCT r.id, r.name
|
|
25
|
+
FROM hazo_roles r
|
|
26
|
+
JOIN hazo_role_permissions rp ON rp.role_id = r.id
|
|
27
|
+
JOIN hazo_permissions p ON p.id = rp.permission_id
|
|
28
|
+
JOIN hazo_user_scopes us ON us.role_id = r.id AND us.scope_id = $1
|
|
29
|
+
WHERE p.permission_name = $2
|
|
30
|
+
`;
|
|
31
|
+
const rows: unknown = await adapter.raw(sql, [scope_id, permission_name]);
|
|
32
|
+
if (!Array.isArray(rows)) return [];
|
|
33
|
+
return rows.map((row) => {
|
|
34
|
+
const r = row as { id: string; name: string };
|
|
35
|
+
return { id: r.id, name: r.name };
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logger.error('find_roles_with_permission_error', {
|
|
39
|
+
filename: 'find_roles_with_permission.ts',
|
|
40
|
+
line_number: 0,
|
|
41
|
+
error: error instanceof Error ? error.message : String(error),
|
|
42
|
+
scope_id,
|
|
43
|
+
permission_name,
|
|
44
|
+
});
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type PermissionDeniedDetails } from '../components/permission_denied_dialog.client.js';
|
|
3
|
+
export declare function showPermissionDeniedDialog(details: PermissionDeniedDetails, message?: string): void;
|
|
4
|
+
export interface PermissionDeniedProviderProps {
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
registerCardRenderer?: (typeKey: string, renderer: React.ComponentType<any>) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function PermissionDeniedProvider({ children, registerCardRenderer }: PermissionDeniedProviderProps): React.JSX.Element;
|
|
9
|
+
export declare function handlePermissionDenied(response: Response): Promise<boolean>;
|
|
10
|
+
export declare function fetchWithPermissionCapture(input: RequestInfo | URL, init?: RequestInit & {
|
|
11
|
+
originUrl?: string;
|
|
12
|
+
actionLabel?: string;
|
|
13
|
+
}): Promise<Response>;
|
|
14
|
+
//# sourceMappingURL=permission_denied_provider.client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission_denied_provider.client.d.ts","sourceRoot":"","sources":["../../src/admin-issues/permission_denied_provider.client.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA8B,MAAM,OAAO,CAAC;AACnD,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,kDAAkD,CAAC;AAO1D,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,uBAAuB,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAInG;AAID,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC;CACtF;AAED,wBAAgB,wBAAwB,CAAC,EAAE,QAAQ,EAAE,oBAAoB,EAAE,EAAE,6BAA6B,qBAgCzG;AAID,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAmBjF;AAID,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,WAAW,GAAG,GAAG,EACxB,IAAI,CAAC,EAAE,WAAW,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAChE,OAAO,CAAC,QAAQ,CAAC,CAcnB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
// file_description: singleton provider + fetch helper that shows PermissionDeniedDialog on 403 responses
|
|
15
|
+
import { useEffect, useState } from 'react';
|
|
16
|
+
import { PermissionDeniedDialog, } from '../components/permission_denied_dialog.client.js';
|
|
17
|
+
import { AuthIssueCard } from './plugin.client.js';
|
|
18
|
+
// ── singleton trigger ─────────────────────────────────────────────────────────
|
|
19
|
+
let _showDialog = null;
|
|
20
|
+
export function showPermissionDeniedDialog(details, message) {
|
|
21
|
+
if (_showDialog) {
|
|
22
|
+
_showDialog(details, message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function PermissionDeniedProvider({ children, registerCardRenderer }) {
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
const [details, setDetails] = useState(null);
|
|
28
|
+
const [message, setMessage] = useState(undefined);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
_showDialog = (d, m) => {
|
|
31
|
+
setDetails(d);
|
|
32
|
+
setMessage(m);
|
|
33
|
+
setOpen(true);
|
|
34
|
+
};
|
|
35
|
+
if (registerCardRenderer) {
|
|
36
|
+
registerCardRenderer('auth_permission', AuthIssueCard);
|
|
37
|
+
}
|
|
38
|
+
return () => {
|
|
39
|
+
_showDialog = null;
|
|
40
|
+
};
|
|
41
|
+
}, [registerCardRenderer]);
|
|
42
|
+
return (_jsxs(_Fragment, { children: [children, _jsx(PermissionDeniedDialog, { open: open, onOpenChange: setOpen, friendlyMessage: message, technicalDetails: details !== null && details !== void 0 ? details : undefined })] }));
|
|
43
|
+
}
|
|
44
|
+
// ── handlePermissionDenied ────────────────────────────────────────────────────
|
|
45
|
+
export async function handlePermissionDenied(response) {
|
|
46
|
+
var _a, _b;
|
|
47
|
+
try {
|
|
48
|
+
const cloned = response.clone();
|
|
49
|
+
const body = await cloned.json();
|
|
50
|
+
if (((_a = body === null || body === void 0 ? void 0 : body.error) === null || _a === void 0 ? void 0 : _a.code) !== 'FORBIDDEN')
|
|
51
|
+
return false;
|
|
52
|
+
const errDetails = (_b = body.error.details) !== null && _b !== void 0 ? _b : {};
|
|
53
|
+
const permDetails = {
|
|
54
|
+
missing_permissions: errDetails.missing_permissions,
|
|
55
|
+
permission_descriptions: errDetails.permission_descriptions,
|
|
56
|
+
action_label: errDetails.action_label,
|
|
57
|
+
origin_url: errDetails.origin_url,
|
|
58
|
+
};
|
|
59
|
+
showPermissionDeniedDialog(permDetails, body.error.message);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch (_c) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ── fetchWithPermissionCapture ────────────────────────────────────────────────
|
|
67
|
+
export async function fetchWithPermissionCapture(input, init) {
|
|
68
|
+
var _a;
|
|
69
|
+
const _b = init !== null && init !== void 0 ? init : {}, { originUrl, actionLabel } = _b, cleanInit = __rest(_b, ["originUrl", "actionLabel"]);
|
|
70
|
+
const headers = new Headers((_a = cleanInit.headers) !== null && _a !== void 0 ? _a : {});
|
|
71
|
+
if (originUrl)
|
|
72
|
+
headers.set('X-Hazo-Origin-Url', originUrl);
|
|
73
|
+
if (actionLabel)
|
|
74
|
+
headers.set('X-Hazo-Action-Label', actionLabel);
|
|
75
|
+
const response = await fetch(input, Object.assign(Object.assign({}, cleanInit), { headers }));
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
await handlePermissionDenied(response);
|
|
78
|
+
}
|
|
79
|
+
return response;
|
|
80
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface IssueCardData {
|
|
3
|
+
id: string;
|
|
4
|
+
type: string;
|
|
5
|
+
status: string;
|
|
6
|
+
subject_user_id: string;
|
|
7
|
+
assigned_to: string | null;
|
|
8
|
+
title: string;
|
|
9
|
+
summary: string;
|
|
10
|
+
payload: Record<string, unknown>;
|
|
11
|
+
occurrence_count: number;
|
|
12
|
+
first_seen_at: string;
|
|
13
|
+
last_seen_at: string;
|
|
14
|
+
resolution: string | null;
|
|
15
|
+
resolution_reason: string | null;
|
|
16
|
+
resolved_by: string | null;
|
|
17
|
+
resolved_at: string | null;
|
|
18
|
+
}
|
|
19
|
+
interface IssueCardRendererProps {
|
|
20
|
+
issue: IssueCardData;
|
|
21
|
+
basePath: string;
|
|
22
|
+
onAction?: (actionKey: string, params: Record<string, unknown>) => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function AuthIssueCard({ issue, onAction }: IssueCardRendererProps): React.JSX.Element;
|
|
25
|
+
/**
|
|
26
|
+
* Convenience registration function.
|
|
27
|
+
* Consumer app calls this at boot, passing hazo_admin/client's registerIssueCardRenderer.
|
|
28
|
+
* hazo_auth does NOT import hazo_admin (DI principle).
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerAuthIssueCardRenderer(opts: {
|
|
31
|
+
registerRenderer: (typeKey: string, renderer: React.ComponentType<IssueCardRendererProps>) => void;
|
|
32
|
+
}): void;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=plugin.client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.client.d.ts","sourceRoot":"","sources":["../../src/admin-issues/plugin.client.tsx"],"names":[],"mappings":"AACA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,UAAU,aAAa;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,UAAU,sBAAsB;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF;AAED,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,sBAAsB,qBA+IxE;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,KAAK,IAAI,CAAC;CACpG,GAAG,IAAI,CAEP"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
export function AuthIssueCard({ issue, onAction }) {
|
|
5
|
+
const [showGrant, setShowGrant] = useState(false);
|
|
6
|
+
const [showDeny, setShowDeny] = useState(false);
|
|
7
|
+
const [roleId, setRoleId] = useState('');
|
|
8
|
+
const [denyReason, setDenyReason] = useState('');
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
const payload = issue.payload;
|
|
12
|
+
const missingPerms = payload.missing_permissions || [];
|
|
13
|
+
const descriptions = payload.permission_descriptions || {};
|
|
14
|
+
const actionLabel = payload.action_label || '';
|
|
15
|
+
const originUrl = payload.origin_url || '';
|
|
16
|
+
const isActive = issue.status === 'new' || issue.status === 'wip';
|
|
17
|
+
const handleGrant = async () => {
|
|
18
|
+
if (!roleId.trim()) {
|
|
19
|
+
setError('Please enter a role ID.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
setLoading(true);
|
|
23
|
+
setError(null);
|
|
24
|
+
try {
|
|
25
|
+
await (onAction === null || onAction === void 0 ? void 0 : onAction('grant', { role_id: roleId.trim() }));
|
|
26
|
+
setShowGrant(false);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const err = e;
|
|
30
|
+
setError((err === null || err === void 0 ? void 0 : err.message) || 'Grant failed');
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const handleDeny = async () => {
|
|
37
|
+
if (!denyReason.trim()) {
|
|
38
|
+
setError('Please provide a reason.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
setLoading(true);
|
|
42
|
+
setError(null);
|
|
43
|
+
try {
|
|
44
|
+
await (onAction === null || onAction === void 0 ? void 0 : onAction('deny', { reason: denyReason.trim() }));
|
|
45
|
+
setShowDeny(false);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
const err = e;
|
|
49
|
+
setError((err === null || err === void 0 ? void 0 : err.message) || 'Deny failed');
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
setLoading(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return (_jsxs("div", { className: "text-sm space-y-2", children: [_jsx("div", { className: "text-gray-700", children: issue.summary }), missingPerms.length > 0 && (_jsxs("div", { className: "text-xs text-gray-500", children: [_jsx("span", { className: "font-medium", children: "Needs: " }), missingPerms.map((p) => descriptions[p] || p).join(', ')] })), actionLabel && (_jsxs("div", { className: "text-xs text-gray-400", children: [_jsx("span", { className: "font-medium", children: "Tried to: " }), actionLabel] })), originUrl && (_jsxs("div", { className: "text-xs text-gray-400", children: [_jsx("span", { className: "font-medium", children: "From: " }), _jsx("span", { className: "font-mono", children: originUrl })] })), issue.occurrence_count > 1 && (_jsxs("div", { className: "text-xs text-amber-600 font-medium", children: ["Occurred ", issue.occurrence_count, "\u00D7 (first: ", new Date(issue.first_seen_at).toLocaleDateString(), ")"] })), isActive && onAction && (_jsxs("div", { className: "flex gap-2 pt-1", children: [_jsx("button", { onClick: () => { setShowGrant(true); setShowDeny(false); setError(null); }, className: "px-2 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700", children: "Grant" }), _jsx("button", { onClick: () => { setShowDeny(true); setShowGrant(false); setError(null); }, className: "px-2 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700", children: "Deny" })] })), showGrant && (_jsxs("div", { className: "mt-2 p-2 border rounded bg-green-50 space-y-2", children: [_jsx("div", { className: "text-xs font-medium text-green-800", children: "Grant access \u2014 enter the role ID to assign:" }), _jsx("input", { type: "text", value: roleId, onChange: (e) => setRoleId(e.target.value), placeholder: "Role ID (UUID)", className: "w-full text-xs border rounded px-2 py-1" }), error && _jsx("div", { className: "text-xs text-red-600", children: error }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: handleGrant, disabled: loading, className: "px-2 py-1 text-xs bg-green-600 text-white rounded disabled:opacity-50", children: loading ? 'Granting…' : 'Confirm Grant' }), _jsx("button", { onClick: () => setShowGrant(false), className: "px-2 py-1 text-xs border rounded", children: "Cancel" })] })] })), showDeny && (_jsxs("div", { className: "mt-2 p-2 border rounded bg-red-50 space-y-2", children: [_jsx("div", { className: "text-xs font-medium text-red-800", children: "Deny request \u2014 provide a reason:" }), _jsx("textarea", { value: denyReason, onChange: (e) => setDenyReason(e.target.value), placeholder: "Reason for denial", rows: 2, className: "w-full text-xs border rounded px-2 py-1" }), error && _jsx("div", { className: "text-xs text-red-600", children: error }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: handleDeny, disabled: loading, className: "px-2 py-1 text-xs bg-red-600 text-white rounded disabled:opacity-50", children: loading ? 'Denying…' : 'Confirm Deny' }), _jsx("button", { onClick: () => setShowDeny(false), className: "px-2 py-1 text-xs border rounded", children: "Cancel" })] })] })), issue.resolution && (_jsxs("div", { className: `text-xs mt-1 ${issue.resolution === 'granted' ? 'text-green-700' : 'text-red-700'}`, children: [issue.resolution === 'granted' ? '✓ Granted' : '✗ Denied', issue.resolution_reason && `: ${issue.resolution_reason}`] }))] }));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Convenience registration function.
|
|
59
|
+
* Consumer app calls this at boot, passing hazo_admin/client's registerIssueCardRenderer.
|
|
60
|
+
* hazo_auth does NOT import hazo_admin (DI principle).
|
|
61
|
+
*/
|
|
62
|
+
export function registerAuthIssueCardRenderer(opts) {
|
|
63
|
+
opts.registerRenderer('auth_permission', AuthIssueCard);
|
|
64
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
interface LocalIssueRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
scope_id: string;
|
|
5
|
+
type: string;
|
|
6
|
+
subject_user_id: string;
|
|
7
|
+
assigned_to: string | null;
|
|
8
|
+
payload: Record<string, unknown>;
|
|
9
|
+
resolution: string | null;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
interface LocalIssueActionCtx {
|
|
13
|
+
getHazoConnect: () => Promise<any> | any;
|
|
14
|
+
actorUserId: string;
|
|
15
|
+
actorPermissions: string[];
|
|
16
|
+
}
|
|
17
|
+
interface LocalIssueActionResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
message?: string;
|
|
20
|
+
data?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
interface LocalNotifyPayload {
|
|
23
|
+
subject: string;
|
|
24
|
+
body: string;
|
|
25
|
+
deep_link?: string;
|
|
26
|
+
payload?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
interface LocalIssueActionDef {
|
|
29
|
+
key: string;
|
|
30
|
+
label: string;
|
|
31
|
+
requiredPermission: string;
|
|
32
|
+
run(issue: LocalIssueRecord, params: Record<string, unknown>, ctx: LocalIssueActionCtx): Promise<LocalIssueActionResult>;
|
|
33
|
+
}
|
|
34
|
+
interface LocalIssueTypeDef {
|
|
35
|
+
typeKey: string;
|
|
36
|
+
label: string;
|
|
37
|
+
buildDescriptor(payload: Record<string, unknown>): {
|
|
38
|
+
title: string;
|
|
39
|
+
summary: string;
|
|
40
|
+
};
|
|
41
|
+
resolveRecipients(issue: LocalIssueRecord, ctx: LocalIssueActionCtx): Promise<{
|
|
42
|
+
user_ids: string[];
|
|
43
|
+
scope_id: string;
|
|
44
|
+
}>;
|
|
45
|
+
actions: LocalIssueActionDef[];
|
|
46
|
+
buildResolutionNotice(issue: LocalIssueRecord, actionKey: string, result: LocalIssueActionResult): LocalNotifyPayload;
|
|
47
|
+
}
|
|
48
|
+
export declare const authIssueTypeDef: LocalIssueTypeDef;
|
|
49
|
+
/**
|
|
50
|
+
* Convenience registration function.
|
|
51
|
+
* The app calls this at boot, passing hazo_admin's registerIssueType.
|
|
52
|
+
* hazo_auth does NOT import hazo_admin (DI principle).
|
|
53
|
+
*/
|
|
54
|
+
export declare function registerAuthIssuePlugin(opts: {
|
|
55
|
+
registerType: (def: any) => void;
|
|
56
|
+
getHazoConnect?: () => Promise<any> | any;
|
|
57
|
+
}): void;
|
|
58
|
+
export {};
|
|
59
|
+
//# sourceMappingURL=plugin.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.server.d.ts","sourceRoot":"","sources":["../../src/admin-issues/plugin.server.ts"],"names":[],"mappings":"AACA,OAAO,aAAa,CAAC;AASrB,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,UAAU,mBAAmB;IAC3B,cAAc,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,UAAU,sBAAsB;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,UAAU,kBAAkB;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,UAAU,mBAAmB;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,MAAM,CAAC;IAC3B,GAAG,CACD,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,GAAG,EAAE,mBAAmB,GACvB,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACpC;AAED,UAAU,iBAAiB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACtF,iBAAiB,CACf,KAAK,EAAE,gBAAgB,EACvB,GAAG,EAAE,mBAAmB,GACvB,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,qBAAqB,CACnB,KAAK,EAAE,gBAAgB,EACvB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,sBAAsB,GAC7B,kBAAkB,CAAC;CACvB;AAED,eAAO,MAAM,gBAAgB,EAAE,iBAsK9B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,YAAY,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;CAC3C,GAAG,IAAI,CAEP"}
|