@almadar/cli-linux-x64 1.5.3 → 1.5.4
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/almadar +0 -0
- package/package.json +1 -1
- package/shells/almadar-shell/.env.example +12 -0
- package/shells/almadar-shell/almadar-shell/.env.example +12 -0
- package/shells/almadar-shell/almadar-shell/package.json +19 -0
- package/shells/almadar-shell/almadar-shell/packages/client/index.html +13 -0
- package/shells/almadar-shell/almadar-shell/packages/client/package.json +49 -0
- package/shells/almadar-shell/almadar-shell/packages/client/postcss.config.js +6 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/App.tsx +68 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/config/firebase.ts +37 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/features/auth/AuthContext.tsx +139 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/features/auth/authService.ts +83 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/features/auth/components/Login.tsx +218 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/features/auth/components/ProtectedRoute.tsx +27 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/features/auth/components/UserProfile.tsx +68 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/features/auth/components/index.ts +3 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/features/auth/index.ts +13 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/features/auth/types.ts +24 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/generated/index.ts +13 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/index.css +6 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/main.tsx +10 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/navigation/index.ts +55 -0
- package/shells/almadar-shell/almadar-shell/packages/client/src/pages/index.ts +12 -0
- package/shells/almadar-shell/almadar-shell/packages/client/tailwind.config.js +12 -0
- package/shells/almadar-shell/almadar-shell/packages/client/tsconfig.json +33 -0
- package/shells/almadar-shell/almadar-shell/packages/client/vite.config.ts +49 -0
- package/shells/almadar-shell/almadar-shell/packages/server/package.json +32 -0
- package/shells/almadar-shell/almadar-shell/packages/server/src/app.ts +36 -0
- package/shells/almadar-shell/almadar-shell/packages/server/src/index.ts +16 -0
- package/shells/almadar-shell/almadar-shell/packages/server/src/routes.ts +11 -0
- package/shells/almadar-shell/almadar-shell/packages/server/src/types/express.d.ts +15 -0
- package/shells/almadar-shell/almadar-shell/packages/server/tsconfig.json +23 -0
- package/shells/almadar-shell/almadar-shell/packages/shared/package.json +10 -0
- package/shells/almadar-shell/almadar-shell/packages/shared/src/index.ts +2 -0
- package/shells/almadar-shell/almadar-shell/turbo.json +17 -0
- package/shells/almadar-shell/package.json +19 -0
- package/shells/almadar-shell/packages/client/index.html +13 -0
- package/shells/almadar-shell/packages/client/package.json +49 -0
- package/shells/almadar-shell/packages/client/postcss.config.js +6 -0
- package/shells/almadar-shell/packages/client/src/App.tsx +68 -0
- package/shells/almadar-shell/packages/client/src/config/firebase.ts +37 -0
- package/shells/almadar-shell/packages/client/src/features/auth/AuthContext.tsx +139 -0
- package/shells/almadar-shell/packages/client/src/features/auth/authService.ts +83 -0
- package/shells/almadar-shell/packages/client/src/features/auth/components/Login.tsx +218 -0
- package/shells/almadar-shell/packages/client/src/features/auth/components/ProtectedRoute.tsx +27 -0
- package/shells/almadar-shell/packages/client/src/features/auth/components/UserProfile.tsx +68 -0
- package/shells/almadar-shell/packages/client/src/features/auth/components/index.ts +3 -0
- package/shells/almadar-shell/packages/client/src/features/auth/index.ts +13 -0
- package/shells/almadar-shell/packages/client/src/features/auth/types.ts +24 -0
- package/shells/almadar-shell/packages/client/src/generated/index.ts +13 -0
- package/shells/almadar-shell/packages/client/src/index.css +6 -0
- package/shells/almadar-shell/packages/client/src/main.tsx +10 -0
- package/shells/almadar-shell/packages/client/src/navigation/index.ts +55 -0
- package/shells/almadar-shell/packages/client/src/pages/index.ts +12 -0
- package/shells/almadar-shell/packages/client/tailwind.config.js +12 -0
- package/shells/almadar-shell/packages/client/tsconfig.json +33 -0
- package/shells/almadar-shell/packages/client/vite.config.ts +49 -0
- package/shells/almadar-shell/packages/server/package.json +32 -0
- package/shells/almadar-shell/packages/server/src/app.ts +36 -0
- package/shells/almadar-shell/packages/server/src/index.ts +16 -0
- package/shells/almadar-shell/packages/server/src/routes.ts +11 -0
- package/shells/almadar-shell/packages/server/src/types/express.d.ts +15 -0
- package/shells/almadar-shell/packages/server/tsconfig.json +23 -0
- package/shells/almadar-shell/packages/shared/package.json +10 -0
- package/shells/almadar-shell/packages/shared/src/index.ts +2 -0
- package/shells/almadar-shell/turbo.json +17 -0
package/almadar
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Server
|
|
2
|
+
PORT=3030
|
|
3
|
+
NODE_ENV=development
|
|
4
|
+
|
|
5
|
+
# Firebase (required for auth — use emulator for local dev)
|
|
6
|
+
FIRESTORE_EMULATOR_HOST=localhost:8080
|
|
7
|
+
# FIREBASE_PROJECT_ID=
|
|
8
|
+
# FIREBASE_CLIENT_EMAIL=
|
|
9
|
+
# FIREBASE_PRIVATE_KEY=
|
|
10
|
+
|
|
11
|
+
# Client (set in packages/client/.env)
|
|
12
|
+
# VITE_API_URL=http://localhost:3030
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Server
|
|
2
|
+
PORT=3030
|
|
3
|
+
NODE_ENV=development
|
|
4
|
+
|
|
5
|
+
# Firebase (required for auth — use emulator for local dev)
|
|
6
|
+
FIRESTORE_EMULATOR_HOST=localhost:8080
|
|
7
|
+
# FIREBASE_PROJECT_ID=
|
|
8
|
+
# FIREBASE_CLIENT_EMAIL=
|
|
9
|
+
# FIREBASE_PRIVATE_KEY=
|
|
10
|
+
|
|
11
|
+
# Client (set in packages/client/.env)
|
|
12
|
+
# VITE_API_URL=http://localhost:3030
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@almadar/shell",
|
|
3
|
+
"version": "1.0.14",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Minimal full-stack shell template for Almadar applications",
|
|
6
|
+
"workspaces": [
|
|
7
|
+
"packages/*"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "npx concurrently -n client,server -c blue,green \"npm run dev -w packages/client\" \"npm run dev -w packages/server\"",
|
|
11
|
+
"build": "npm run build --workspaces --if-present",
|
|
12
|
+
"typecheck": "turbo run typecheck",
|
|
13
|
+
"lint": "turbo run lint"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"concurrently": "^8.2.0",
|
|
17
|
+
"turbo": "^2.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Almadar App</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@almadar/shell-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"lint": "eslint src/",
|
|
11
|
+
"preview": "vite preview",
|
|
12
|
+
"test": "vitest run --passWithNoTests",
|
|
13
|
+
"test:watch": "vitest"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@almadar/ui": "^1.0.0",
|
|
17
|
+
"@almadar/evaluator": "^1.0.0",
|
|
18
|
+
"@almadar/patterns": "^1.0.0",
|
|
19
|
+
"@almadar/core": "^1.0.0",
|
|
20
|
+
"firebase": "^11.0.0",
|
|
21
|
+
"@tanstack/react-query": "^5.62.0",
|
|
22
|
+
"react": "^18.3.1",
|
|
23
|
+
"react-dom": "^18.3.1",
|
|
24
|
+
"react-router-dom": "^7.1.0",
|
|
25
|
+
"zustand": "^5.0.2",
|
|
26
|
+
"react-markdown": "^9.0.0",
|
|
27
|
+
"remark-gfm": "^4.0.0",
|
|
28
|
+
"remark-math": "^6.0.0",
|
|
29
|
+
"rehype-katex": "^7.0.0",
|
|
30
|
+
"rehype-raw": "^7.0.0",
|
|
31
|
+
"react-force-graph-2d": "^1.25.0",
|
|
32
|
+
"@monaco-editor/react": "^4.6.0",
|
|
33
|
+
"monaco-editor": "^0.52.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@testing-library/react": "^16.1.0",
|
|
37
|
+
"@testing-library/jest-dom": "^6.6.0",
|
|
38
|
+
"@types/react": "^18.3.0",
|
|
39
|
+
"@types/react-dom": "^18.3.0",
|
|
40
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
41
|
+
"autoprefixer": "^10.4.20",
|
|
42
|
+
"jsdom": "^25.0.0",
|
|
43
|
+
"postcss": "^8.4.49",
|
|
44
|
+
"tailwindcss": "^3.4.17",
|
|
45
|
+
"typescript": "^5.7.0",
|
|
46
|
+
"vite": "^6.0.0",
|
|
47
|
+
"vitest": "^2.1.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Main application component with compiler-generated content placeholders.
|
|
5
|
+
* The Rust compiler replaces {{PLACEHOLDERS}} with generated code.
|
|
6
|
+
*
|
|
7
|
+
* Navigation works via schema-driven NavigationProvider:
|
|
8
|
+
* - NavigationProvider holds active page state
|
|
9
|
+
* - navigateTo() switches pages and fires INIT with payload
|
|
10
|
+
* - No dependency on react-router for internal navigation
|
|
11
|
+
* - react-router is optional for URL bookmarkability
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
15
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
16
|
+
import {
|
|
17
|
+
ThemeProvider,
|
|
18
|
+
EventBusProvider,
|
|
19
|
+
UISlotProvider,
|
|
20
|
+
VerificationProvider,
|
|
21
|
+
} from '@almadar/ui/providers';
|
|
22
|
+
import { NavigationProvider } from '@almadar/ui/renderer';
|
|
23
|
+
|
|
24
|
+
// {{GENERATED_IMPORTS}}
|
|
25
|
+
|
|
26
|
+
// Generated schema import (compiler fills this in)
|
|
27
|
+
// {{GENERATED_SCHEMA_IMPORT}}
|
|
28
|
+
const schema = { orbitals: [] }; // Placeholder - replaced by compiler
|
|
29
|
+
|
|
30
|
+
const queryClient = new QueryClient({
|
|
31
|
+
defaultOptions: {
|
|
32
|
+
queries: {
|
|
33
|
+
staleTime: 1000 * 60,
|
|
34
|
+
refetchOnWindowFocus: false,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function App() {
|
|
40
|
+
return (
|
|
41
|
+
<QueryClientProvider client={queryClient}>
|
|
42
|
+
<ThemeProvider>
|
|
43
|
+
<EventBusProvider>
|
|
44
|
+
<VerificationProvider>
|
|
45
|
+
<UISlotProvider>
|
|
46
|
+
<NavigationProvider
|
|
47
|
+
schema={schema}
|
|
48
|
+
updateUrl={true}
|
|
49
|
+
onNavigate={(pageName, path, payload) => {
|
|
50
|
+
console.log('[App] Navigation:', { pageName, path, payload });
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<BrowserRouter>
|
|
54
|
+
<Routes>
|
|
55
|
+
{/* {{GENERATED_ROUTES}} */}
|
|
56
|
+
<Route path="/" element={<div>Welcome to Almadar</div>} />
|
|
57
|
+
</Routes>
|
|
58
|
+
</BrowserRouter>
|
|
59
|
+
</NavigationProvider>
|
|
60
|
+
</UISlotProvider>
|
|
61
|
+
</VerificationProvider>
|
|
62
|
+
</EventBusProvider>
|
|
63
|
+
</ThemeProvider>
|
|
64
|
+
</QueryClientProvider>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default App;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { initializeApp, getApps, getApp, FirebaseApp } from 'firebase/app';
|
|
2
|
+
import { getAuth, Auth } from 'firebase/auth';
|
|
3
|
+
|
|
4
|
+
let app: FirebaseApp;
|
|
5
|
+
let auth: Auth;
|
|
6
|
+
|
|
7
|
+
export async function initializeFirebase(): Promise<void> {
|
|
8
|
+
if (getApps().length > 0) {
|
|
9
|
+
app = getApp();
|
|
10
|
+
auth = getAuth(app);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let config;
|
|
15
|
+
try {
|
|
16
|
+
// On Firebase Hosting, fetch auto-config from reserved URL
|
|
17
|
+
const res = await fetch('/__/firebase/init.json');
|
|
18
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
19
|
+
config = await res.json();
|
|
20
|
+
} catch {
|
|
21
|
+
// Fall back to env vars for local development
|
|
22
|
+
config = {
|
|
23
|
+
apiKey: import.meta.env.VITE_APP_FIREBASE_API_KEY,
|
|
24
|
+
authDomain: import.meta.env.VITE_APP_FIREBASE_AUTH_DOMAIN,
|
|
25
|
+
projectId: import.meta.env.VITE_APP_FIREBASE_PROJECT_ID,
|
|
26
|
+
storageBucket: import.meta.env.VITE_APP_FIREBASE_STORAGE_BUCKET,
|
|
27
|
+
messagingSenderId: import.meta.env.VITE_APP_FIREBASE_MESSAGING_SENDER_ID,
|
|
28
|
+
appId: import.meta.env.VITE_APP_FIREBASE_APP_ID,
|
|
29
|
+
measurementId: import.meta.env.VITE_APP_FIREBASE_MEASUREMENT_ID,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
app = initializeApp(config);
|
|
34
|
+
auth = getAuth(app);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { auth, app };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
2
|
+
import { User } from 'firebase/auth';
|
|
3
|
+
import { auth, initializeFirebase } from '../../config/firebase';
|
|
4
|
+
import { authService } from './authService';
|
|
5
|
+
import { AuthContextType } from './types';
|
|
6
|
+
|
|
7
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
8
|
+
|
|
9
|
+
export const useAuthContext = () => {
|
|
10
|
+
const context = useContext(AuthContext);
|
|
11
|
+
if (context === undefined) {
|
|
12
|
+
throw new Error('useAuthContext must be used within an AuthProvider');
|
|
13
|
+
}
|
|
14
|
+
return context;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
interface AuthProviderProps {
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|
22
|
+
const [user, setUser] = useState<User | null>(null);
|
|
23
|
+
const [loading, setLoading] = useState(true);
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
let unsubscribe: (() => void) | undefined;
|
|
28
|
+
|
|
29
|
+
initializeFirebase().then(() => {
|
|
30
|
+
unsubscribe = auth.onAuthStateChanged((firebaseUser) => {
|
|
31
|
+
setUser(firebaseUser);
|
|
32
|
+
setLoading(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return () => unsubscribe?.();
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const clearError = () => setError(null);
|
|
40
|
+
|
|
41
|
+
const signInWithGoogle = async () => {
|
|
42
|
+
try {
|
|
43
|
+
setLoading(true);
|
|
44
|
+
clearError();
|
|
45
|
+
await authService.signInWithGoogle();
|
|
46
|
+
} catch (err: unknown) {
|
|
47
|
+
setLoading(false);
|
|
48
|
+
const firebaseErr = err as { code?: string; message?: string };
|
|
49
|
+
const isCancel =
|
|
50
|
+
firebaseErr.code === 'auth/popup-closed-by-user' ||
|
|
51
|
+
firebaseErr.code === 'auth/cancelled-popup-request' ||
|
|
52
|
+
firebaseErr.code === 'auth/popup-blocked';
|
|
53
|
+
if (!isCancel) {
|
|
54
|
+
setError(firebaseErr.message ?? 'Google sign-in failed');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const signInWithEmail = async (email: string, password: string) => {
|
|
60
|
+
try {
|
|
61
|
+
setLoading(true);
|
|
62
|
+
clearError();
|
|
63
|
+
await authService.signInWithEmail(email, password);
|
|
64
|
+
} catch (err: unknown) {
|
|
65
|
+
setLoading(false);
|
|
66
|
+
setError((err as { message?: string }).message ?? 'Sign-in failed');
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const signUpWithEmail = async (email: string, password: string, displayName?: string) => {
|
|
71
|
+
try {
|
|
72
|
+
setLoading(true);
|
|
73
|
+
clearError();
|
|
74
|
+
await authService.signUpWithEmail(email, password, displayName);
|
|
75
|
+
} catch (err: unknown) {
|
|
76
|
+
setLoading(false);
|
|
77
|
+
setError((err as { message?: string }).message ?? 'Sign-up failed');
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const sendSignInLinkToEmail = async (email: string) => {
|
|
82
|
+
try {
|
|
83
|
+
setLoading(true);
|
|
84
|
+
clearError();
|
|
85
|
+
const actionCodeSettings = {
|
|
86
|
+
url: `${window.location.origin}/login`,
|
|
87
|
+
handleCodeInApp: true,
|
|
88
|
+
};
|
|
89
|
+
await authService.sendSignInLinkToEmail(email, actionCodeSettings);
|
|
90
|
+
setLoading(false);
|
|
91
|
+
} catch (err: unknown) {
|
|
92
|
+
setLoading(false);
|
|
93
|
+
setError((err as { message?: string }).message ?? 'Failed to send sign-in link');
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const signInWithEmailLink = async (email: string, emailLink: string) => {
|
|
98
|
+
try {
|
|
99
|
+
setLoading(true);
|
|
100
|
+
clearError();
|
|
101
|
+
await authService.signInWithEmailLink(email, emailLink);
|
|
102
|
+
} catch (err: unknown) {
|
|
103
|
+
setLoading(false);
|
|
104
|
+
setError((err as { message?: string }).message ?? 'Email link sign-in failed');
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const isSignInWithEmailLink = (emailLink: string): boolean => {
|
|
109
|
+
return authService.isSignInWithEmailLink(emailLink);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const signOut = async () => {
|
|
113
|
+
try {
|
|
114
|
+
setLoading(true);
|
|
115
|
+
await authService.signOut();
|
|
116
|
+
setUser(null);
|
|
117
|
+
setLoading(false);
|
|
118
|
+
} catch (err: unknown) {
|
|
119
|
+
setLoading(false);
|
|
120
|
+
setError((err as { message?: string }).message ?? 'Sign-out failed');
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const value: AuthContextType = {
|
|
125
|
+
user,
|
|
126
|
+
loading,
|
|
127
|
+
error,
|
|
128
|
+
signInWithGoogle,
|
|
129
|
+
signOut,
|
|
130
|
+
signInWithEmail,
|
|
131
|
+
signUpWithEmail,
|
|
132
|
+
sendSignInLinkToEmail,
|
|
133
|
+
signInWithEmailLink,
|
|
134
|
+
isSignInWithEmailLink,
|
|
135
|
+
clearError,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
139
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
signInWithPopup,
|
|
3
|
+
GoogleAuthProvider,
|
|
4
|
+
signInWithEmailAndPassword,
|
|
5
|
+
createUserWithEmailAndPassword,
|
|
6
|
+
updateProfile,
|
|
7
|
+
signOut as firebaseSignOut,
|
|
8
|
+
sendSignInLinkToEmail as firebaseSendSignInLinkToEmail,
|
|
9
|
+
isSignInWithEmailLink as firebaseIsSignInWithEmailLink,
|
|
10
|
+
signInWithEmailLink as firebaseSignInWithEmailLink,
|
|
11
|
+
ActionCodeSettings,
|
|
12
|
+
} from 'firebase/auth';
|
|
13
|
+
import { auth } from '../../config/firebase';
|
|
14
|
+
|
|
15
|
+
const googleProvider = new GoogleAuthProvider();
|
|
16
|
+
|
|
17
|
+
export const authService = {
|
|
18
|
+
// Google Sign In
|
|
19
|
+
signInWithGoogle: async () => {
|
|
20
|
+
try {
|
|
21
|
+
const result = await signInWithPopup(auth, googleProvider);
|
|
22
|
+
return result.user;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// Email/Password Sign In
|
|
29
|
+
signInWithEmail: async (email: string, password: string) => {
|
|
30
|
+
try {
|
|
31
|
+
const result = await signInWithEmailAndPassword(auth, email, password);
|
|
32
|
+
return result.user;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Email/Password Sign Up
|
|
39
|
+
signUpWithEmail: async (email: string, password: string, displayName?: string) => {
|
|
40
|
+
try {
|
|
41
|
+
const result = await createUserWithEmailAndPassword(auth, email, password);
|
|
42
|
+
|
|
43
|
+
if (displayName) {
|
|
44
|
+
await updateProfile(result.user, { displayName });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result.user;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Sign Out
|
|
54
|
+
signOut: async () => {
|
|
55
|
+
try {
|
|
56
|
+
await firebaseSignOut(auth);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Email Link Authentication
|
|
63
|
+
sendSignInLinkToEmail: async (email: string, actionCodeSettings: ActionCodeSettings) => {
|
|
64
|
+
try {
|
|
65
|
+
await firebaseSendSignInLinkToEmail(auth, email, actionCodeSettings);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
isSignInWithEmailLink: (emailLink: string): boolean => {
|
|
72
|
+
return firebaseIsSignInWithEmailLink(auth, emailLink);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
signInWithEmailLink: async (email: string, emailLink: string) => {
|
|
76
|
+
try {
|
|
77
|
+
const result = await firebaseSignInWithEmailLink(auth, email, emailLink);
|
|
78
|
+
return result.user;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
package/shells/almadar-shell/almadar-shell/packages/client/src/features/auth/components/Login.tsx
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useAuthContext } from '../AuthContext';
|
|
3
|
+
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
const EMAIL_FOR_SIGN_IN_KEY = 'emailForSignIn';
|
|
6
|
+
|
|
7
|
+
const Login: React.FC = () => {
|
|
8
|
+
const [email, setEmail] = useState('');
|
|
9
|
+
const [password, setPassword] = useState('');
|
|
10
|
+
const [isSignUp, setIsSignUp] = useState(false);
|
|
11
|
+
const [displayName, setDisplayName] = useState('');
|
|
12
|
+
const [completingEmailLink, setCompletingEmailLink] = useState(false);
|
|
13
|
+
const [emailForLinkCompletion, setEmailForLinkCompletion] = useState('');
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
loading,
|
|
17
|
+
error,
|
|
18
|
+
signInWithGoogle,
|
|
19
|
+
signInWithEmail,
|
|
20
|
+
signUpWithEmail,
|
|
21
|
+
signInWithEmailLink,
|
|
22
|
+
isSignInWithEmailLink,
|
|
23
|
+
clearError,
|
|
24
|
+
} = useAuthContext();
|
|
25
|
+
const navigate = useNavigate();
|
|
26
|
+
const [searchParams] = useSearchParams();
|
|
27
|
+
|
|
28
|
+
// Redirect on successful auth
|
|
29
|
+
const { user } = useAuthContext();
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (user) {
|
|
32
|
+
const returnUrl = searchParams.get('returnUrl') || '/';
|
|
33
|
+
navigate(returnUrl, { replace: true });
|
|
34
|
+
}
|
|
35
|
+
}, [user, navigate, searchParams]);
|
|
36
|
+
|
|
37
|
+
// Check if user is returning from email link
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (isSignInWithEmailLink(window.location.href)) {
|
|
40
|
+
const emailForSignIn = window.localStorage.getItem(EMAIL_FOR_SIGN_IN_KEY);
|
|
41
|
+
|
|
42
|
+
if (emailForSignIn) {
|
|
43
|
+
setCompletingEmailLink(true);
|
|
44
|
+
signInWithEmailLink(emailForSignIn, window.location.href)
|
|
45
|
+
.then(() => {
|
|
46
|
+
window.localStorage.removeItem(EMAIL_FOR_SIGN_IN_KEY);
|
|
47
|
+
window.history.replaceState({}, document.title, '/login');
|
|
48
|
+
setCompletingEmailLink(false);
|
|
49
|
+
})
|
|
50
|
+
.catch(() => {
|
|
51
|
+
setCompletingEmailLink(false);
|
|
52
|
+
setEmailForLinkCompletion(emailForSignIn);
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
setCompletingEmailLink(true);
|
|
56
|
+
setEmailForLinkCompletion('');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}, [signInWithEmailLink, isSignInWithEmailLink]);
|
|
60
|
+
|
|
61
|
+
const handleGoogleSignIn = async () => {
|
|
62
|
+
clearError();
|
|
63
|
+
await signInWithGoogle();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleEmailAuth = async (e: React.FormEvent) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
clearError();
|
|
69
|
+
if (isSignUp) {
|
|
70
|
+
await signUpWithEmail(email, password, displayName);
|
|
71
|
+
} else {
|
|
72
|
+
await signInWithEmail(email, password);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleCompleteEmailLink = async (e: React.FormEvent) => {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
if (!emailForLinkCompletion.trim()) return;
|
|
79
|
+
|
|
80
|
+
await signInWithEmailLink(emailForLinkCompletion, window.location.href);
|
|
81
|
+
window.localStorage.removeItem(EMAIL_FOR_SIGN_IN_KEY);
|
|
82
|
+
window.history.replaceState({}, document.title, '/login');
|
|
83
|
+
setCompletingEmailLink(false);
|
|
84
|
+
setEmailForLinkCompletion('');
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
|
|
89
|
+
<div className="max-w-md w-full space-y-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-8 shadow-lg">
|
|
90
|
+
<div>
|
|
91
|
+
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-gray-100">
|
|
92
|
+
{completingEmailLink
|
|
93
|
+
? 'Complete sign-in'
|
|
94
|
+
: isSignUp
|
|
95
|
+
? 'Create your account'
|
|
96
|
+
: 'Sign in to your account'}
|
|
97
|
+
</h2>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="mt-8 space-y-6">
|
|
101
|
+
{completingEmailLink && (
|
|
102
|
+
<form onSubmit={handleCompleteEmailLink} className="space-y-4">
|
|
103
|
+
<div>
|
|
104
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
|
105
|
+
Please enter your email address to complete sign-in.
|
|
106
|
+
</p>
|
|
107
|
+
<input
|
|
108
|
+
type="email"
|
|
109
|
+
autoComplete="email"
|
|
110
|
+
required
|
|
111
|
+
value={emailForLinkCompletion}
|
|
112
|
+
onChange={(e) => setEmailForLinkCompletion(e.target.value)}
|
|
113
|
+
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
114
|
+
placeholder="Enter your email"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{error && (
|
|
119
|
+
<div className="text-red-600 dark:text-red-300 text-sm text-center bg-red-50 dark:bg-red-900/20 p-3 rounded-md border border-red-200 dark:border-red-800">
|
|
120
|
+
{error}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
<button
|
|
125
|
+
type="submit"
|
|
126
|
+
disabled={loading || !emailForLinkCompletion.trim()}
|
|
127
|
+
className="w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
|
128
|
+
>
|
|
129
|
+
{loading ? 'Signing in...' : 'Complete sign-in'}
|
|
130
|
+
</button>
|
|
131
|
+
</form>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{!completingEmailLink && (
|
|
135
|
+
<>
|
|
136
|
+
<button
|
|
137
|
+
onClick={handleGoogleSignIn}
|
|
138
|
+
disabled={loading}
|
|
139
|
+
className="w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
|
140
|
+
>
|
|
141
|
+
{loading ? 'Signing in...' : 'Sign in with Google'}
|
|
142
|
+
</button>
|
|
143
|
+
|
|
144
|
+
<div className="relative">
|
|
145
|
+
<div className="absolute inset-0 flex items-center">
|
|
146
|
+
<div className="w-full border-t border-gray-300 dark:border-gray-700" />
|
|
147
|
+
</div>
|
|
148
|
+
<div className="relative flex justify-center text-sm">
|
|
149
|
+
<span className="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400">
|
|
150
|
+
Or sign in with email
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<form className="space-y-4" onSubmit={handleEmailAuth}>
|
|
156
|
+
{isSignUp && (
|
|
157
|
+
<input
|
|
158
|
+
type="text"
|
|
159
|
+
required
|
|
160
|
+
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
161
|
+
placeholder="Display Name"
|
|
162
|
+
value={displayName}
|
|
163
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
<input
|
|
168
|
+
type="email"
|
|
169
|
+
autoComplete="email"
|
|
170
|
+
required
|
|
171
|
+
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
172
|
+
placeholder="Email address"
|
|
173
|
+
value={email}
|
|
174
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
175
|
+
/>
|
|
176
|
+
|
|
177
|
+
<input
|
|
178
|
+
type="password"
|
|
179
|
+
autoComplete={isSignUp ? 'new-password' : 'current-password'}
|
|
180
|
+
required
|
|
181
|
+
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
182
|
+
placeholder="Password"
|
|
183
|
+
value={password}
|
|
184
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
185
|
+
/>
|
|
186
|
+
|
|
187
|
+
{error && (
|
|
188
|
+
<div className="text-red-600 dark:text-red-300 text-sm text-center bg-red-50 dark:bg-red-900/20 p-3 rounded-md border border-red-200 dark:border-red-800">
|
|
189
|
+
{error}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
<button
|
|
194
|
+
type="submit"
|
|
195
|
+
disabled={loading}
|
|
196
|
+
className="w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
|
197
|
+
>
|
|
198
|
+
{loading ? 'Processing...' : isSignUp ? 'Sign Up' : 'Sign In'}
|
|
199
|
+
</button>
|
|
200
|
+
</form>
|
|
201
|
+
|
|
202
|
+
<div className="text-center">
|
|
203
|
+
<button
|
|
204
|
+
onClick={() => setIsSignUp(!isSignUp)}
|
|
205
|
+
className="text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 font-medium text-sm"
|
|
206
|
+
>
|
|
207
|
+
{isSignUp ? 'Already have an account? Sign in' : "Don't have an account? Sign up"}
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export default Login;
|