@ma7moudsalama/falak-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +378 -0
  2. package/bin/falak.js +157 -0
  3. package/index.js +5 -0
  4. package/lib/scaffold.js +23 -0
  5. package/package.json +46 -0
  6. package/template/_env.example +34 -0
  7. package/template/_gitignore +8 -0
  8. package/template/firebase-rules.json +36 -0
  9. package/template/index.html +21 -0
  10. package/template/package.json +36 -0
  11. package/template/postcss.config.js +6 -0
  12. package/template/public/favicon.svg +5 -0
  13. package/template/src/App.vue +95 -0
  14. package/template/src/assets/main.css +100 -0
  15. package/template/src/components/layout/AppLayout.vue +163 -0
  16. package/template/src/composables/useAuth.js +393 -0
  17. package/template/src/composables/useCrypto.js +153 -0
  18. package/template/src/composables/useDatabase.js +341 -0
  19. package/template/src/composables/useGroq.js +237 -0
  20. package/template/src/composables/usePaymob.js +392 -0
  21. package/template/src/firebase/index.js +87 -0
  22. package/template/src/i18n/index.js +66 -0
  23. package/template/src/i18n/locales/ar.json +121 -0
  24. package/template/src/i18n/locales/en.json +121 -0
  25. package/template/src/main.js +59 -0
  26. package/template/src/router/index.js +127 -0
  27. package/template/src/stores/auth.js +14 -0
  28. package/template/src/views/AdminView.vue +67 -0
  29. package/template/src/views/DashboardView.vue +253 -0
  30. package/template/src/views/HomeView.vue +13 -0
  31. package/template/src/views/NotFoundView.vue +8 -0
  32. package/template/src/views/ProfileView.vue +134 -0
  33. package/template/src/views/auth/ForgotView.vue +57 -0
  34. package/template/src/views/auth/LoginView.vue +169 -0
  35. package/template/src/views/auth/RegisterView.vue +103 -0
  36. package/template/tailwind.config.js +41 -0
  37. package/template/vite.config.js +29 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * useAuth — Firebase Authentication Composable
3
+ * ──────────────────────────────────────────────
4
+ * Supports:
5
+ * • Email + Password (register / login / reset)
6
+ * • Google OAuth
7
+ * • Facebook OAuth
8
+ * • User roles stored in RTDB: /users/{uid}/role
9
+ * • Session data mirrored to RTDB: /sessions/{uid}
10
+ * • Reactive currentUser, userRole, isAuthenticated
11
+ */
12
+
13
+ import { ref, computed, readonly } from 'vue'
14
+ import {
15
+ createUserWithEmailAndPassword,
16
+ signInWithEmailAndPassword,
17
+ signOut,
18
+ sendPasswordResetEmail,
19
+ sendEmailVerification,
20
+ updateProfile,
21
+ updatePassword,
22
+ reauthenticateWithCredential,
23
+ EmailAuthProvider,
24
+ GoogleAuthProvider,
25
+ FacebookAuthProvider,
26
+ signInWithPopup,
27
+ signInWithRedirect,
28
+ getRedirectResult,
29
+ onAuthStateChanged,
30
+ deleteUser
31
+ } from 'firebase/auth'
32
+ import {
33
+ ref as dbRef,
34
+ set,
35
+ get,
36
+ update,
37
+ onValue,
38
+ serverTimestamp,
39
+ onDisconnect,
40
+ remove
41
+ } from 'firebase/database'
42
+ import { auth, rtdb } from '@/firebase/index.js'
43
+ import { useCrypto } from '@/composables/useCrypto.js'
44
+
45
+ // ── Singleton state (shared across all composable calls) ──
46
+ const currentUser = ref(null)
47
+ const userProfile = ref(null)
48
+ const userRole = ref(null)
49
+ const isAuthReady = ref(false)
50
+ const authError = ref(null)
51
+ const isLoading = ref(false)
52
+
53
+ let roleListener = null
54
+ let sessionRef = null
55
+
56
+ // ── Roles ──────────────────────────────────────
57
+ export const ROLES = {
58
+ SUPER_ADMIN: 'super_admin',
59
+ ADMIN: 'admin',
60
+ USER: 'user',
61
+ GUEST: 'guest'
62
+ }
63
+
64
+ // ── Role permissions map ───────────────────────
65
+ const ROLE_PERMISSIONS = {
66
+ super_admin: ['read', 'write', 'delete', 'manage_users', 'manage_roles'],
67
+ admin: ['read', 'write', 'delete', 'manage_users'],
68
+ user: ['read', 'write'],
69
+ guest: ['read']
70
+ }
71
+
72
+ // ── Initialize Auth State Listener ────────────
73
+ function initAuthListener() {
74
+ onAuthStateChanged(auth, async (user) => {
75
+ if (user) {
76
+ currentUser.value = user
77
+ await syncUserProfile(user)
78
+ listenToRole(user.uid)
79
+ await updateSession(user.uid, true)
80
+ } else {
81
+ if (sessionRef) {
82
+ await updateSession(currentUser.value?.uid, false).catch(() => {})
83
+ }
84
+ currentUser.value = null
85
+ userProfile.value = null
86
+ userRole.value = null
87
+ if (roleListener) { roleListener(); roleListener = null }
88
+ }
89
+ isAuthReady.value = true
90
+ })
91
+ }
92
+
93
+ // ── Sync user profile from RTDB ───────────────
94
+ async function syncUserProfile(user) {
95
+ const snap = await get(dbRef(rtdb, `users/${user.uid}`))
96
+ if (snap.exists()) {
97
+ userProfile.value = snap.val()
98
+ } else {
99
+ // First-time: create profile
100
+ const profile = {
101
+ uid: user.uid,
102
+ email: user.email,
103
+ displayName: user.displayName || '',
104
+ photoURL: user.photoURL || '',
105
+ role: ROLES.USER,
106
+ createdAt: serverTimestamp(),
107
+ provider: user.providerData[0]?.providerId || 'email'
108
+ }
109
+ await set(dbRef(rtdb, `users/${user.uid}`), profile)
110
+ userProfile.value = profile
111
+ }
112
+ }
113
+
114
+ // ── Listen to role changes in real-time ───────
115
+ function listenToRole(uid) {
116
+ if (roleListener) roleListener()
117
+ const roleDbRef = dbRef(rtdb, `users/${uid}/role`)
118
+ roleListener = onValue(roleDbRef, (snap) => {
119
+ userRole.value = snap.val() || ROLES.USER
120
+ })
121
+ }
122
+
123
+ // ── Session management in RTDB ─────────────────
124
+ async function updateSession(uid, online) {
125
+ if (!uid) return
126
+ sessionRef = dbRef(rtdb, `sessions/${uid}`)
127
+ const data = {
128
+ online,
129
+ lastSeen: serverTimestamp(),
130
+ userAgent: navigator.userAgent,
131
+ ...(online ? { loginAt: serverTimestamp() } : {})
132
+ }
133
+ await set(sessionRef, data)
134
+ // Auto-mark offline on disconnect
135
+ if (online) {
136
+ onDisconnect(sessionRef).update({ online: false, lastSeen: serverTimestamp() })
137
+ }
138
+ }
139
+
140
+ // ── Initialize on first import ─────────────────
141
+ initAuthListener()
142
+
143
+ // ── Composable ────────────────────────────────
144
+ export function useAuth() {
145
+ const { encrypt } = useCrypto()
146
+
147
+ // ── Computed ──────────────────────────────
148
+ const isAuthenticated = computed(() => !!currentUser.value)
149
+ const isAdmin = computed(() => [ROLES.ADMIN, ROLES.SUPER_ADMIN].includes(userRole.value))
150
+ const isSuperAdmin = computed(() => userRole.value === ROLES.SUPER_ADMIN)
151
+ const userPermissions = computed(() => ROLE_PERMISSIONS[userRole.value] || [])
152
+
153
+ function hasPermission(permission) {
154
+ return userPermissions.value.includes(permission)
155
+ }
156
+
157
+ function hasRole(...roles) {
158
+ return roles.includes(userRole.value)
159
+ }
160
+
161
+ // ── Email / Password ──────────────────────
162
+ async function register({ email, password, displayName }) {
163
+ isLoading.value = true
164
+ authError.value = null
165
+ try {
166
+ const cred = await createUserWithEmailAndPassword(auth, email, password)
167
+ if (displayName) {
168
+ await updateProfile(cred.user, { displayName })
169
+ }
170
+ await sendEmailVerification(cred.user)
171
+ return { success: true, user: cred.user }
172
+ } catch (err) {
173
+ authError.value = formatAuthError(err)
174
+ return { success: false, error: authError.value }
175
+ } finally {
176
+ isLoading.value = false
177
+ }
178
+ }
179
+
180
+ async function login({ email, password }) {
181
+ isLoading.value = true
182
+ authError.value = null
183
+ try {
184
+ const cred = await signInWithEmailAndPassword(auth, email, password)
185
+ return { success: true, user: cred.user }
186
+ } catch (err) {
187
+ authError.value = formatAuthError(err)
188
+ return { success: false, error: authError.value }
189
+ } finally {
190
+ isLoading.value = false
191
+ }
192
+ }
193
+
194
+ async function resetPassword(email) {
195
+ isLoading.value = true
196
+ authError.value = null
197
+ try {
198
+ await sendPasswordResetEmail(auth, email)
199
+ return { success: true }
200
+ } catch (err) {
201
+ authError.value = formatAuthError(err)
202
+ return { success: false, error: authError.value }
203
+ } finally {
204
+ isLoading.value = false
205
+ }
206
+ }
207
+
208
+ async function changePassword({ currentPassword, newPassword }) {
209
+ isLoading.value = true
210
+ authError.value = null
211
+ try {
212
+ const credential = EmailAuthProvider.credential(
213
+ currentUser.value.email,
214
+ currentPassword
215
+ )
216
+ await reauthenticateWithCredential(currentUser.value, credential)
217
+ await updatePassword(currentUser.value, newPassword)
218
+ return { success: true }
219
+ } catch (err) {
220
+ authError.value = formatAuthError(err)
221
+ return { success: false, error: authError.value }
222
+ } finally {
223
+ isLoading.value = false
224
+ }
225
+ }
226
+
227
+ // ── Google OAuth ──────────────────────────
228
+ async function loginWithGoogle(useRedirect = false) {
229
+ isLoading.value = true
230
+ authError.value = null
231
+ const provider = new GoogleAuthProvider()
232
+ provider.addScope('profile')
233
+ provider.addScope('email')
234
+ try {
235
+ let cred
236
+ if (useRedirect) {
237
+ await signInWithRedirect(auth, provider)
238
+ cred = await getRedirectResult(auth)
239
+ } else {
240
+ cred = await signInWithPopup(auth, provider)
241
+ }
242
+ return { success: true, user: cred?.user }
243
+ } catch (err) {
244
+ authError.value = formatAuthError(err)
245
+ return { success: false, error: authError.value }
246
+ } finally {
247
+ isLoading.value = false
248
+ }
249
+ }
250
+
251
+ // ── Facebook OAuth ────────────────────────
252
+ async function loginWithFacebook(useRedirect = false) {
253
+ isLoading.value = true
254
+ authError.value = null
255
+ const provider = new FacebookAuthProvider()
256
+ provider.addScope('email')
257
+ provider.addScope('public_profile')
258
+ try {
259
+ let cred
260
+ if (useRedirect) {
261
+ await signInWithRedirect(auth, provider)
262
+ cred = await getRedirectResult(auth)
263
+ } else {
264
+ cred = await signInWithPopup(auth, provider)
265
+ }
266
+ return { success: true, user: cred?.user }
267
+ } catch (err) {
268
+ authError.value = formatAuthError(err)
269
+ return { success: false, error: authError.value }
270
+ } finally {
271
+ isLoading.value = false
272
+ }
273
+ }
274
+
275
+ // ── Sign Out ──────────────────────────────
276
+ async function logout() {
277
+ isLoading.value = true
278
+ try {
279
+ if (currentUser.value) {
280
+ await updateSession(currentUser.value.uid, false)
281
+ }
282
+ await signOut(auth)
283
+ return { success: true }
284
+ } catch (err) {
285
+ authError.value = formatAuthError(err)
286
+ return { success: false, error: authError.value }
287
+ } finally {
288
+ isLoading.value = false
289
+ }
290
+ }
291
+
292
+ // ── Update Profile ────────────────────────
293
+ async function updateUserProfile(data) {
294
+ isLoading.value = true
295
+ try {
296
+ if (data.displayName || data.photoURL) {
297
+ await updateProfile(currentUser.value, {
298
+ displayName: data.displayName,
299
+ photoURL: data.photoURL
300
+ })
301
+ }
302
+ // Persist extra fields to RTDB
303
+ await update(dbRef(rtdb, `users/${currentUser.value.uid}`), {
304
+ ...data,
305
+ updatedAt: serverTimestamp()
306
+ })
307
+ userProfile.value = { ...userProfile.value, ...data }
308
+ return { success: true }
309
+ } catch (err) {
310
+ return { success: false, error: err.message }
311
+ } finally {
312
+ isLoading.value = false
313
+ }
314
+ }
315
+
316
+ // ── Admin: Set Role ───────────────────────
317
+ async function setUserRole(uid, role) {
318
+ if (!isSuperAdmin.value && !isAdmin.value) {
319
+ return { success: false, error: 'Insufficient permissions' }
320
+ }
321
+ if (!Object.values(ROLES).includes(role)) {
322
+ return { success: false, error: `Invalid role: ${role}` }
323
+ }
324
+ try {
325
+ await update(dbRef(rtdb, `users/${uid}`), { role })
326
+ return { success: true }
327
+ } catch (err) {
328
+ return { success: false, error: err.message }
329
+ }
330
+ }
331
+
332
+ // ── Admin: Delete User ────────────────────
333
+ async function deleteAccount() {
334
+ try {
335
+ const uid = currentUser.value.uid
336
+ await remove(dbRef(rtdb, `users/${uid}`))
337
+ await remove(dbRef(rtdb, `sessions/${uid}`))
338
+ await deleteUser(currentUser.value)
339
+ return { success: true }
340
+ } catch (err) {
341
+ return { success: false, error: err.message }
342
+ }
343
+ }
344
+
345
+ // ── Auth Error Formatter ──────────────────
346
+ function formatAuthError(err) {
347
+ const map = {
348
+ 'auth/user-not-found': 'No account found with this email.',
349
+ 'auth/wrong-password': 'Incorrect password.',
350
+ 'auth/email-already-in-use': 'This email is already registered.',
351
+ 'auth/weak-password': 'Password must be at least 6 characters.',
352
+ 'auth/invalid-email': 'Invalid email address.',
353
+ 'auth/popup-closed-by-user': 'Sign-in popup was closed.',
354
+ 'auth/account-exists-with-different-credential':
355
+ 'An account already exists with this email. Try another login method.',
356
+ 'auth/too-many-requests': 'Too many attempts. Please try again later.',
357
+ 'auth/network-request-failed': 'Network error. Check your connection.',
358
+ 'auth/requires-recent-login': 'Please log in again to perform this action.'
359
+ }
360
+ return map[err.code] || err.message
361
+ }
362
+
363
+ return {
364
+ // State (readonly to prevent external mutation)
365
+ currentUser: readonly(currentUser),
366
+ userProfile: readonly(userProfile),
367
+ userRole: readonly(userRole),
368
+ isAuthReady: readonly(isAuthReady),
369
+ authError: readonly(authError),
370
+ isLoading: readonly(isLoading),
371
+
372
+ // Computed
373
+ isAuthenticated,
374
+ isAdmin,
375
+ isSuperAdmin,
376
+ userPermissions,
377
+
378
+ // Methods
379
+ register,
380
+ login,
381
+ logout,
382
+ resetPassword,
383
+ changePassword,
384
+ loginWithGoogle,
385
+ loginWithFacebook,
386
+ updateUserProfile,
387
+ setUserRole,
388
+ deleteAccount,
389
+ hasPermission,
390
+ hasRole,
391
+ ROLES
392
+ }
393
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * useCrypto — Encryption & Decryption Composable
3
+ * ────────────────────────────────────────────────
4
+ * Uses AES-256 via crypto-js.
5
+ * Encrypt sensitive data before storing in Firebase or localStorage.
6
+ *
7
+ * Usage:
8
+ * const { encrypt, decrypt, encryptObject, decryptObject, hash } = useCrypto()
9
+ * const cipher = encrypt('hello world')
10
+ * const plain = decrypt(cipher) // → 'hello world'
11
+ */
12
+
13
+ import CryptoJS from 'crypto-js'
14
+
15
+ const SECRET = import.meta.env.VITE_ENCRYPTION_KEY || 'fallback_dev_key_change_me'
16
+
17
+ // ── Derive a key from the secret (PBKDF2) ─────
18
+ function deriveKey(salt = 'falak_salt') {
19
+ return CryptoJS.PBKDF2(SECRET, salt, { keySize: 256 / 32, iterations: 1000 })
20
+ }
21
+
22
+ export function useCrypto() {
23
+ /**
24
+ * Encrypt a string value.
25
+ * Returns a base64-encoded string prefixed with the IV.
26
+ */
27
+ function encrypt(plaintext, customKey = null) {
28
+ if (plaintext === null || plaintext === undefined) return plaintext
29
+ try {
30
+ const key = customKey ? CryptoJS.enc.Utf8.parse(customKey) : deriveKey()
31
+ const iv = CryptoJS.lib.WordArray.random(128 / 8)
32
+ const encrypted = CryptoJS.AES.encrypt(String(plaintext), key, {
33
+ iv,
34
+ mode: CryptoJS.mode.CBC,
35
+ padding: CryptoJS.pad.Pkcs7
36
+ })
37
+ // Prepend IV so decrypt can use it
38
+ const combined = iv.toString(CryptoJS.enc.Hex) + ':' + encrypted.toString()
39
+ return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(combined))
40
+ } catch (err) {
41
+ console.error('[useCrypto] encrypt error:', err)
42
+ return null
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Decrypt a value encrypted with encrypt().
48
+ */
49
+ function decrypt(ciphertext, customKey = null) {
50
+ if (!ciphertext) return ciphertext
51
+ try {
52
+ const decoded = CryptoJS.enc.Base64.parse(ciphertext).toString(CryptoJS.enc.Utf8)
53
+ const [ivHex, encryptedStr] = decoded.split(':')
54
+ const iv = CryptoJS.enc.Hex.parse(ivHex)
55
+ const key = customKey ? CryptoJS.enc.Utf8.parse(customKey) : deriveKey()
56
+ const decrypted = CryptoJS.AES.decrypt(encryptedStr, key, {
57
+ iv,
58
+ mode: CryptoJS.mode.CBC,
59
+ padding: CryptoJS.pad.Pkcs7
60
+ })
61
+ return decrypted.toString(CryptoJS.enc.Utf8)
62
+ } catch (err) {
63
+ console.error('[useCrypto] decrypt error:', err)
64
+ return null
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Encrypt a plain JS object → JSON string → encrypted.
70
+ */
71
+ function encryptObject(obj, customKey = null) {
72
+ if (!obj) return null
73
+ return encrypt(JSON.stringify(obj), customKey)
74
+ }
75
+
76
+ /**
77
+ * Decrypt back to a JS object.
78
+ */
79
+ function decryptObject(ciphertext, customKey = null) {
80
+ const str = decrypt(ciphertext, customKey)
81
+ if (!str) return null
82
+ try {
83
+ return JSON.parse(str)
84
+ } catch {
85
+ return null
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Encrypt only specified fields of an object, leave others plain.
91
+ * Useful for partial encryption of Firebase records.
92
+ *
93
+ * @param {Object} obj - source object
94
+ * @param {string[]} fields - keys to encrypt
95
+ */
96
+ function encryptFields(obj, fields = []) {
97
+ if (!obj) return obj
98
+ const result = { ...obj }
99
+ for (const field of fields) {
100
+ if (result[field] !== undefined) {
101
+ result[field] = encrypt(String(result[field]))
102
+ }
103
+ }
104
+ return result
105
+ }
106
+
107
+ /**
108
+ * Decrypt only specified fields.
109
+ */
110
+ function decryptFields(obj, fields = []) {
111
+ if (!obj) return obj
112
+ const result = { ...obj }
113
+ for (const field of fields) {
114
+ if (result[field]) {
115
+ result[field] = decrypt(result[field])
116
+ }
117
+ }
118
+ return result
119
+ }
120
+
121
+ /**
122
+ * One-way SHA256 hash (for passwords, tokens, etc.)
123
+ */
124
+ function hash(value) {
125
+ return CryptoJS.SHA256(String(value)).toString(CryptoJS.enc.Hex)
126
+ }
127
+
128
+ /**
129
+ * HMAC-SHA256 signature (used by Paymob, etc.)
130
+ */
131
+ function hmac(message, secret = SECRET) {
132
+ return CryptoJS.HmacSHA256(String(message), secret).toString(CryptoJS.enc.Hex)
133
+ }
134
+
135
+ /**
136
+ * Generate a random secure token (hex string).
137
+ */
138
+ function randomToken(bytes = 32) {
139
+ return CryptoJS.lib.WordArray.random(bytes).toString(CryptoJS.enc.Hex)
140
+ }
141
+
142
+ return {
143
+ encrypt,
144
+ decrypt,
145
+ encryptObject,
146
+ decryptObject,
147
+ encryptFields,
148
+ decryptFields,
149
+ hash,
150
+ hmac,
151
+ randomToken
152
+ }
153
+ }