gmoonc 0.0.17 → 0.0.18
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/README.md
CHANGED
|
@@ -63,6 +63,15 @@ The logo is installed at `src/gmoonc/assets/gmoonc-logo.png`. You can replace it
|
|
|
63
63
|
|
|
64
64
|
## Changelog
|
|
65
65
|
|
|
66
|
+
### 0.0.18
|
|
67
|
+
- Fix: env.ts no longer throws when .env is missing - only logs warning
|
|
68
|
+
- Fix: client.ts import corrected - hasSupabaseEnv imported from env.ts
|
|
69
|
+
- Fix: GMooncSupabaseSessionProvider imports corrected - hasSupabaseEnv from env.ts
|
|
70
|
+
- Fix: createMissingEnvClient improved - more robust mock client
|
|
71
|
+
- Fix: GMooncAppLayout route protection added - redirects to /login when not authenticated
|
|
72
|
+
- Fix: All imports in generated files now without .js suffix
|
|
73
|
+
- Fix: client.ts syntax error fixed - removed extra closing brace
|
|
74
|
+
|
|
66
75
|
### 0.0.17
|
|
67
76
|
- Feature: New `supabase --vite` command to setup Supabase integration
|
|
68
77
|
- Feature: Supabase Auth + RBAC integration with session provider
|
package/dist/index.cjs
CHANGED
|
@@ -787,30 +787,35 @@ function generateEnvFile(projectDir, supabaseDir, platform) {
|
|
|
787
787
|
* Supabase environment variables for Vite
|
|
788
788
|
*
|
|
789
789
|
* This file reads environment variables from import.meta.env (Vite)
|
|
790
|
-
* and validates their presence.
|
|
790
|
+
* and validates their presence. Never throws - only logs warnings.
|
|
791
791
|
*/
|
|
792
792
|
|
|
793
793
|
const supabaseUrlValue = import.meta.env.VITE_SUPABASE_URL;
|
|
794
794
|
const supabaseAnonKeyValue = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
|
795
795
|
|
|
796
|
-
|
|
796
|
+
// Safe trim with fallback to empty string
|
|
797
|
+
const url = typeof supabaseUrlValue === 'string' ? supabaseUrlValue.trim() : '';
|
|
798
|
+
const anonKey = typeof supabaseAnonKeyValue === 'string' ? supabaseAnonKeyValue.trim() : '';
|
|
797
799
|
|
|
798
|
-
|
|
800
|
+
export const supabaseUrl = url;
|
|
801
|
+
export const supabaseAnonKey = anonKey;
|
|
802
|
+
export const hasSupabaseEnv = Boolean(url && anonKey);
|
|
803
|
+
|
|
804
|
+
// Log warning once if env vars are missing (only in browser, only in dev)
|
|
805
|
+
if (!hasSupabaseEnv && typeof window !== 'undefined' && import.meta.env.DEV) {
|
|
799
806
|
const missingVars: string[] = [];
|
|
800
|
-
if (!
|
|
801
|
-
if (!
|
|
802
|
-
|
|
803
|
-
const errorMessage = \`Missing Supabase environment variables: \${missingVars.join(', ')}.\\n\\nPlease:\\n1. Copy .env.example to .env\\n2. Fill VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY\\n3. Restart dev server\`;
|
|
804
|
-
|
|
805
|
-
if (typeof window !== 'undefined') {
|
|
806
|
-
console.error('\u274C', errorMessage);
|
|
807
|
-
}
|
|
807
|
+
if (!url) missingVars.push('VITE_SUPABASE_URL');
|
|
808
|
+
if (!anonKey) missingVars.push('VITE_SUPABASE_ANON_KEY');
|
|
808
809
|
|
|
809
|
-
|
|
810
|
+
console.warn(
|
|
811
|
+
\`\u26A0\uFE0F Supabase environment variables missing: \${missingVars.join(', ')}.\\n\` +
|
|
812
|
+
\`Please:\\n\` +
|
|
813
|
+
\`1. Copy .env.example to .env\\n\` +
|
|
814
|
+
\`2. Fill VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY\\n\` +
|
|
815
|
+
\`3. Restart dev server\\n\` +
|
|
816
|
+
\`The app will continue without authentication until configured.\`
|
|
817
|
+
);
|
|
810
818
|
}
|
|
811
|
-
|
|
812
|
-
export const supabaseUrl = supabaseUrlValue!;
|
|
813
|
-
export const supabaseAnonKey = supabaseAnonKeyValue!;
|
|
814
819
|
`;
|
|
815
820
|
writeFileSafe(envFilePath, content);
|
|
816
821
|
logSuccess("Generated src/gmoonc/supabase/env.ts");
|
|
@@ -818,7 +823,7 @@ export const supabaseAnonKey = supabaseAnonKeyValue!;
|
|
|
818
823
|
function generateClientFile(projectDir, supabaseDir) {
|
|
819
824
|
const clientFilePath = (0, import_path6.join)(supabaseDir, "client.ts");
|
|
820
825
|
const content = `import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
|
821
|
-
import { supabaseUrl, supabaseAnonKey, hasSupabaseEnv } from './env
|
|
826
|
+
import { supabaseUrl, supabaseAnonKey, hasSupabaseEnv } from './env';
|
|
822
827
|
|
|
823
828
|
// Database types (minimal - extend as needed)
|
|
824
829
|
export interface Database {
|
|
@@ -1118,37 +1123,58 @@ export interface Database {
|
|
|
1118
1123
|
// Singleton pattern to avoid multiple instances
|
|
1119
1124
|
let supabaseInstance: SupabaseClient<Database> | null = null;
|
|
1120
1125
|
|
|
1121
|
-
|
|
1126
|
+
let warningLogged = false;
|
|
1127
|
+
|
|
1128
|
+
const createMissingEnvClient = (): SupabaseClient<Database> => {
|
|
1122
1129
|
const missingEnvMessage = 'Supabase environment variables are missing or empty.';
|
|
1123
1130
|
|
|
1131
|
+
// Log warning once
|
|
1132
|
+
if (!warningLogged && typeof window !== 'undefined') {
|
|
1133
|
+
console.warn('\u26A0\uFE0F Supabase env not configured. Authentication features will not work.');
|
|
1134
|
+
warningLogged = true;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1124
1137
|
const notConfigured = () => Promise.reject(new Error(missingEnvMessage));
|
|
1138
|
+
const emptyResult = () => Promise.resolve({ data: null, error: new Error(missingEnvMessage) });
|
|
1139
|
+
const emptyArray = () => Promise.resolve({ data: [], error: new Error(missingEnvMessage) });
|
|
1125
1140
|
|
|
1126
1141
|
return {
|
|
1127
1142
|
auth: {
|
|
1128
|
-
getSession: async () => ({ data: { session: null }, error:
|
|
1143
|
+
getSession: async () => ({ data: { session: null }, error: null }),
|
|
1129
1144
|
signInWithPassword: notConfigured,
|
|
1130
1145
|
signUp: notConfigured,
|
|
1131
|
-
signOut:
|
|
1146
|
+
signOut: async () => ({ error: null }),
|
|
1132
1147
|
resend: notConfigured,
|
|
1133
|
-
onAuthStateChange: () => ({
|
|
1148
|
+
onAuthStateChange: () => ({
|
|
1149
|
+
data: { subscription: { unsubscribe: () => {} } },
|
|
1150
|
+
error: null
|
|
1151
|
+
}),
|
|
1134
1152
|
startAutoRefresh: () => Promise.resolve(),
|
|
1135
1153
|
stopAutoRefresh: () => {},
|
|
1136
1154
|
},
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
eq: () => ({
|
|
1155
|
+
from: (table: string) => ({
|
|
1156
|
+
select: () => ({
|
|
1157
|
+
eq: () => ({
|
|
1158
|
+
single: () => emptyResult(),
|
|
1159
|
+
maybeSingle: () => emptyResult(),
|
|
1160
|
+
}),
|
|
1161
|
+
order: () => ({
|
|
1162
|
+
eq: () => emptyArray(),
|
|
1163
|
+
}),
|
|
1140
1164
|
}),
|
|
1141
|
-
|
|
1142
|
-
|
|
1165
|
+
insert: () => notConfigured(),
|
|
1166
|
+
update: () => notConfigured(),
|
|
1167
|
+
delete: () => notConfigured(),
|
|
1168
|
+
upsert: () => notConfigured(),
|
|
1169
|
+
}),
|
|
1170
|
+
rpc: () => Promise.resolve({ data: null, error: new Error(missingEnvMessage) }),
|
|
1171
|
+
} as unknown as SupabaseClient<Database>;
|
|
1143
1172
|
};
|
|
1144
1173
|
|
|
1145
1174
|
export const supabase = (() => {
|
|
1146
1175
|
if (supabaseInstance) return supabaseInstance;
|
|
1147
1176
|
|
|
1148
1177
|
if (!hasSupabaseEnv) {
|
|
1149
|
-
if (typeof window !== 'undefined' && import.meta.env.DEV) {
|
|
1150
|
-
console.warn('\u26A0\uFE0F Supabase env vars missing. App will continue without authentication.');
|
|
1151
|
-
}
|
|
1152
1178
|
supabaseInstance = createMissingEnvClient();
|
|
1153
1179
|
return supabaseInstance;
|
|
1154
1180
|
}
|
|
@@ -1196,7 +1222,8 @@ function generateSessionProvider(projectDir, supabaseDir) {
|
|
|
1196
1222
|
const content = `import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
|
1197
1223
|
import { useNavigate } from 'react-router-dom';
|
|
1198
1224
|
import { User } from '@supabase/supabase-js';
|
|
1199
|
-
import { supabase
|
|
1225
|
+
import { supabase } from '../client';
|
|
1226
|
+
import { hasSupabaseEnv } from '../env';
|
|
1200
1227
|
|
|
1201
1228
|
export interface GMooncUser {
|
|
1202
1229
|
id: string;
|
|
@@ -1418,8 +1445,8 @@ export function useGMooncSession(): GMooncSessionContextType {
|
|
|
1418
1445
|
}
|
|
1419
1446
|
function generateRbacHelpers(projectDir, supabaseDir) {
|
|
1420
1447
|
const getMyProfilePath = (0, import_path6.join)(supabaseDir, "rbac/getMyProfile.ts");
|
|
1421
|
-
const getMyProfileContent = `import { supabase } from '../client
|
|
1422
|
-
import type { Database } from '../client
|
|
1448
|
+
const getMyProfileContent = `import { supabase } from '../client';
|
|
1449
|
+
import type { Database } from '../client';
|
|
1423
1450
|
|
|
1424
1451
|
type Profile = Database['public']['Tables']['profiles']['Row'];
|
|
1425
1452
|
|
|
@@ -1456,7 +1483,7 @@ export async function getMyProfile(): Promise<Profile | null> {
|
|
|
1456
1483
|
writeFileSafe(getMyProfilePath, getMyProfileContent);
|
|
1457
1484
|
logSuccess("Generated src/gmoonc/supabase/rbac/getMyProfile.ts");
|
|
1458
1485
|
const hasPermissionPath = (0, import_path6.join)(supabaseDir, "rbac/hasPermission.ts");
|
|
1459
|
-
const hasPermissionContent = `import { supabase } from '../client
|
|
1486
|
+
const hasPermissionContent = `import { supabase } from '../client';
|
|
1460
1487
|
|
|
1461
1488
|
/**
|
|
1462
1489
|
* Check if the current user has a specific permission
|
|
@@ -1545,7 +1572,7 @@ export function hasPermissionSync(
|
|
|
1545
1572
|
writeFileSafe(hasPermissionPath, hasPermissionContent);
|
|
1546
1573
|
logSuccess("Generated src/gmoonc/supabase/rbac/hasPermission.ts");
|
|
1547
1574
|
const getUserPermissionsPath = (0, import_path6.join)(supabaseDir, "rbac/getUserPermissions.ts");
|
|
1548
|
-
const getUserPermissionsContent = `import { supabase } from '../client
|
|
1575
|
+
const getUserPermissionsContent = `import { supabase } from '../client';
|
|
1549
1576
|
|
|
1550
1577
|
export interface UserPermission {
|
|
1551
1578
|
module_name: string;
|
|
@@ -1589,10 +1616,10 @@ export async function getUserPermissions(): Promise<UserPermission[]> {
|
|
|
1589
1616
|
writeFileSafe(getUserPermissionsPath, getUserPermissionsContent);
|
|
1590
1617
|
logSuccess("Generated src/gmoonc/supabase/rbac/getUserPermissions.ts");
|
|
1591
1618
|
const rbacIndexPath = (0, import_path6.join)(supabaseDir, "rbac/index.ts");
|
|
1592
|
-
const rbacIndexContent = `export { getMyProfile } from './getMyProfile
|
|
1593
|
-
export { hasPermission, hasPermissionSync } from './hasPermission
|
|
1594
|
-
export { getUserPermissions } from './getUserPermissions
|
|
1595
|
-
export type { UserPermission } from './getUserPermissions
|
|
1619
|
+
const rbacIndexContent = `export { getMyProfile } from './getMyProfile';
|
|
1620
|
+
export { hasPermission, hasPermissionSync } from './hasPermission';
|
|
1621
|
+
export { getUserPermissions } from './getUserPermissions';
|
|
1622
|
+
export type { UserPermission } from './getUserPermissions';
|
|
1596
1623
|
`;
|
|
1597
1624
|
writeFileSafe(rbacIndexPath, rbacIndexContent);
|
|
1598
1625
|
logSuccess("Generated src/gmoonc/supabase/rbac/index.ts");
|
|
@@ -1638,18 +1665,64 @@ function patchExistingCode(projectDir, gmooncDir) {
|
|
|
1638
1665
|
);
|
|
1639
1666
|
content = content.replace(
|
|
1640
1667
|
/import\s+{\s*GMooncSessionProvider[^}]*}\s+from\s+['"][^'"]*session\/GMooncSessionContext['"]/g,
|
|
1641
|
-
"import { GMooncSupabaseSessionProvider } from '../supabase/auth/GMooncSupabaseSessionProvider'"
|
|
1668
|
+
"import { GMooncSupabaseSessionProvider, useGMooncSession } from '../supabase/auth/GMooncSupabaseSessionProvider'"
|
|
1642
1669
|
);
|
|
1670
|
+
if (!content.includes("from 'react-router-dom'") || !content.includes("Navigate")) {
|
|
1671
|
+
content = content.replace(
|
|
1672
|
+
/import\s+{\s*([^}]*)\s*}\s+from\s+['"]react-router-dom['"]/,
|
|
1673
|
+
(match, imports) => {
|
|
1674
|
+
if (!imports.includes("Navigate")) {
|
|
1675
|
+
return `import { ${imports}, Navigate } from 'react-router-dom'`;
|
|
1676
|
+
}
|
|
1677
|
+
return match;
|
|
1678
|
+
}
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1643
1681
|
content = content.replace(/GMooncSessionProvider/g, "GMooncSupabaseSessionProvider");
|
|
1682
|
+
if (!content.includes("!isLoading && !isAuthenticated")) {
|
|
1683
|
+
content = content.replace(
|
|
1684
|
+
/const\s+{\s*roles,\s*logout\s*}\s+=\s+useGMooncSession\(\);/,
|
|
1685
|
+
"const { roles, logout, isLoading, isAuthenticated } = useGMooncSession();"
|
|
1686
|
+
);
|
|
1687
|
+
content = content.replace(
|
|
1688
|
+
/(\s+}, \[navigate, getBasePath\]\);)\s+(\s+return\s+\()/,
|
|
1689
|
+
`$1
|
|
1690
|
+
|
|
1691
|
+
// Route protection: redirect to login if not authenticated
|
|
1692
|
+
if (!isLoading && !isAuthenticated) {
|
|
1693
|
+
return <Navigate to="/login" replace />;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// Show loading state while checking authentication
|
|
1697
|
+
if (isLoading) {
|
|
1698
|
+
return (
|
|
1699
|
+
<div className="gmoonc-root">
|
|
1700
|
+
<div style={{
|
|
1701
|
+
display: 'flex',
|
|
1702
|
+
justifyContent: 'center',
|
|
1703
|
+
alignItems: 'center',
|
|
1704
|
+
height: '100vh',
|
|
1705
|
+
fontSize: 'var(--gmoonc-font-size-base, 16px)',
|
|
1706
|
+
color: 'var(--gmoonc-color-text, #333)'
|
|
1707
|
+
}}>
|
|
1708
|
+
Loading...
|
|
1709
|
+
</div>
|
|
1710
|
+
</div>
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
$2`
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1644
1717
|
writeFileSafe(layoutPath, content);
|
|
1645
|
-
logSuccess("Patched layout/GMooncAppLayout.tsx to use GMooncSupabaseSessionProvider");
|
|
1718
|
+
logSuccess("Patched layout/GMooncAppLayout.tsx to use GMooncSupabaseSessionProvider and add route protection");
|
|
1646
1719
|
}
|
|
1647
1720
|
logInfo("Auth pages will use Supabase via GMooncSupabaseSessionProvider");
|
|
1648
1721
|
}
|
|
1649
1722
|
|
|
1650
1723
|
// src/cli/index.ts
|
|
1651
1724
|
var program = new import_commander.Command();
|
|
1652
|
-
program.name("gmoonc").description("Goalmoon Ctrl (gmoonc): Install complete dashboard into your React project").version("0.0.
|
|
1725
|
+
program.name("gmoonc").description("Goalmoon Ctrl (gmoonc): Install complete dashboard into your React project").version("0.0.18").option("--base <path>", "Base path for dashboard routes", "/app").option("--skip-router-patch", "Skip automatic router integration (only copy files and inject CSS)").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
|
|
1653
1726
|
try {
|
|
1654
1727
|
logInfo("\u{1F680} Starting gmoonc installer...");
|
|
1655
1728
|
logInfo("\u{1F4E6} Installing complete dashboard into your React project\n");
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useCallback } from 'react';
|
|
2
|
-
import { Outlet, useLocation, useNavigate, Link } from 'react-router-dom';
|
|
2
|
+
import { Outlet, useLocation, useNavigate, Link, Navigate } from 'react-router-dom';
|
|
3
3
|
import { GmooncShell } from '../ui/shell';
|
|
4
4
|
import { defaultConfig } from '../config/defaultConfig';
|
|
5
5
|
import { GMooncSessionProvider, useGMooncSession } from '../session/GMooncSessionContext';
|
|
@@ -9,7 +9,7 @@ import logoUrl from '../assets/gmoonc-logo.png';
|
|
|
9
9
|
function GMooncAppLayoutInner() {
|
|
10
10
|
const location = useLocation();
|
|
11
11
|
const navigate = useNavigate();
|
|
12
|
-
const { roles, logout } = useGMooncSession();
|
|
12
|
+
const { roles, logout, isLoading, isAuthenticated } = useGMooncSession();
|
|
13
13
|
|
|
14
14
|
// Determine basePath from current location
|
|
15
15
|
// If path is /app/..., basePath is /app
|
|
@@ -47,6 +47,29 @@ function GMooncAppLayoutInner() {
|
|
|
47
47
|
navigate(normalizedBasePath);
|
|
48
48
|
}, [navigate, getBasePath]);
|
|
49
49
|
|
|
50
|
+
// Route protection: redirect to login if not authenticated
|
|
51
|
+
if (!isLoading && !isAuthenticated) {
|
|
52
|
+
return <Navigate to="/login" replace />;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Show loading state while checking authentication
|
|
56
|
+
if (isLoading) {
|
|
57
|
+
return (
|
|
58
|
+
<div className="gmoonc-root">
|
|
59
|
+
<div style={{
|
|
60
|
+
display: 'flex',
|
|
61
|
+
justifyContent: 'center',
|
|
62
|
+
alignItems: 'center',
|
|
63
|
+
height: '100vh',
|
|
64
|
+
fontSize: 'var(--gmoonc-font-size-base, 16px)',
|
|
65
|
+
color: 'var(--gmoonc-color-text, #333)'
|
|
66
|
+
}}>
|
|
67
|
+
Loading...
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
return (
|
|
51
74
|
<div className="gmoonc-root">
|
|
52
75
|
<GmooncShell
|