From d25b67b9de201927151f4c2843931df840cb5a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=B5=20Ho=C3=A0ng=20Ph=C3=BAc?= Date: Mon, 9 Jun 2025 22:31:39 +0700 Subject: [PATCH 1/8] chore: enhance TokenVerifier component to support development mode and add platform services to user schema --- .../app/[locale]/(auth)/verify-token/page.tsx | 3 +- .../20250609135112_add_platform_services.sql | 74 +++++++ .../app/[locale]/(auth)/verify-token/page.tsx | 3 +- .../app/[locale]/(auth)/verify-token/page.tsx | 3 +- .../app/[locale]/(auth)/verify-token/page.tsx | 3 +- .../app/[locale]/(auth)/verify-token/page.tsx | 3 +- packages/auth/src/cross-app/index.ts | 4 +- .../src/cross-app/token-verifier-core.tsx | 6 +- .../auth/src/cross-app/token-verifier.tsx | 4 +- packages/types/src/supabase.ts | 187 +++++++++--------- 10 files changed, 188 insertions(+), 102 deletions(-) create mode 100644 apps/db/supabase/migrations/20250609135112_add_platform_services.sql diff --git a/apps/calendar/src/app/[locale]/(auth)/verify-token/page.tsx b/apps/calendar/src/app/[locale]/(auth)/verify-token/page.tsx index aa93987996..1b7c86f246 100644 --- a/apps/calendar/src/app/[locale]/(auth)/verify-token/page.tsx +++ b/apps/calendar/src/app/[locale]/(auth)/verify-token/page.tsx @@ -1,5 +1,6 @@ +import { DEV_MODE } from '@/constants/common'; import { TokenVerifier } from '@tuturuuu/auth/cross-app/token-verifier'; export default function VerifyTokenPage() { - return ; + return ; } diff --git a/apps/db/supabase/migrations/20250609135112_add_platform_services.sql b/apps/db/supabase/migrations/20250609135112_add_platform_services.sql new file mode 100644 index 0000000000..528c0f11ec --- /dev/null +++ b/apps/db/supabase/migrations/20250609135112_add_platform_services.sql @@ -0,0 +1,74 @@ +create type "public"."platform_service" as enum ('TUTURUUU', 'REWISE', 'NOVA', 'UPSKII'); + +alter table "public"."users" add column "services" platform_service[]; + +alter table "public"."users" alter column "services" set default '{TUTURUUU}'; + +-- Update existing users to have the default service if they don't have any services +UPDATE public.users +SET services = '{TUTURUUU}'::platform_service[] +WHERE services IS NULL; + +CREATE OR REPLACE FUNCTION public.validate_cross_app_token_with_session( + p_token TEXT, + p_target_app TEXT +) +RETURNS TABLE(user_id UUID, session_data JSONB) AS $$ +DECLARE + v_record RECORD; + v_required_service platform_service; + v_user_services platform_service[]; +BEGIN + -- Find the token and get the user_id, session_data, and origin_app if it's valid + SELECT t.user_id, t.session_data, t.origin_app INTO v_record + FROM public.cross_app_tokens t + WHERE t.token = p_token + AND t.target_app = p_target_app + AND t.expires_at > now() + AND t.used_at IS NULL + AND t.is_revoked = false; + + -- Log the found record for debugging + RAISE NOTICE 'Found token record: user_id=%, session_data=%, origin_app=%', v_record.user_id, v_record.session_data, v_record.origin_app; + + -- If the token is valid, check additional permissions for service access + IF v_record.user_id IS NOT NULL THEN + -- If origin app is web, check that user has the required service for the target app + IF v_record.origin_app = 'web' THEN + -- Map target app to required platform service + CASE p_target_app + WHEN 'platform' THEN v_required_service := 'TUTURUUU'; + WHEN 'rewise' THEN v_required_service := 'REWISE'; + WHEN 'nova' THEN v_required_service := 'NOVA'; + WHEN 'upskii' THEN v_required_service := 'UPSKII'; + ELSE v_required_service := NULL; + END CASE; + + -- Get user's services + SELECT COALESCE(services, '{}'::platform_service[]) INTO v_user_services + FROM public.users + WHERE id = v_record.user_id; + + -- Add the required service if user doesn't have it yet + IF v_required_service IS NOT NULL AND NOT (v_required_service = ANY(v_user_services)) THEN + RAISE NOTICE 'Adding missing service % for user % accessing target app %', v_required_service, v_record.user_id, p_target_app; + -- Add the service to the user's services array + UPDATE public.users + SET services = array_append(services, v_required_service) + WHERE id = v_record.user_id; + END IF; + END IF; + + -- Mark token as used + UPDATE public.cross_app_tokens + SET used_at = now() + WHERE token = p_token; + + -- Return the user_id and session_data + RETURN QUERY SELECT v_record.user_id, v_record.session_data; + ELSE + -- Return NULL if token is invalid + RETURN QUERY SELECT NULL::UUID, NULL::JSONB; + END IF; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; \ No newline at end of file diff --git a/apps/famigo/src/app/[locale]/(auth)/verify-token/page.tsx b/apps/famigo/src/app/[locale]/(auth)/verify-token/page.tsx index aa93987996..1b7c86f246 100644 --- a/apps/famigo/src/app/[locale]/(auth)/verify-token/page.tsx +++ b/apps/famigo/src/app/[locale]/(auth)/verify-token/page.tsx @@ -1,5 +1,6 @@ +import { DEV_MODE } from '@/constants/common'; import { TokenVerifier } from '@tuturuuu/auth/cross-app/token-verifier'; export default function VerifyTokenPage() { - return ; + return ; } diff --git a/apps/nova/src/app/[locale]/(auth)/verify-token/page.tsx b/apps/nova/src/app/[locale]/(auth)/verify-token/page.tsx index aa93987996..1b7c86f246 100644 --- a/apps/nova/src/app/[locale]/(auth)/verify-token/page.tsx +++ b/apps/nova/src/app/[locale]/(auth)/verify-token/page.tsx @@ -1,5 +1,6 @@ +import { DEV_MODE } from '@/constants/common'; import { TokenVerifier } from '@tuturuuu/auth/cross-app/token-verifier'; export default function VerifyTokenPage() { - return ; + return ; } diff --git a/apps/rewise/src/app/[locale]/(auth)/verify-token/page.tsx b/apps/rewise/src/app/[locale]/(auth)/verify-token/page.tsx index aa93987996..1b7c86f246 100644 --- a/apps/rewise/src/app/[locale]/(auth)/verify-token/page.tsx +++ b/apps/rewise/src/app/[locale]/(auth)/verify-token/page.tsx @@ -1,5 +1,6 @@ +import { DEV_MODE } from '@/constants/common'; import { TokenVerifier } from '@tuturuuu/auth/cross-app/token-verifier'; export default function VerifyTokenPage() { - return ; + return ; } diff --git a/apps/upskii/src/app/[locale]/(auth)/verify-token/page.tsx b/apps/upskii/src/app/[locale]/(auth)/verify-token/page.tsx index aa93987996..1b7c86f246 100644 --- a/apps/upskii/src/app/[locale]/(auth)/verify-token/page.tsx +++ b/apps/upskii/src/app/[locale]/(auth)/verify-token/page.tsx @@ -1,5 +1,6 @@ +import { DEV_MODE } from '@/constants/common'; import { TokenVerifier } from '@tuturuuu/auth/cross-app/token-verifier'; export default function VerifyTokenPage() { - return ; + return ; } diff --git a/packages/auth/src/cross-app/index.ts b/packages/auth/src/cross-app/index.ts index 8bb8945c53..40f1e2fdc8 100644 --- a/packages/auth/src/cross-app/index.ts +++ b/packages/auth/src/cross-app/index.ts @@ -207,10 +207,12 @@ export const verifyRouteToken = async ({ searchParams, token, router, + devMode, }: { searchParams: URLSearchParams; token: string | null; router: any; + devMode: boolean; }) => { const supabase = createClient(); @@ -230,7 +232,7 @@ export const verifyRouteToken = async ({ } else { let userId = user?.id; - if (!userId) { + if (devMode || !userId) { const res = await fetch('/api/auth/verify-app-token', { method: 'POST', headers: { diff --git a/packages/auth/src/cross-app/token-verifier-core.tsx b/packages/auth/src/cross-app/token-verifier-core.tsx index ea38c133b6..c5c39151cf 100644 --- a/packages/auth/src/cross-app/token-verifier-core.tsx +++ b/packages/auth/src/cross-app/token-verifier-core.tsx @@ -5,14 +5,14 @@ import { LoadingIndicator } from '@tuturuuu/ui/custom/loading-indicator'; import { useRouter, useSearchParams } from 'next/navigation'; import { useEffect } from 'react'; -export function TokenVerifierCore() { +export function TokenVerifierCore({ devMode }: { devMode: boolean }) { const searchParams = useSearchParams(); const token = searchParams.get('token'); const router = useRouter(); useEffect(() => { - verifyRouteToken({ searchParams, token, router }); - }, [token, router, searchParams]); + verifyRouteToken({ searchParams, token, router, devMode }); + }, [token, router, searchParams, devMode]); return (
diff --git a/packages/auth/src/cross-app/token-verifier.tsx b/packages/auth/src/cross-app/token-verifier.tsx index 590177ee60..c08cc3fec8 100644 --- a/packages/auth/src/cross-app/token-verifier.tsx +++ b/packages/auth/src/cross-app/token-verifier.tsx @@ -3,7 +3,7 @@ import { LoadingIndicator } from '@tuturuuu/ui/custom/loading-indicator'; import Image from 'next/image'; import { Suspense } from 'react'; -export function TokenVerifier() { +export function TokenVerifier({ devMode }: { devMode: boolean }) { return (
@@ -23,7 +23,7 @@ export function TokenVerifier() {
} > - +
); diff --git a/packages/types/src/supabase.ts b/packages/types/src/supabase.ts index cd3fa404ba..efe697a426 100644 --- a/packages/types/src/supabase.ts +++ b/packages/types/src/supabase.ts @@ -4510,6 +4510,7 @@ export type Database = { display_name: string | null; handle: string | null; id: string; + services: Database['public']['Enums']['platform_service'][] | null; }; Insert: { avatar_url?: string | null; @@ -4519,6 +4520,7 @@ export type Database = { display_name?: string | null; handle?: string | null; id?: string; + services?: Database['public']['Enums']['platform_service'][] | null; }; Update: { avatar_url?: string | null; @@ -4528,6 +4530,7 @@ export type Database = { display_name?: string | null; handle?: string | null; id?: string; + services?: Database['public']['Enums']['platform_service'][] | null; }; Relationships: [ { @@ -7191,7 +7194,7 @@ export type Database = { }; Functions: { calculate_productivity_score: { - Args: { duration_seconds: number; category_color: string }; + Args: { category_color: string; duration_seconds: number }; Returns: number; }; cleanup_expired_cross_app_tokens: { @@ -7204,12 +7207,12 @@ export type Database = { }; count_search_users: { Args: - | { search_query: string } | { - search_query: string; role_filter?: string; + search_query: string; enabled_filter?: boolean; - }; + } + | { search_query: string }; Returns: number; }; create_ai_chat: { @@ -7219,11 +7222,11 @@ export type Database = { generate_cross_app_token: { Args: | { - p_origin_app: string; p_target_app: string; - p_expiry_seconds?: number; - p_session_data?: Json; p_user_id: string; + p_session_data?: Json; + p_expiry_seconds?: number; + p_origin_app: string; } | { p_user_id: string; @@ -7234,17 +7237,17 @@ export type Database = { Returns: string; }; get_challenge_stats: { - Args: { challenge_id_param: string; user_id_param: string }; + Args: { user_id_param: string; challenge_id_param: string }; Returns: { - total_score: number; problems_attempted: number; + total_score: number; }[]; }; get_daily_income_expense: { Args: { _ws_id: string; past_days?: number }; Returns: { - total_expense: number; total_income: number; + total_expense: number; day: string; }[]; }; @@ -7252,8 +7255,8 @@ export type Database = { Args: { past_days?: number }; Returns: { total_completion_tokens: number; - day: string; total_prompt_tokens: number; + day: string; }[]; }; get_finance_invoices_count: { @@ -7279,9 +7282,9 @@ export type Database = { get_hourly_prompt_completion_tokens: { Args: { past_hours?: number }; Returns: { + hour: string; total_completion_tokens: number; total_prompt_tokens: number; - hour: string; }[]; }; get_inventory_batches_count: { @@ -7294,22 +7297,22 @@ export type Database = { }; get_inventory_products: { Args: { - _category_ids?: string[]; - _ws_id?: string; - _warehouse_ids?: string[]; _has_unit?: boolean; + _warehouse_ids?: string[]; + _ws_id?: string; + _category_ids?: string[]; }; Returns: { - amount: number; - id: string; - name: string; - manufacturer: string; unit: string; - unit_id: string; - category: string; - price: number; - ws_id: string; created_at: string; + ws_id: string; + amount: number; + price: number; + category: string; + unit_id: string; + manufacturer: string; + name: string; + id: string; }[]; }; get_inventory_products_count: { @@ -7339,9 +7342,9 @@ export type Database = { get_monthly_prompt_completion_tokens: { Args: { past_months?: number }; Returns: { + month: string; total_prompt_tokens: number; total_completion_tokens: number; - month: string; }[]; }; get_pending_event_participants: { @@ -7351,17 +7354,17 @@ export type Database = { get_possible_excluded_groups: { Args: { _ws_id: string; included_groups: string[] }; Returns: { - name: string; - ws_id: string; amount: number; id: string; + name: string; + ws_id: string; }[]; }; get_possible_excluded_tags: { - Args: { included_tags: string[]; _ws_id: string }; + Args: { _ws_id: string; included_tags: string[] }; Returns: { - id: string; name: string; + id: string; ws_id: string; amount: number; }[]; @@ -7369,21 +7372,20 @@ export type Database = { get_session_statistics: { Args: Record; Returns: { + total_count: number; latest_session_date: string; + completed_count: number; unique_users_count: number; active_count: number; - completed_count: number; - total_count: number; }[]; }; get_session_templates: { Args: { + limit_count?: number; workspace_id: string; user_id_param: string; - limit_count?: number; }; Returns: { - title: string; description: string; category_id: string; task_id: string; @@ -7394,43 +7396,44 @@ export type Database = { usage_count: number; avg_duration: number; last_used: string; + title: string; }[]; }; get_submission_statistics: { Args: Record; Returns: { - unique_users_count: number; total_count: number; latest_submission_date: string; + unique_users_count: number; }[]; }; get_transaction_categories_with_amount: { Args: Record; Returns: { ws_id: string; + created_at: string; id: string; + amount: number; name: string; is_expense: boolean; - created_at: string; - amount: number; }[]; }; get_user_role: { - Args: { ws_id: string; user_id: string }; + Args: { user_id: string; ws_id: string }; Returns: string; }; get_user_tasks: { Args: { _board_id: string }; Returns: { - priority: number; - id: string; + description: string; name: string; + id: string; + priority: number; board_id: string; - list_id: string; - end_date: string; completed: boolean; start_date: string; - description: string; + list_id: string; + end_date: string; }[]; }; get_workspace_drive_size: { @@ -7446,24 +7449,24 @@ export type Database = { Returns: number; }; get_workspace_transactions_count: { - Args: { ws_id: string; end_date?: string; start_date?: string }; + Args: { start_date?: string; end_date?: string; ws_id: string }; Returns: number; }; get_workspace_user_groups: { Args: { - _ws_id: string; - included_tags: string[]; excluded_tags: string[]; + included_tags: string[]; + _ws_id: string; search_query: string; }; Returns: { - created_at: string; id: string; - name: string; - notes: string; - ws_id: string; tags: string[]; + created_at: string; tag_count: number; + ws_id: string; + notes: string; + name: string; }[]; }; get_workspace_user_groups_count: { @@ -7473,31 +7476,31 @@ export type Database = { get_workspace_users: { Args: { search_query: string; - _ws_id: string; - included_groups: string[]; excluded_groups: string[]; + included_groups: string[]; + _ws_id: string; }; Returns: { - id: string; - avatar_url: string; full_name: string; - display_name: string; - phone: string; - gender: string; - birthday: string; - ethnicity: string; - guardian: string; - address: string; - national_id: string; - note: string; - balance: number; - ws_id: string; - groups: string[]; - group_count: number; - linked_users: Json; - created_at: string; updated_at: string; + created_at: string; + linked_users: Json; + group_count: number; + groups: string[]; + ws_id: string; + balance: number; + note: string; + national_id: string; + address: string; + guardian: string; + ethnicity: string; + birthday: string; + gender: string; + phone: string; email: string; + display_name: string; + avatar_url: string; + id: string; }[]; }; get_workspace_users_count: { @@ -7509,11 +7512,11 @@ export type Database = { Returns: number; }; get_workspace_wallets_expense: { - Args: { ws_id: string; start_date?: string; end_date?: string }; + Args: { ws_id: string; end_date?: string; start_date?: string }; Returns: number; }; get_workspace_wallets_income: { - Args: { ws_id: string; end_date?: string; start_date?: string }; + Args: { end_date?: string; start_date?: string; ws_id: string }; Returns: number; }; has_other_owner: { @@ -7521,7 +7524,7 @@ export type Database = { Returns: boolean; }; insert_ai_chat_message: { - Args: { chat_id: string; message: string; source: string }; + Args: { chat_id: string; source: string; message: string }; Returns: undefined; }; is_list_accessible: { @@ -7541,15 +7544,15 @@ export type Database = { Returns: boolean; }; is_nova_user_email_in_team: { - Args: { _team_id: string; _user_email: string }; + Args: { _user_email: string; _team_id: string }; Returns: boolean; }; is_nova_user_id_in_team: { - Args: { _user_id: string; _team_id: string }; + Args: { _team_id: string; _user_id: string }; Returns: boolean; }; is_org_member: { - Args: { _user_id: string; _org_id: string }; + Args: { _org_id: string; _user_id: string }; Returns: boolean; }; is_project_member: { @@ -7565,7 +7568,7 @@ export type Database = { Returns: boolean; }; is_user_task_in_board: { - Args: { _task_id: string; _user_id: string }; + Args: { _user_id: string; _task_id: string }; Returns: boolean; }; nova_get_all_challenges_with_user_stats: { @@ -7573,7 +7576,7 @@ export type Database = { Returns: Json; }; nova_get_challenge_with_user_stats: { - Args: { user_id: string; challenge_id: string }; + Args: { challenge_id: string; user_id: string }; Returns: Json; }; nova_get_user_daily_sessions: { @@ -7581,7 +7584,7 @@ export type Database = { Returns: number; }; nova_get_user_total_sessions: { - Args: { challenge_id: string; user_id: string }; + Args: { user_id: string; challenge_id: string }; Returns: number; }; revoke_all_cross_app_tokens: { @@ -7590,7 +7593,7 @@ export type Database = { }; search_users: { Args: - | { page_size: number; page_number: number; search_query: string } + | { page_size: number; search_query: string; page_number: number } | { search_query: string; page_number: number; @@ -7599,6 +7602,14 @@ export type Database = { enabled_filter?: boolean; }; Returns: { + allow_challenge_management: boolean; + allow_manage_all_challenges: boolean; + allow_role_management: boolean; + email: string; + new_email: string; + birthday: string; + team_name: string[]; + id: string; display_name: string; deleted: boolean; avatar_url: string; @@ -7607,28 +7618,20 @@ export type Database = { created_at: string; user_id: string; enabled: boolean; - allow_challenge_management: boolean; - allow_manage_all_challenges: boolean; - team_name: string[]; - id: string; - allow_role_management: boolean; - email: string; - new_email: string; - birthday: string; }[]; }; search_users_by_name: { Args: { - min_similarity?: number; search_query: string; + min_similarity?: number; result_limit?: number; }; Returns: { id: string; handle: string; + display_name: string; avatar_url: string; relevance: number; - display_name: string; }[]; }; transactions_have_same_abs_amount: { @@ -7636,7 +7639,7 @@ export type Database = { Returns: boolean; }; transactions_have_same_amount: { - Args: { transaction_id_1: string; transaction_id_2: string }; + Args: { transaction_id_2: string; transaction_id_1: string }; Returns: boolean; }; update_expired_sessions: { @@ -7648,14 +7651,14 @@ export type Database = { Returns: undefined; }; validate_cross_app_token: { - Args: { p_token: string; p_target_app: string }; + Args: { p_target_app: string; p_token: string }; Returns: string; }; validate_cross_app_token_with_session: { - Args: { p_token: string; p_target_app: string }; + Args: { p_target_app: string; p_token: string }; Returns: { - user_id: string; session_data: Json; + user_id: string; }[]; }; }; @@ -7671,6 +7674,7 @@ export type Database = { calendar_hour_type: 'WORK' | 'PERSONAL' | 'MEETING'; chat_role: 'FUNCTION' | 'USER' | 'SYSTEM' | 'ASSISTANT'; dataset_type: 'excel' | 'csv' | 'html'; + platform_service: 'TUTURUUU' | 'REWISE' | 'NOVA' | 'UPSKII'; task_board_status: 'not_started' | 'active' | 'done' | 'closed'; workspace_role_permission: | 'view_infrastructure' @@ -7823,6 +7827,7 @@ export const Constants = { calendar_hour_type: ['WORK', 'PERSONAL', 'MEETING'], chat_role: ['FUNCTION', 'USER', 'SYSTEM', 'ASSISTANT'], dataset_type: ['excel', 'csv', 'html'], + platform_service: ['TUTURUUU', 'REWISE', 'NOVA', 'UPSKII'], task_board_status: ['not_started', 'active', 'done', 'closed'], workspace_role_permission: [ 'view_infrastructure', From 3961c95f6a37038c9053b1956406b75d039d6c64 Mon Sep 17 00:00:00 2001 From: VNOsST Date: Fri, 13 Jun 2025 11:49:25 +0700 Subject: [PATCH 2/8] db: sb:typegen --- packages/types/src/supabase.ts | 187 ++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 87 deletions(-) diff --git a/packages/types/src/supabase.ts b/packages/types/src/supabase.ts index c2268fd10e..3104b3bc34 100644 --- a/packages/types/src/supabase.ts +++ b/packages/types/src/supabase.ts @@ -7323,7 +7323,7 @@ export type Database = { }; Functions: { calculate_productivity_score: { - Args: { category_color: string; duration_seconds: number }; + Args: { duration_seconds: number; category_color: string }; Returns: number; }; cleanup_expired_cross_app_tokens: { @@ -7336,56 +7336,56 @@ export type Database = { }; count_search_users: { Args: + | { search_query: string } | { - role_filter?: string; search_query: string; + role_filter?: string; enabled_filter?: boolean; - } - | { search_query: string }; + }; Returns: number; }; create_ai_chat: { - Args: { model: string; message: string; title: string }; + Args: { title: string; message: string; model: string }; Returns: string; }; generate_cross_app_token: { Args: | { - p_expiry_seconds?: number; + p_user_id: string; p_origin_app: string; - p_session_data?: Json; p_target_app: string; - p_user_id: string; + p_expiry_seconds?: number; } | { p_user_id: string; - p_expiry_seconds?: number; p_origin_app: string; p_target_app: string; + p_expiry_seconds?: number; + p_session_data?: Json; }; Returns: string; }; get_challenge_stats: { - Args: { user_id_param: string; challenge_id_param: string }; + Args: { challenge_id_param: string; user_id_param: string }; Returns: { - problems_attempted: number; total_score: number; + problems_attempted: number; }[]; }; get_daily_income_expense: { - Args: { past_days?: number; _ws_id: string }; + Args: { _ws_id: string; past_days?: number }; Returns: { + day: string; total_income: number; total_expense: number; - day: string; }[]; }; get_daily_prompt_completion_tokens: { Args: { past_days?: number }; Returns: { - total_completion_tokens: number; - total_prompt_tokens: number; day: string; + total_prompt_tokens: number; + total_completion_tokens: number; }[]; }; get_finance_invoices_count: { @@ -7412,8 +7412,8 @@ export type Database = { Args: { past_hours?: number }; Returns: { hour: string; - total_completion_tokens: number; total_prompt_tokens: number; + total_completion_tokens: number; }[]; }; get_inventory_batches_count: { @@ -7426,22 +7426,22 @@ export type Database = { }; get_inventory_products: { Args: { - _has_unit?: boolean; - _warehouse_ids?: string[]; - _ws_id?: string; _category_ids?: string[]; + _ws_id?: string; + _warehouse_ids?: string[]; + _has_unit?: boolean; }; Returns: { - name: string; id: string; - created_at: string; - ws_id: string; - amount: number; - price: number; - category: string; - unit_id: string; - unit: string; + name: string; manufacturer: string; + unit: string; + unit_id: string; + category: string; + price: number; + amount: number; + ws_id: string; + created_at: string; }[]; }; get_inventory_products_count: { @@ -7463,17 +7463,17 @@ export type Database = { get_monthly_income_expense: { Args: { _ws_id: string; past_months?: number }; Returns: { - total_expense: number; month: string; total_income: number; + total_expense: number; }[]; }; get_monthly_prompt_completion_tokens: { Args: { past_months?: number }; Returns: { month: string; - total_completion_tokens: number; total_prompt_tokens: number; + total_completion_tokens: number; }[]; }; get_pending_event_participants: { @@ -7481,12 +7481,12 @@ export type Database = { Returns: number; }; get_possible_excluded_groups: { - Args: { included_groups: string[]; _ws_id: string }; + Args: { _ws_id: string; included_groups: string[] }; Returns: { id: string; name: string; - amount: number; ws_id: string; + amount: number; }[]; }; get_possible_excluded_tags: { @@ -7494,52 +7494,57 @@ export type Database = { Returns: { id: string; name: string; - amount: number; ws_id: string; + amount: number; }[]; }; get_session_statistics: { Args: Record; Returns: { + total_count: number; + unique_users_count: number; active_count: number; completed_count: number; latest_session_date: string; - total_count: number; - unique_users_count: number; }[]; }; get_session_templates: { Args: { - limit_count?: number; workspace_id: string; user_id_param: string; + limit_count?: number; }; Returns: { - avg_duration: number; - category_id: string; - description: string; - last_used: string; title: string; + description: string; + category_id: string; + task_id: string; tags: string[]; + category_name: string; + category_color: string; + task_name: string; + usage_count: number; + avg_duration: number; + last_used: string; }[]; }; get_submission_statistics: { Args: Record; Returns: { - latest_submission_date: string; total_count: number; + latest_submission_date: string; unique_users_count: number; }[]; }; get_transaction_categories_with_amount: { Args: Record; Returns: { - amount: number; id: string; - is_expense: boolean; name: string; + is_expense: boolean; ws_id: string; created_at: string; + amount: number; }[]; }; get_user_role: { @@ -7557,24 +7562,26 @@ export type Database = { get_user_sessions: { Args: { user_id: string }; Returns: { - ip: string; - user_agent: string; - updated_at: string; - created_at: string; session_id: string; + created_at: string; + updated_at: string; + user_agent: string; + ip: string; is_current: boolean; }[]; }; get_user_tasks: { Args: { _board_id: string }; Returns: { - board_id: string; - completed: boolean; + id: string; + name: string; description: string; + priority: number; + completed: boolean; + start_date: string; end_date: string; list_id: string; - name: string; - start_date: string; + board_id: string; }[]; }; get_workspace_drive_size: { @@ -7590,25 +7597,24 @@ export type Database = { Returns: number; }; get_workspace_transactions_count: { - Args: { end_date?: string; start_date?: string; ws_id: string }; + Args: { ws_id: string; start_date?: string; end_date?: string }; Returns: number; }; get_workspace_user_groups: { Args: { _ws_id: string; - excluded_tags: string[]; included_tags: string[]; + excluded_tags: string[]; search_query: string; }; Returns: { id: string; name: string; - amount: number; + notes: string; ws_id: string; tags: string[]; tag_count: number; created_at: string; - notes: string; }[]; }; get_workspace_user_groups_count: { @@ -7618,24 +7624,25 @@ export type Database = { get_workspace_users: { Args: { _ws_id: string; - excluded_groups: string[]; included_groups: string[]; - search_query?: string; + excluded_groups: string[]; + search_query: string; }; Returns: { + id: string; avatar_url: string; - display_name: string; full_name: string; - id: string; + display_name: string; + email: string; phone: string; - birthday: string; gender: string; - guardian: string; + birthday: string; ethnicity: string; + guardian: string; address: string; national_id: string; - balance: number; note: string; + balance: number; ws_id: string; groups: string[]; group_count: number; @@ -7653,15 +7660,15 @@ export type Database = { Returns: number; }; get_workspace_wallets_expense: { - Args: { end_date?: string; start_date?: string; ws_id: string }; + Args: { ws_id: string; start_date?: string; end_date?: string }; Returns: number; }; get_workspace_wallets_income: { - Args: { end_date?: string; start_date?: string; ws_id: string }; + Args: { ws_id: string; start_date?: string; end_date?: string }; Returns: number; }; has_other_owner: { - Args: { _user_id: string; _ws_id: string }; + Args: { _ws_id: string; _user_id: string }; Returns: boolean; }; insert_ai_chat_message: { @@ -7689,11 +7696,11 @@ export type Database = { Returns: boolean; }; is_nova_user_id_in_team: { - Args: { _team_id: string; _user_id: string }; + Args: { _user_id: string; _team_id: string }; Returns: boolean; }; is_org_member: { - Args: { _org_id: string; _user_id: string }; + Args: { _user_id: string; _org_id: string }; Returns: boolean; }; is_project_member: { @@ -7705,7 +7712,7 @@ export type Database = { Returns: boolean; }; is_task_board_member: { - Args: { _board_id: string; _user_id: string }; + Args: { _user_id: string; _board_id: string }; Returns: boolean; }; is_user_task_in_board: { @@ -7725,7 +7732,7 @@ export type Database = { Returns: number; }; nova_get_user_total_sessions: { - Args: { user_id: string; challenge_id: string }; + Args: { challenge_id: string; user_id: string }; Returns: number; }; revoke_all_cross_app_tokens: { @@ -7742,31 +7749,31 @@ export type Database = { }; search_users: { Args: + | { search_query: string; page_number: number; page_size: number } | { - role_filter?: string; - enabled_filter?: boolean; search_query: string; page_number: number; page_size: number; - } - | { search_query: string; page_number: number; page_size: number }; + role_filter?: string; + enabled_filter?: boolean; + }; Returns: { - handle: string; id: string; - allow_challenge_management: boolean; - allow_manage_all_challenges: boolean; - allow_role_management: boolean; - email: string; - new_email: string; - birthday: string; - team_name: string[]; display_name: string; deleted: boolean; avatar_url: string; + handle: string; bio: string; created_at: string; user_id: string; enabled: boolean; + allow_challenge_management: boolean; + allow_manage_all_challenges: boolean; + allow_role_management: boolean; + email: string; + new_email: string; + birthday: string; + team_name: string[]; }[]; }; search_users_by_name: { @@ -7776,13 +7783,19 @@ export type Database = { min_similarity?: number; }; Returns: { - avatar_url: string; - display_name: string; - handle: string; id: string; + handle: string; + display_name: string; + avatar_url: string; relevance: number; }[]; }; + sum_quiz_scores: { + Args: { p_set_id: string }; + Returns: { + sum: number; + }[]; + }; transactions_have_same_abs_amount: { Args: { transaction_id_1: string; transaction_id_2: string }; Returns: boolean; @@ -7796,18 +7809,18 @@ export type Database = { Returns: undefined; }; update_session_total_score: { - Args: { user_id_param: string; challenge_id_param: string }; + Args: { challenge_id_param: string; user_id_param: string }; Returns: undefined; }; validate_cross_app_token: { - Args: { p_target_app: string; p_token: string }; + Args: { p_token: string; p_target_app: string }; Returns: string; }; validate_cross_app_token_with_session: { - Args: { p_target_app: string; p_token: string }; + Args: { p_token: string; p_target_app: string }; Returns: { - session_data: Json; user_id: string; + session_data: Json; }[]; }; }; From fd8caeab208d60385958dd02ae3a38ed43e4c09b Mon Sep 17 00:00:00 2001 From: VNOsST Date: Fri, 13 Jun 2025 13:10:42 +0700 Subject: [PATCH 3/8] Fix build errors Filter for users who have the services attribute of the respective service --- .../(admin)/(role-management)/users/page.tsx | 20 +++++++++++++++--- .../(admin)/(role-management)/users/page.tsx | 21 +++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/apps/nova/src/app/[locale]/(dashboard)/(admin)/(role-management)/users/page.tsx b/apps/nova/src/app/[locale]/(dashboard)/(admin)/(role-management)/users/page.tsx index 2060d077e4..7bde184af0 100644 --- a/apps/nova/src/app/[locale]/(dashboard)/(admin)/(role-management)/users/page.tsx +++ b/apps/nova/src/app/[locale]/(dashboard)/(admin)/(role-management)/users/page.tsx @@ -136,13 +136,25 @@ async function getUserData({ if (countError) { console.error('Error getting count:', countError); return { - userData: data || [], - userCount: data?.length || 0, + userData: (data || []) + .map((user: any) => ({ + ...user, + services: user.services || [], + })) + .filter((user: any) => user.services?.includes('NOVA')), + userCount: (data || []).filter((user: any) => + user.services?.includes('NOVA') + ).length, }; } return { - userData: data || [], + userData: (data || []) + .map((user: any) => ({ + ...user, + services: user.services || [], + })) + .filter((user: any) => user.services?.includes('NOVA')), userCount: countData || 0, }; } @@ -156,6 +168,7 @@ async function getUserData({ count: 'exact', } ) + .contains('users.services', ['NOVA']) .order('created_at', { ascending: false }) .order('user_id'); @@ -204,6 +217,7 @@ async function getUserData({ userData: data.map(({ nova_team_members, ...user }) => ({ ...user, + services: user.services || [], team_name: nova_team_members ?.map((member) => member.team_name) diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(admin)/(role-management)/users/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(admin)/(role-management)/users/page.tsx index 53f3c83dd7..9d259c919f 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(admin)/(role-management)/users/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(admin)/(role-management)/users/page.tsx @@ -139,11 +139,26 @@ async function getUserData({ if (countError) { console.error('Error getting count:', countError); - return { userData: data || [], userCount: data?.length || 0 }; + return { + userData: (data || []) + .map((user: any) => ({ + ...user, + services: user.services || [], + })) + .filter((user: any) => user.services?.includes('UPSKII')), + userCount: (data || []).filter((user: any) => + user.services?.includes('UPSKII') + ).length, + }; } return { - userData: data || [], + userData: (data || []) + .map((user: any) => ({ + ...user, + services: user.services || [], + })) + .filter((user: any) => user.services?.includes('UPSKII')), userCount: countData || 0, }; } @@ -157,6 +172,7 @@ async function getUserData({ count: 'exact', } ) + .contains('users.services', ['UPSKII']) .order('created_at', { ascending: false }) .order('user_id'); @@ -205,6 +221,7 @@ async function getUserData({ userData: data.map(({ nova_team_members, ...user }) => ({ ...user, + services: user.services || [], team_name: nova_team_members ?.map((member) => member.team_name) From d596c16b3ecd65bb830bdfdbf01453f0706ed5c6 Mon Sep 17 00:00:00 2001 From: VNOsST Date: Fri, 13 Jun 2025 20:37:46 +0700 Subject: [PATCH 4/8] Approval and Requests Approval Module Request Education banner --- .../approvals/columns.tsx | 129 +++++++++++++ .../(workspace-settings)/approvals/page.tsx | 177 +++++++++++++++++ .../approvals/row-actions.tsx | 182 ++++++++++++++++++ .../approvals/status-filter.tsx | 48 +++++ .../[wsId]/(workspace-settings)/layout.tsx | 5 + .../[locale]/(dashboard)/[wsId]/layout.tsx | 17 ++ .../src/components/request-access-button.tsx | 170 ++++++++++++++++ .../components/request-education-banner.tsx | 56 ++++++ 8 files changed, 784 insertions(+) create mode 100644 apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/columns.tsx create mode 100644 apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/page.tsx create mode 100644 apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/row-actions.tsx create mode 100644 apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/status-filter.tsx create mode 100644 apps/upskii/src/components/request-access-button.tsx create mode 100644 apps/upskii/src/components/request-education-banner.tsx diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/columns.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/columns.tsx new file mode 100644 index 0000000000..86c80d0f07 --- /dev/null +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/columns.tsx @@ -0,0 +1,129 @@ +'use client'; + +import { WorkspaceApprovalRequest } from './page'; +import { ApprovalRowActions } from './row-actions'; +import { ColumnDef } from '@tanstack/react-table'; +import { Badge } from '@tuturuuu/ui/badge'; +import { DataTableColumnHeader } from '@tuturuuu/ui/custom/tables/data-table-column-header'; +import { cn } from '@tuturuuu/utils/format'; +import moment from 'moment'; + +export const approvalsColumns = ( + t: any, + namespace: string | undefined +): ColumnDef[] => [ + { + accessorKey: 'id', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('id')} +
+ ), + }, + { + accessorKey: 'workspace_name', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('workspace_name')} +
+ ), + }, + { + accessorKey: 'creator_name', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const creatorName = row.getValue('creator_name') as string; + + return ( +
+
+ {creatorName} +
+
+ ); + }, + }, + { + accessorKey: 'feature_requested', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('feature_requested')} +
+ ), + }, + { + accessorKey: 'request_message', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('request_message')} +
+ ), + }, + { + accessorKey: 'status', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const status = row.getValue('status') as string; + + return ( + + {status} + + ); + }, + }, + { + accessorKey: 'created_at', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.getValue('created_at') + ? moment(row.getValue('created_at')).format('DD/MM/YYYY, HH:mm') + : '-'} +
+ ), + }, + { + id: 'actions', + header: ({ column }) => , + cell: ({ row }) => , + }, +]; diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/page.tsx new file mode 100644 index 0000000000..3957846ce5 --- /dev/null +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/page.tsx @@ -0,0 +1,177 @@ +import { approvalsColumns } from './columns'; +import { StatusFilter } from './status-filter'; +import { CustomDataTable } from '@/components/custom-data-table'; +import { getPermissions } from '@/lib/workspace-helper'; +import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; +import { Separator } from '@tuturuuu/ui/separator'; +import { getTranslations } from 'next-intl/server'; +import { redirect } from 'next/navigation'; + +interface Props { + params: Promise<{ + wsId: string; + }>; + searchParams: Promise<{ + q?: string; + page?: string; + pageSize?: string; + status?: string; + }>; +} + +export default async function ApprovalsPage({ params, searchParams }: Props) { + const { wsId } = await params; + const { status } = await searchParams; + const { withoutPermission } = await getPermissions({ + wsId, + }); + + // Only allow root workspace access to approvals + if (withoutPermission('view_infrastructure')) redirect(`/${wsId}/settings`); + + const approvals = await getApprovalRequests(await searchParams); + const t = await getTranslations(); + + return ( + <> + + + +
+
+
+ +
+
+ + +
+ + ); +} + +// Dummy function to simulate fetching approval requests +// This will be replaced with actual API calls later +const getApprovalRequests = async ({ + q, + page = '1', + pageSize = '10', + status, +}: { + q?: string; + page?: string; + pageSize?: string; + status?: string; +}): Promise<{ + data: WorkspaceApprovalRequest[]; + count: number; +}> => { + // Dummy data - replace with actual database query + const dummyData: WorkspaceApprovalRequest[] = [ + { + id: '1', + workspace_id: 'ws-001', + workspace_name: 'Marketing Team Workspace', + creator_id: 'user-001', + creator_name: 'John Smith', + feature_requested: 'Advanced Analytics', + request_message: + 'We need advanced analytics to track our marketing campaigns more effectively.', + status: 'pending', + created_at: '2024-01-15T10:30:00Z', + updated_at: '2024-01-15T10:30:00Z', + }, + { + id: '2', + workspace_id: 'ws-002', + workspace_name: 'Product Development', + creator_id: 'user-002', + creator_name: 'Sarah Johnson', + + feature_requested: 'API Access', + request_message: + 'We require API access to integrate with our existing development tools.', + status: 'pending', + created_at: '2024-01-14T14:22:00Z', + updated_at: '2024-01-14T14:22:00Z', + }, + { + id: '3', + workspace_id: 'ws-003', + workspace_name: 'Customer Support Hub', + creator_id: 'user-003', + creator_name: 'Michael Chen', + + feature_requested: 'Premium Support', + request_message: + 'Our team handles critical customer issues and needs priority support.', + status: 'approved', + created_at: '2024-01-13T09:15:00Z', + updated_at: '2024-01-13T16:45:00Z', + }, + { + id: '4', + workspace_id: 'ws-004', + workspace_name: 'Sales Operations', + creator_id: 'user-004', + creator_name: 'Emily Davis', + + feature_requested: 'Custom Integrations', + request_message: + 'We need custom integrations with our CRM system for better data flow.', + status: 'rejected', + created_at: '2024-01-12T11:20:00Z', + updated_at: '2024-01-12T15:30:00Z', + }, + ]; + + // Simulate filtering and pagination + let filteredData = dummyData; + + if (status && status !== 'all') { + filteredData = filteredData.filter((item) => item.status === status); + } + + if (q) { + filteredData = filteredData.filter( + (item) => + item.workspace_name.toLowerCase().includes(q.toLowerCase()) || + item.creator_name.toLowerCase().includes(q.toLowerCase()) || + item.feature_requested.toLowerCase().includes(q.toLowerCase()) + ); + } + + const startIndex = (parseInt(page) - 1) * parseInt(pageSize); + const endIndex = startIndex + parseInt(pageSize); + const paginatedData = filteredData.slice(startIndex, endIndex); + + return { + data: paginatedData, + count: filteredData.length, + }; +}; + +// Type definition for workspace approval requests +export interface WorkspaceApprovalRequest { + id: string; + workspace_id: string; + workspace_name: string; + creator_id: string; + creator_name: string; + feature_requested: string; + request_message: string; + status: 'pending' | 'approved' | 'rejected'; + created_at: string; + updated_at: string; +} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/row-actions.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/row-actions.tsx new file mode 100644 index 0000000000..e729ae889d --- /dev/null +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/row-actions.tsx @@ -0,0 +1,182 @@ +'use client'; + +import { WorkspaceApprovalRequest } from './page'; +import { Row } from '@tanstack/react-table'; +import { Button } from '@tuturuuu/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@tuturuuu/ui/dropdown-menu'; +import { Check, Ellipsis, Eye, Loader2, X } from '@tuturuuu/ui/icons'; +import { useTranslations } from 'next-intl'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +interface ApprovalRowActionsProps { + row: Row; +} + +export function ApprovalRowActions({ row }: ApprovalRowActionsProps) { + const t = useTranslations(); + const approval = row.original; + const [isLoading, setIsLoading] = useState(false); + + const handleApprove = async () => { + setIsLoading(true); + try { + // TODO: Replace with actual API call + await mockApprovalAction(approval.id, 'approve'); + toast.success(`Approved feature request for ${approval.workspace_name}`); + // Refresh the page or update the data + window.location.reload(); + } catch (error) { + toast.error('Failed to approve request'); + } finally { + setIsLoading(false); + } + }; + + const handleReject = async () => { + setIsLoading(true); + try { + // TODO: Replace with actual API call + await mockApprovalAction(approval.id, 'reject'); + toast.success(`Rejected feature request for ${approval.workspace_name}`); + // Refresh the page or update the data + window.location.reload(); + } catch (error) { + toast.error('Failed to reject request'); + } finally { + setIsLoading(false); + } + }; + + const handleViewDetails = () => { + // TODO: Open a modal or navigate to details page + toast.info('Details view not implemented yet'); + }; + + const isPending = approval.status === 'pending'; + + if (isPending) { + return ( + + + + + + + + View Details + + + + {isLoading ? ( + + ) : ( + + )} + Approve Request + + + {isLoading ? ( + + ) : ( + + )} + Reject Request + + + + ); + } + + // For approved/rejected requests, only show view details + return ( + + + + + + + + View Details + + {approval.status === 'approved' && ( + <> + + + {isLoading ? ( + + ) : ( + + )} + Revoke Access + + + )} + {approval.status === 'rejected' && ( + <> + + + {isLoading ? ( + + ) : ( + + )} + Approve Request + + + )} + + + ); +} + +// Mock function to simulate API calls +// TODO: Replace with actual API implementation +const mockApprovalAction = async ( + requestId: string, + action: 'approve' | 'reject' +): Promise => { + // Simulate API delay + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Simulate occasional failures for testing + if (Math.random() < 0.1) { + throw new Error(`Failed to ${action} request`); + } + + console.log(`${action} request ${requestId}`); +}; diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/status-filter.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/status-filter.tsx new file mode 100644 index 0000000000..c19b437bc0 --- /dev/null +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/approvals/status-filter.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@tuturuuu/ui/select'; +import { useRouter, useSearchParams } from 'next/navigation'; + +interface StatusFilterProps { + currentStatus?: string; +} + +export function StatusFilter({ currentStatus }: StatusFilterProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const handleStatusChange = (status: string) => { + const params = new URLSearchParams(searchParams); + + if (status === 'all') { + params.delete('status'); + } else { + params.set('status', status); + } + + // Reset to first page when filtering + params.delete('page'); + + router.push(`?${params.toString()}`); + }; + + return ( + + ); +} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/layout.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/layout.tsx index 02b1369bdf..86c309bcd9 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/layout.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/layout.tsx @@ -72,6 +72,11 @@ export default async function Layout({ children, params }: LayoutProps) { disabled: withoutPermission('manage_workspace_audit_logs'), requireRootWorkspace: true, }, + { + title: 'Approvals', + href: `/${wsId}/approvals`, + requireRootWorkspace: true, + }, ]; return ( diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/layout.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/layout.tsx index 6ff2b74b7a..363920789e 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/layout.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/layout.tsx @@ -2,6 +2,7 @@ import InvitationCard from '@/app/[locale]/(dashboard)/_components/invitation-ca import { Structure } from '@/components/layout/structure'; import NavbarActions from '@/components/navbar-actions'; import type { NavLink } from '@/components/navigation'; +import { EducationBanner } from '@/components/request-education-banner'; import { UserNav } from '@/components/user-nav'; import { SIDEBAR_COLLAPSED_COOKIE_NAME } from '@/constants/common'; import { @@ -50,6 +51,14 @@ export default async function Layout({ children, params }: LayoutProps) { value: 'true', }); + const ENABLE_EDUCATION = await verifySecret({ + forceAdmin: true, + wsId, + name: 'ENABLE_EDUCATION', + value: 'true', + }); + console.log('ENABLE_EDUCATION', ENABLE_EDUCATION); + const navLinks: (NavLink | null)[] = [ // { // title: t('sidebar_tabs.education'), @@ -266,8 +275,13 @@ export default async function Layout({ children, params }: LayoutProps) { ]; const workspace = await getWorkspace(wsId); + const user = await getCurrentUser(); + // Check if user is workspace creator for education access request + const isWorkspaceCreator = workspace?.role === 'OWNER'; + const shouldShowRequestButton = isWorkspaceCreator && !ENABLE_EDUCATION; + const collapsed = (await cookies()).get(SIDEBAR_COLLAPSED_COOKIE_NAME); const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined; @@ -306,6 +320,9 @@ export default async function Layout({ children, params }: LayoutProps) { } > + {shouldShowRequestButton && workspace.name && ( + + )} {children} ); diff --git a/apps/upskii/src/components/request-access-button.tsx b/apps/upskii/src/components/request-access-button.tsx new file mode 100644 index 0000000000..83884d9bba --- /dev/null +++ b/apps/upskii/src/components/request-access-button.tsx @@ -0,0 +1,170 @@ +'use client'; + +import { Button } from '@tuturuuu/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@tuturuuu/ui/dialog'; +import { BookText, Loader2, Send } from '@tuturuuu/ui/icons'; +import { Label } from '@tuturuuu/ui/label'; +import { Textarea } from '@tuturuuu/ui/textarea'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +interface RequestAccessButtonProps { + workspaceName: string; + wsId: string; +} + +export function RequestAccessButton({ + workspaceName, + wsId, +}: RequestAccessButtonProps) { + const [open, setOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [message, setMessage] = useState(''); + + const handleSubmitRequest = async () => { + if (!message.trim()) { + toast.error('Please provide a reason for your request'); + return; + } + + setIsLoading(true); + try { + // TODO: Replace with actual API call + await mockSubmitFeatureRequest({ + wsId, + workspaceName, + feature: 'Education Features', + message: message.trim(), + }); + + toast.success( + 'Access request submitted successfully! Platform administrators will review your request.' + ); + setOpen(false); + setMessage(''); + } catch (error) { + toast.error('Failed to submit request. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + + + + + Request Education Features + + + Request access to education features for your workspace " + {workspaceName}". Platform administrators will review your request + and approve access if appropriate. + + + +
+
+ +