postbase 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yml +74 -0
- package/CLA.md +60 -0
- package/CONTRIBUTORS.md +35 -0
- package/LICENSE +661 -0
- package/README.md +211 -0
- package/admin/404.html +33 -0
- package/admin/README.md +21 -0
- package/admin/index.html +15 -0
- package/admin/jsconfig.json +20 -0
- package/admin/lib/postbase.js +222 -0
- package/admin/package-lock.json +3746 -0
- package/admin/package.json +27 -0
- package/admin/public/assets/img/admin-ui.png +0 -0
- package/admin/public/assets/img/blank-profile-picture-960_720.webp +0 -0
- package/admin/public/assets/img/chart-active-users.png +0 -0
- package/admin/public/assets/img/icon-transparent.png +0 -0
- package/admin/src/App.jsx +48 -0
- package/admin/src/auth.js +11 -0
- package/admin/src/common/formatDateTime.js +18 -0
- package/admin/src/components/AuthPanel.jsx +88 -0
- package/admin/src/components/Header.jsx +67 -0
- package/admin/src/main.jsx +6 -0
- package/admin/src/pages/Dashboard.jsx +24 -0
- package/admin/src/pages/Home.jsx +52 -0
- package/admin/src/pages/Login.jsx +10 -0
- package/admin/src/pages/authentication/Users.jsx +199 -0
- package/admin/src/pages/firestore/Database.jsx +29 -0
- package/admin/src/pages/storage/files.jsx +29 -0
- package/admin/src/postbase.js +15 -0
- package/admin/src/styles.css +3 -0
- package/admin/tailwind.config.cjs +11 -0
- package/admin/template.env +2 -0
- package/admin/vite.config.js +21 -0
- package/assets/img/HomePageScreenshot.png +0 -0
- package/assets/img/better-auth-logo-dark.136b122f.png +0 -0
- package/assets/img/better-auth-logo-light.4b03f444.png +0 -0
- package/assets/img/expresjs.png +0 -0
- package/assets/img/icon-transparent.png +0 -0
- package/assets/img/icon.png +0 -0
- package/assets/img/letsencrypt-logo-horizontal.png +0 -0
- package/assets/img/logo.png +0 -0
- package/assets/img/node.js_logo.png +0 -0
- package/assets/img/nodejsLight.svg +39 -0
- package/assets/img/postgres.png +0 -0
- package/backend/README.md +49 -0
- package/backend/admin/auth.js +9 -0
- package/backend/app.js +68 -0
- package/backend/auth.js +92 -0
- package/backend/env.js +12 -0
- package/backend/lib/postbase/adminClient.js +520 -0
- package/backend/lib/postbase/compat/admin.js +44 -0
- package/backend/lib/postbase/db.js +17 -0
- package/backend/lib/postbase/genericRouter.js +603 -0
- package/backend/lib/postbase/local-storage.js +56 -0
- package/backend/lib/postbase/metadataCache.js +32 -0
- package/backend/lib/postbase/middlewares/auth.js +57 -0
- package/backend/lib/postbase/migrations/1765239687559_rtdb-nodes.js +93 -0
- package/backend/lib/postbase/package-lock.json +5873 -0
- package/backend/lib/postbase/package.json +19 -0
- package/backend/lib/postbase/rtdb/router.js +190 -0
- package/backend/lib/postbase/rtdb/rulesEngine.js +63 -0
- package/backend/lib/postbase/rtdb/ws.js +84 -0
- package/backend/lib/postbase/rulesEngine.js +62 -0
- package/backend/lib/postbase/storage.js +130 -0
- package/backend/lib/postbase/tests/README.md +22 -0
- package/backend/lib/postbase/tests/db.js +9 -0
- package/backend/lib/postbase/tests/rtdb.rest.test.js +46 -0
- package/backend/lib/postbase/tests/rtdb.ws.test.js +113 -0
- package/backend/lib/postbase/tests/rules.js +26 -0
- package/backend/lib/postbase/tests/testServer.js +46 -0
- package/backend/lib/postbase/websocket.js +131 -0
- package/backend/local.js +6 -0
- package/backend/main.js +20 -0
- package/backend/middlewares/auth_middleware.js +10 -0
- package/backend/migrations/1762137399366-init.sql +98 -0
- package/backend/migrations/1762137399367_init_jsonb_schema.js +68 -0
- package/backend/migrations/1762149999999_enable_realtime_changes.js +48 -0
- package/backend/migrations/1765224247654_rtdb-nodes.js +93 -0
- package/backend/package-lock.json +2374 -0
- package/backend/package.json +27 -0
- package/backend/postbase_db_rules.js +128 -0
- package/backend/postbase_rtdb_rules.js +27 -0
- package/backend/postbase_storage_rules.js +45 -0
- package/backend/template.env +10 -0
- package/backend-systemd/README.md +39 -0
- package/backend-systemd/your_website.com.service +12 -0
- package/frontend/404.html +33 -0
- package/frontend/README.md +25 -0
- package/frontend/index.html +15 -0
- package/frontend/jsconfig.json +20 -0
- package/frontend/lib/postbase/auth.js +132 -0
- package/frontend/lib/postbase/compat/firebase/app.js +3 -0
- package/frontend/lib/postbase/compat/firebase/auth.js +115 -0
- package/frontend/lib/postbase/compat/firebase/database.js +11 -0
- package/frontend/lib/postbase/compat/firebase/firestore/lite.js +61 -0
- package/frontend/lib/postbase/compat/firebase/storage.js +10 -0
- package/frontend/lib/postbase/db.js +657 -0
- package/frontend/lib/postbase/package-lock.json +6284 -0
- package/frontend/lib/postbase/package.json +17 -0
- package/frontend/lib/postbase/rtdb.js +108 -0
- package/frontend/lib/postbase/storage.js +293 -0
- package/frontend/lib/postbase/tests/rtdb.client.test.js +88 -0
- package/frontend/lib/postbase/tests/waitFor.js +13 -0
- package/frontend/lib/postbase/utils.js +1 -0
- package/frontend/package-lock.json +2977 -0
- package/frontend/package.json +24 -0
- package/frontend/src/App.jsx +38 -0
- package/frontend/src/auth.js +52 -0
- package/frontend/src/components/AuthPanel.jsx +85 -0
- package/frontend/src/components/Header.jsx +54 -0
- package/frontend/src/main.jsx +5 -0
- package/frontend/src/pages/Dashboard.jsx +24 -0
- package/frontend/src/pages/Home.jsx +178 -0
- package/frontend/src/pages/Login.jsx +10 -0
- package/frontend/src/postbase.js +14 -0
- package/frontend/src/styles.css +1 -0
- package/frontend/tailwind.config.cjs +11 -0
- package/frontend/template.env +2 -0
- package/frontend/vite.config.js +18 -0
- package/git/hooks/README.md +31 -0
- package/git/hooks/post-receive +26 -0
- package/nginx/README.md +84 -0
- package/nginx/apt/www.your_website.com.conf +80 -0
- package/nginx/homebrew/www.your_website.com.conf +80 -0
- package/nginx/letsencrypt/README +14 -0
- package/package.json +8 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "postbase-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "Umair Ashraf",
|
|
6
|
+
"url": "http://github.com/umrashrf/postbase",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "nodemon local.js",
|
|
9
|
+
"migrate:up": "node-pg-migrate up",
|
|
10
|
+
"migrate:down": "node-pg-migrate down",
|
|
11
|
+
"migrate:redo": "node-pg-migrate redo",
|
|
12
|
+
"migrate:create": "node-pg-migrate create"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"better-auth": "^1.3.34",
|
|
16
|
+
"cors": "^2.8.5",
|
|
17
|
+
"dotenv": "^16.6.1",
|
|
18
|
+
"express": "^4.21.2",
|
|
19
|
+
"multer": "^2.0.2",
|
|
20
|
+
"pg": "^8.16.3",
|
|
21
|
+
"uuid": "^9.0.0",
|
|
22
|
+
"ws": "^8.18.3"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"node-pg-migrate": "^8.0.3"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// server/rules.js
|
|
2
|
+
import { RuleHelpers as H } from './lib/postbase/rulesEngine.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This ruleset mirrors your Firestore rules.
|
|
6
|
+
* Each table corresponds to a Firestore collection.
|
|
7
|
+
* All functions receive (request, resource)
|
|
8
|
+
* - request.auth may be null or { uid, ... }
|
|
9
|
+
* - request.resource.data equivalent → request.resource (on create)
|
|
10
|
+
* - resource.data → resource (for read/update/delete)
|
|
11
|
+
*/
|
|
12
|
+
export default {
|
|
13
|
+
tables: {
|
|
14
|
+
/** === USERS === */
|
|
15
|
+
users: {
|
|
16
|
+
// Allow read and update if the auth.id matches the userId (row id)
|
|
17
|
+
read: (req, res) => H.isAuth(req) && (req.auth.id === String(res.id) || req.user?.role === "admin"),
|
|
18
|
+
update: (req, res) => H.isAuth(req) && req.auth.id === String(res.id),
|
|
19
|
+
|
|
20
|
+
// Allow create if auth.id == resource.userId
|
|
21
|
+
create: (req, res) => H.isAuth(req) && req.auth.id === String(res.id),
|
|
22
|
+
|
|
23
|
+
// Delete not allowed
|
|
24
|
+
delete: () => false,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
/** === REVIEWS === */
|
|
28
|
+
reviews: {
|
|
29
|
+
// Everyone can read
|
|
30
|
+
read: () => true,
|
|
31
|
+
|
|
32
|
+
// Authenticated users can create reviews with proper fields and constraints
|
|
33
|
+
create: (req, res) => {
|
|
34
|
+
if (!H.isAuth(req)) return false;
|
|
35
|
+
const d = req.resource || {};
|
|
36
|
+
const isString = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
37
|
+
const isInt = (v) => Number.isInteger(v);
|
|
38
|
+
|
|
39
|
+
const valid =
|
|
40
|
+
isString(d.name) &&
|
|
41
|
+
isString(d.email) &&
|
|
42
|
+
isString(d.comment) &&
|
|
43
|
+
isInt(d.stars) &&
|
|
44
|
+
d.stars >= 1 &&
|
|
45
|
+
d.stars <= 5 &&
|
|
46
|
+
// check createdAt equals request.time: approximated by presence of field
|
|
47
|
+
!!d.createdAt;
|
|
48
|
+
|
|
49
|
+
return valid;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// No updates or deletes
|
|
53
|
+
update: () => false,
|
|
54
|
+
delete: () => false,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/** === DEV_REQUESTS === */
|
|
58
|
+
dev_requests: {
|
|
59
|
+
// Authenticated user can read their own requests
|
|
60
|
+
read: (req, res) =>
|
|
61
|
+
H.isAuth(req) && req.auth.id === String(res.user_id || res.userId),
|
|
62
|
+
|
|
63
|
+
// Authenticated user can create their own requests with valid data
|
|
64
|
+
create: (req, res) => {
|
|
65
|
+
if (!H.isAuth(req)) return false;
|
|
66
|
+
const d = req.resource || {};
|
|
67
|
+
const isString = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
req.auth.id === String(d.user_id || d.userId) &&
|
|
71
|
+
isString(d.name) &&
|
|
72
|
+
isString(d.email) &&
|
|
73
|
+
isString(d.description)
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// No updates or deletes
|
|
78
|
+
update: () => false,
|
|
79
|
+
delete: () => false,
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/** === API_KEYS === */
|
|
83
|
+
api_keys: {
|
|
84
|
+
// Read / update / delete allowed only for owner
|
|
85
|
+
read: (req, res) => {
|
|
86
|
+
if (!res) return H.isAuth(req);
|
|
87
|
+
return H.isAuth(req) && req.auth.id === String(res.user_id || res.userId);
|
|
88
|
+
},
|
|
89
|
+
update: (req, res) =>
|
|
90
|
+
H.isAuth(req) && req.auth.id === String(res.user_id || res.userId),
|
|
91
|
+
delete: (req, res) =>
|
|
92
|
+
H.isAuth(req) && req.auth.id === String(res.user_id || res.userId),
|
|
93
|
+
|
|
94
|
+
// Create: must be owner and valid key string with length >= 16
|
|
95
|
+
create: (req, res) => {
|
|
96
|
+
if (!H.isAuth(req)) return false;
|
|
97
|
+
const d = req.resource || {};
|
|
98
|
+
const isString = (v) => typeof v === 'string' && v.trim().length >= 16;
|
|
99
|
+
return (
|
|
100
|
+
req.auth.id === String(d.user_id || d.userId) &&
|
|
101
|
+
isString(d.key)
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/** === BILLING === */
|
|
107
|
+
billing: {
|
|
108
|
+
// Read and write allowed only for owner
|
|
109
|
+
read: (req, res) =>
|
|
110
|
+
H.isAuth(req) && req.auth.id === String(res.user_id || res.userId),
|
|
111
|
+
create: (req, res) =>
|
|
112
|
+
H.isAuth(req) && req.auth.id === String(res.user_id || res.userId),
|
|
113
|
+
update: (req, res) =>
|
|
114
|
+
H.isAuth(req) && req.auth.id === String(res.user_id || res.userId),
|
|
115
|
+
delete: (req, res) =>
|
|
116
|
+
H.isAuth(req) && req.auth.id === String(res.user_id || res.userId),
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/** === GLOBAL DEFAULTS === */
|
|
121
|
+
default: {
|
|
122
|
+
// Default deny everything (read/write false)
|
|
123
|
+
read: () => false,
|
|
124
|
+
create: () => false,
|
|
125
|
+
update: () => false,
|
|
126
|
+
delete: () => false,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// backend/postbase_rtdb_rules.js
|
|
2
|
+
export default {
|
|
3
|
+
paths: {
|
|
4
|
+
"/users/$uid": {
|
|
5
|
+
read: (req, ctx) =>
|
|
6
|
+
!!req.auth && req.auth.id === ctx.params.uid,
|
|
7
|
+
|
|
8
|
+
write: (req, ctx) =>
|
|
9
|
+
!!req.auth && req.auth.id === ctx.params.uid,
|
|
10
|
+
|
|
11
|
+
delete: (req, ctx) =>
|
|
12
|
+
!!req.auth && req.auth.id === ctx.params.uid,
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
"/public": {
|
|
16
|
+
read: true,
|
|
17
|
+
write: false,
|
|
18
|
+
delete: false,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
default: {
|
|
23
|
+
read: false,
|
|
24
|
+
write: false,
|
|
25
|
+
delete: false,
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// fileRules.js
|
|
2
|
+
import { RuleHelpers as H } from './lib/postbase/rulesEngine.js'; // adjust import path to your evaluator helpers
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
tables: {
|
|
6
|
+
files: {
|
|
7
|
+
// read: allowed if authenticated and auth.id equals owner OR auth.id in allowedUsers
|
|
8
|
+
read: (req, resource) => {
|
|
9
|
+
if (!resource) return false;
|
|
10
|
+
if (!H.isAuth(req)) return false;
|
|
11
|
+
const uid = req.auth.id;
|
|
12
|
+
if (!uid) return false;
|
|
13
|
+
if (resource.owner && String(resource.owner) === String(uid)) return true;
|
|
14
|
+
if (Array.isArray(resource.allowedUsers) && resource.allowedUsers.map(String).includes(String(uid))) return true;
|
|
15
|
+
return false;
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
// create: allow if auth.id equals incoming owner OR auth.id in incoming allowedUsers
|
|
19
|
+
create: (req, resource) => {
|
|
20
|
+
if (!H.isAuth(req)) return false;
|
|
21
|
+
const uid = req.auth.id;
|
|
22
|
+
if (!uid) return false;
|
|
23
|
+
// resource is the incoming metadata object: must specify owner or allowedUsers
|
|
24
|
+
if (resource.owner && String(resource.owner) === String(uid)) return true;
|
|
25
|
+
if (Array.isArray(resource.allowedUsers) && resource.allowedUsers.map(String).includes(String(uid))) return true;
|
|
26
|
+
return false;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// delete: only owner
|
|
30
|
+
delete: (req, resource) => {
|
|
31
|
+
if (!H.isAuth(req)) return false;
|
|
32
|
+
const uid = req.auth.id;
|
|
33
|
+
return resource && resource.owner && String(resource.owner) === String(uid);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// default denies everything
|
|
39
|
+
default: {
|
|
40
|
+
read: () => false,
|
|
41
|
+
create: () => false,
|
|
42
|
+
update: () => false,
|
|
43
|
+
delete: () => false
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
API_BASE=https://www.your_website.com/api
|
|
2
|
+
BETTER_AUTH_SECRET=
|
|
3
|
+
GOOGLE_CLIENT_ID=
|
|
4
|
+
GOOGLE_CLIENT_SECRET=
|
|
5
|
+
POSTBASE_BACKEND_HTTP_PORT=8081
|
|
6
|
+
POSTBASE_BACKEND_HTTPS_PORT=4431
|
|
7
|
+
LETSENCRYPT_CERT="/Users/your_username/your_website/nginx/letsencrypt/cert.pem"
|
|
8
|
+
LETSENCRYPT_KEY="/Users/your_username/your_website/nginx/letsencrypt/privkey.pem"
|
|
9
|
+
DATABASE_URL="postgresql://postgres:yoursecretpassword@127.0.0.1:5432/postgres"
|
|
10
|
+
ADMIN_USER_ID=
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Systemd for backend process
|
|
2
|
+
|
|
3
|
+
### Reload the systemd user configuration to recognize the new service:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
systemctl --user daemon-reload
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
### Start the service.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
systemctl --user start your_website.com.service
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Enable the service to start automatically on login:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
systemctl --user enable your_website.com.service
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Check the service status.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
systemctl --user status your_website.com.service
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Restart the service.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
systemctl --user restart your_website.com.service
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Lingering
|
|
34
|
+
|
|
35
|
+
### Lingering: If you want your user service to continue running even after you log out, you need to enable user lingering:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
loginctl enable-linger your_username
|
|
39
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Page Not Found</title>
|
|
7
|
+
|
|
8
|
+
<style media="screen">
|
|
9
|
+
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
|
|
10
|
+
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
|
|
11
|
+
#message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
|
|
12
|
+
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
|
|
13
|
+
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
|
|
14
|
+
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
|
|
15
|
+
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
|
|
16
|
+
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
|
|
17
|
+
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
|
|
18
|
+
@media (max-width: 600px) {
|
|
19
|
+
body, #message { margin-top: 0; background: white; box-shadow: none; }
|
|
20
|
+
body { border-top: 16px solid #ffa100; }
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<div id="message">
|
|
26
|
+
<h2>404</h2>
|
|
27
|
+
<h1>Page Not Found</h1>
|
|
28
|
+
<p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
|
|
29
|
+
<h3>Why am I seeing this?</h3>
|
|
30
|
+
<p>This page was generated by the Firebase Command-Line Interface. To modify it, edit the <code>404.html</code> file in your project's configured <code>public</code> directory.</p>
|
|
31
|
+
</div>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Frontend
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
cd frontend
|
|
7
|
+
cp template.env .env
|
|
8
|
+
npm insall
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Make sure your backend is running and double check frontend/.env file is accurate.
|
|
12
|
+
|
|
13
|
+
## Run
|
|
14
|
+
|
|
15
|
+
This will watch for changes and build while running the server.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Build (Nginx)
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
npm run build
|
|
25
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
7
|
+
<title>Postbase Demo</title>
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<div id="app"></div>
|
|
12
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
13
|
+
</body>
|
|
14
|
+
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"noEmit": true,
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"checkJs": true,
|
|
9
|
+
|
|
10
|
+
/* Preact Config */
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"jsxImportSource": "preact",
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"paths": {
|
|
15
|
+
"react": ["./node_modules/preact/compat/"],
|
|
16
|
+
"react-dom": ["./node_modules/preact/compat/"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"include": ["node_modules/vite/client.d.ts", "**/*"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A wrapper around better-auth/client's createAuthClient
|
|
3
|
+
* that adds Firebase-like helpers:
|
|
4
|
+
* - onAuthStateChanged(callback)
|
|
5
|
+
* - getIdToken()
|
|
6
|
+
* - currentUser
|
|
7
|
+
*/
|
|
8
|
+
export function createAuthClient(betterAuthClient) {
|
|
9
|
+
// Track current user state
|
|
10
|
+
let currentUser = null;
|
|
11
|
+
const subscribers = new Set();
|
|
12
|
+
|
|
13
|
+
function notifySubscribers(user) {
|
|
14
|
+
for (const cb of subscribers) {
|
|
15
|
+
try {
|
|
16
|
+
cb(user);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error("Error in onAuthStateChanged listener:", err);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Poll for session changes (adjust interval as needed)
|
|
24
|
+
async function checkSession() {
|
|
25
|
+
try {
|
|
26
|
+
const { data } = await betterAuthClient.getSession();
|
|
27
|
+
const newUser = data?.user ?? null;
|
|
28
|
+
if (newUser) {
|
|
29
|
+
newUser.uid = newUser.id;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (JSON.stringify(newUser) !== JSON.stringify(currentUser)) {
|
|
33
|
+
currentUser = newUser;
|
|
34
|
+
if (currentUser) {
|
|
35
|
+
currentUser.getIdToken = getIdToken;
|
|
36
|
+
}
|
|
37
|
+
notifySubscribers(currentUser);
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error("Error checking session:", err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Simple polling loop (every 5 seconds by default)
|
|
45
|
+
const POLL_INTERVAL = 5000;
|
|
46
|
+
let pollTimer = setInterval(checkSession, POLL_INTERVAL);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Adds a listener for auth state changes.
|
|
50
|
+
* Returns an unsubscribe function (for React/Preact useEffect cleanup).
|
|
51
|
+
*/
|
|
52
|
+
function onAuthStateChanged(callback) {
|
|
53
|
+
subscribers.add(callback);
|
|
54
|
+
|
|
55
|
+
// Immediately invoke with current user
|
|
56
|
+
(async () => {
|
|
57
|
+
const { data } = await betterAuthClient.getSession();
|
|
58
|
+
const newUser = data?.user ?? null;
|
|
59
|
+
if (newUser) {
|
|
60
|
+
newUser.uid = newUser.id;
|
|
61
|
+
}
|
|
62
|
+
currentUser = newUser;
|
|
63
|
+
if (currentUser) {
|
|
64
|
+
currentUser.getIdToken = getIdToken;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
callback(newUser);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error("Error in onAuthStateChanged listener:", err);
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
|
|
73
|
+
// Return unsubscribe
|
|
74
|
+
return () => {
|
|
75
|
+
subscribers.delete(callback);
|
|
76
|
+
if (subscribers.size === 0) {
|
|
77
|
+
clearInterval(pollTimer);
|
|
78
|
+
pollTimer = null;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Firebase-like getIdToken() equivalent.
|
|
85
|
+
* Returns the current access token (JWT) if a session exists.
|
|
86
|
+
*/
|
|
87
|
+
async function getIdToken(refresh = false) {
|
|
88
|
+
const { data } = await betterAuthClient.getSession();
|
|
89
|
+
const token =
|
|
90
|
+
data?.session?.token ?? data?.session?.token ?? null;
|
|
91
|
+
return token;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function getBetterAuthToken() {
|
|
95
|
+
const authToken = window.sessionStorage.getItem('authToken');
|
|
96
|
+
if (authToken) {
|
|
97
|
+
return authToken;
|
|
98
|
+
}
|
|
99
|
+
const { data } = await betterAuthClient.getSession();
|
|
100
|
+
if (data && data.hasOwnProperty('session') && data.session?.token) {
|
|
101
|
+
window.sessionStorage.setItem('authToken', data.session?.token);
|
|
102
|
+
return data.session?.token;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const signOut = (...args) => {
|
|
108
|
+
const authToken = window.sessionStorage.getItem('authToken');
|
|
109
|
+
if (authToken) {
|
|
110
|
+
window.sessionStorage.removeItem('authToken');
|
|
111
|
+
}
|
|
112
|
+
// at the end because it can trigger navigation
|
|
113
|
+
betterAuthClient.signOut.apply(this, ...args);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Return wrapped client with added helpers and dynamic currentUser getter
|
|
117
|
+
return {
|
|
118
|
+
...betterAuthClient,
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Returns the most recent known user (Firebase-like).
|
|
122
|
+
* This does not fetch the server — it reflects the last known session state.
|
|
123
|
+
*/
|
|
124
|
+
get currentUser() {
|
|
125
|
+
return currentUser;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
onAuthStateChanged,
|
|
129
|
+
getBetterAuthToken,
|
|
130
|
+
signOut,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
//import { createAuthClient } from '../../auth';
|
|
2
|
+
|
|
3
|
+
export function createFirebaseAuthClient(postbaseAuthClientWithBetterAuthFunctions) {
|
|
4
|
+
// if (!(auth instanceof createAuthClient)) {
|
|
5
|
+
// throw new Error("");
|
|
6
|
+
// }
|
|
7
|
+
|
|
8
|
+
const createUserWithEmailAndPassword = async (auth, email, password, name = null) => {
|
|
9
|
+
try {
|
|
10
|
+
// better-auth requires name whereas firebase auth doesn't
|
|
11
|
+
let _name = name;
|
|
12
|
+
if (!_name) {
|
|
13
|
+
_name = email.split('@')[0];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await auth.signUp.email({
|
|
17
|
+
email,
|
|
18
|
+
password,
|
|
19
|
+
name,
|
|
20
|
+
//callbackURL: "/dashboard",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const { data } = await auth.getSession();
|
|
24
|
+
const user = data?.user ?? null;
|
|
25
|
+
|
|
26
|
+
const userCredential = { user };
|
|
27
|
+
|
|
28
|
+
return userCredential;
|
|
29
|
+
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throw {
|
|
32
|
+
code: '500',
|
|
33
|
+
message: err.message,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const signInWithEmailAndPassword = async (auth, email, password) => {
|
|
39
|
+
try {
|
|
40
|
+
await auth.signIn.email({
|
|
41
|
+
email,
|
|
42
|
+
password,
|
|
43
|
+
//callbackURL: '/dashboard',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const { data } = await auth.getSession();
|
|
47
|
+
const user = data?.user ?? null;
|
|
48
|
+
|
|
49
|
+
const userCredential = { user };
|
|
50
|
+
|
|
51
|
+
return userCredential;
|
|
52
|
+
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw {
|
|
55
|
+
code: '500',
|
|
56
|
+
message: err.message,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const sendEmailVerification = async (currentUser) => {
|
|
62
|
+
/** // Must have following function on the server:
|
|
63
|
+
* // https://better-auth.com/docs/concepts/email
|
|
64
|
+
* import { betterAuth } from 'better-auth';
|
|
65
|
+
import { sendEmail } from './email'; // your email sending function
|
|
66
|
+
export const auth = betterAuth({
|
|
67
|
+
emailVerification: {
|
|
68
|
+
sendVerificationEmail: async ({ user, url, token }, request) => {
|
|
69
|
+
void sendEmail({
|
|
70
|
+
to: user.email,
|
|
71
|
+
subject: 'Verify your email address',
|
|
72
|
+
text: `Click the link to verify your email: ${url}`
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
*/
|
|
78
|
+
await postbaseAuthClientWithBetterAuthFunctions.sendVerificationEmail({
|
|
79
|
+
email: currentUser.email
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const updateProfile = async () => { };
|
|
84
|
+
|
|
85
|
+
const updateEmail = async () => { };
|
|
86
|
+
|
|
87
|
+
const updatePassword = async () => { };
|
|
88
|
+
|
|
89
|
+
const sendPasswordResetEmail = async (auth, email) => {
|
|
90
|
+
await auth.requestPasswordReset({
|
|
91
|
+
email,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const signOut = async (auth) => {
|
|
97
|
+
const authToken = window.sessionStorage.getItem('authToken');
|
|
98
|
+
if (authToken) {
|
|
99
|
+
window.sessionStorage.removeItem('authToken');
|
|
100
|
+
}
|
|
101
|
+
// at the end because it can trigger navigation
|
|
102
|
+
auth.signOut.apply(this, []);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
createUserWithEmailAndPassword,
|
|
107
|
+
sendEmailVerification,
|
|
108
|
+
sendPasswordResetEmail,
|
|
109
|
+
signInWithEmailAndPassword,
|
|
110
|
+
updateProfile,
|
|
111
|
+
updateEmail,
|
|
112
|
+
updatePassword,
|
|
113
|
+
signOut,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getBetterAuthToken } from "../../../../src/auth";
|
|
2
|
+
import { RtdbClient } from "../../rtdb";
|
|
3
|
+
|
|
4
|
+
export function getDatabase(app) {
|
|
5
|
+
// app is just {} having baseUrl and getAuthToken
|
|
6
|
+
return new RtdbClient({
|
|
7
|
+
restUrl: app.baseUrl,
|
|
8
|
+
wsUrl: app.baseUrl.replace('https://', 'wss://'),
|
|
9
|
+
getAuthToken: getBetterAuthToken,
|
|
10
|
+
});
|
|
11
|
+
}
|