instaserve 1.1.11 → 1.1.14
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 +64 -1
- package/api.txt +61 -0
- package/lib/auth0.js +43 -10
- package/lib/r2.js +41 -5
- package/lib/sqlite.js +22 -5
- package/package.json +3 -2
- package/routes-full.js +12 -0
- package/test_api.sh +170 -0
package/README.md
CHANGED
|
@@ -8,10 +8,27 @@
|
|
|
8
8
|
</p>
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Instaserve uses environment variables for Auth0 and Cloudflare R2 integration. Create a `.env` file (if your runner supports it) or pass them before the command.
|
|
14
|
+
|
|
15
|
+
### Auth0 Variables
|
|
16
|
+
- `AUTH0_DOMAIN`: Your Auth0 domain
|
|
17
|
+
- `AUTH0_CLIENT_ID`: Your Auth0 Client ID
|
|
18
|
+
- `AUTH0_CLIENT_SECRET`: Your Auth0 Client Secret
|
|
19
|
+
|
|
20
|
+
### Cloudflare R2 Variables (via `lib/r2.js`)
|
|
21
|
+
- `CLOUDFLARE_ACCOUNT_ID`: R2 Account ID
|
|
22
|
+
- `CLOUDFLARE_ACCESS_KEY_ID`: R2 Access Key ID
|
|
23
|
+
- `CLOUDFLARE_SECRET_ACCESS_KEY`: R2 Secret Access Key
|
|
24
|
+
|
|
11
25
|
## Usage
|
|
12
26
|
|
|
13
27
|
<div style="background: white; padding: 15px; border-radius: 6px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); margin: 15px 0;">
|
|
14
|
-
<pre style="margin: 0;"><code
|
|
28
|
+
<pre style="margin: 0;"><code># Run with env vars
|
|
29
|
+
AUTH0_DOMAIN=... AUTH0_CLIENT_ID=... npx instaserve [options]
|
|
30
|
+
|
|
31
|
+
# Generate routes file
|
|
15
32
|
npx instaserve generate-routes</code></pre>
|
|
16
33
|
</div>
|
|
17
34
|
|
|
@@ -238,3 +255,49 @@ export default {
|
|
|
238
255
|
}
|
|
239
256
|
}
|
|
240
257
|
```
|
|
258
|
+
|
|
259
|
+
## Library Modules
|
|
260
|
+
|
|
261
|
+
The `lib/` directory contains useful modules for authentication, storage, and database manipulation.
|
|
262
|
+
|
|
263
|
+
### Auth0 (`lib/auth0.js`)
|
|
264
|
+
Provides authentication routes and `req.user` handling via Auth0.
|
|
265
|
+
- **Routes:** `GET /login`, `GET /logout`, `GET /callback`
|
|
266
|
+
- **Usage:** Import and spread into your routes.
|
|
267
|
+
```javascript
|
|
268
|
+
import auth0 from './lib/auth0.js';
|
|
269
|
+
|
|
270
|
+
export default {
|
|
271
|
+
...auth0,
|
|
272
|
+
// other routes
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### R2 Storage (`lib/r2.js`)
|
|
277
|
+
Utilities for Cloudflare R2 or S3-compatible object storage.
|
|
278
|
+
- **Features:** `uploadToR2`, `downloadFromR2`, `listR2Files`
|
|
279
|
+
- **Routes:** `fileRoutes` exports helpers for file management.
|
|
280
|
+
- **Usage:**
|
|
281
|
+
```javascript
|
|
282
|
+
import { fileRoutes } from './lib/r2.js';
|
|
283
|
+
|
|
284
|
+
export default {
|
|
285
|
+
...fileRoutes,
|
|
286
|
+
// other routes
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### SQLite (`lib/sqlite.js`)
|
|
291
|
+
Provides a local SQLite database with a per-user Key-Value store.
|
|
292
|
+
- **Features:** User management table, KV table.
|
|
293
|
+
- **Routes:** `_auth` middleware, CRUD for KV store (`/api`, `/all`).
|
|
294
|
+
- **Usage:**
|
|
295
|
+
```javascript
|
|
296
|
+
import sqliteRoutes from './lib/sqlite.js';
|
|
297
|
+
|
|
298
|
+
export default {
|
|
299
|
+
...sqliteRoutes,
|
|
300
|
+
// other routes
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
```
|
package/api.txt
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Instaserve API Documentation
|
|
2
|
+
|
|
3
|
+
Authentication
|
|
4
|
+
--------------
|
|
5
|
+
GET /login
|
|
6
|
+
Redirects to Auth0 for sign-in.
|
|
7
|
+
Sets a secure http-only session cookie ('token') upon success.
|
|
8
|
+
|
|
9
|
+
GET /logout
|
|
10
|
+
Logs the user out and clears the session.
|
|
11
|
+
|
|
12
|
+
Key-Value Store (SQLite)
|
|
13
|
+
------------------------
|
|
14
|
+
* All endpoints require authentication.
|
|
15
|
+
* Data is isolated per user.
|
|
16
|
+
|
|
17
|
+
POST /api
|
|
18
|
+
Set a key-value pair.
|
|
19
|
+
Body (JSON): { "key": "myKey", "value": "myValue" }
|
|
20
|
+
Returns: { "key": "myKey", "value": "myValue" }
|
|
21
|
+
|
|
22
|
+
GET /api?key=myKey
|
|
23
|
+
Retrieve a value by key.
|
|
24
|
+
Returns: { "key": "myKey", "value": "myValue" }
|
|
25
|
+
|
|
26
|
+
GET /all
|
|
27
|
+
List all key-value pairs for the current user.
|
|
28
|
+
Returns: [ { "key": "key1", "value": "val1" }, ... ]
|
|
29
|
+
|
|
30
|
+
DELETE /api
|
|
31
|
+
Delete a key.
|
|
32
|
+
Body (JSON): { "key": "myKey" }
|
|
33
|
+
Returns: { "deleted": true, "key": "myKey" }
|
|
34
|
+
|
|
35
|
+
Storage (R2 / S3)
|
|
36
|
+
-----------------
|
|
37
|
+
* Requires R2 environment variables to be set on the server.
|
|
38
|
+
* Uses the credentials provided in endpoints or server defaults.
|
|
39
|
+
|
|
40
|
+
POST /upload
|
|
41
|
+
Upload a file.
|
|
42
|
+
Body (JSON):
|
|
43
|
+
{
|
|
44
|
+
"bucket": "my-bucket-name",
|
|
45
|
+
"key": "filename.jpg",
|
|
46
|
+
"body": "base64_encoded_string_contents",
|
|
47
|
+
"contentType": "image/jpeg"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
POST /files
|
|
51
|
+
List files in a bucket.
|
|
52
|
+
Body (JSON): { "bucket": "my-bucket-name" }
|
|
53
|
+
Returns: List of file objects (key, size, lastModified).
|
|
54
|
+
|
|
55
|
+
GET /download?bucket=my-bucket-name&key=filename.jpg
|
|
56
|
+
Download a file directly.
|
|
57
|
+
(Available via the 'download' route handler)
|
|
58
|
+
|
|
59
|
+
GET /delete?bucket=my-bucket-name&key=filename.jpg
|
|
60
|
+
Delete a file.
|
|
61
|
+
(Available via the 'delete' route handler)
|
package/lib/auth0.js
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
const auth0_domain = process.env.AUTH0_DOMAIN || process.env.auth0_domain;
|
|
4
|
+
const auth0_clientid = process.env.AUTH0_CLIENT_ID || process.env.auth0_clientid;
|
|
5
|
+
const auth0_clientsecret = process.env.AUTH0_CLIENT_SECRET || process.env.auth0_clientsecret;
|
|
6
|
+
|
|
7
|
+
const LOGGEDIN_REDIRECT = "/";
|
|
8
|
+
const LOGGEDOUT_REDIRECT = "/";
|
|
9
|
+
|
|
10
|
+
if (!auth0_domain || !auth0_clientid || !auth0_clientsecret) {
|
|
11
|
+
console.warn("Auth0 environment variables not set! Please set AUTH0_DOMAIN, AUTH0_CLIENT_ID, and AUTH0_CLIENT_SECRET.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Auth0 Authentication Module
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* 1. Import this module in your routes.js file.
|
|
19
|
+
* 2. Merge it into your default export routes object: `...auth0`.
|
|
20
|
+
* 3. Ensure 'secrets' is globally available or imported with `auth0_domain`, `auth0_clientid`, and `auth0_clientsecret`.
|
|
21
|
+
* 4. Ensure `global.sqlite` is initialized (typically by importing lib/sqlite.js).
|
|
22
|
+
*
|
|
23
|
+
* Routes provided:
|
|
24
|
+
* - GET /login: Initiates Auth0 login flow.
|
|
25
|
+
* - GET /logout: Clears session and token.
|
|
26
|
+
* - GET /callback: Handles Auth0 callback, validates token, creates user in DB.
|
|
27
|
+
*/
|
|
28
|
+
|
|
1
29
|
export default {
|
|
2
30
|
"GET /login": (req, res) => {
|
|
3
|
-
const REDIRECT_URI =
|
|
31
|
+
const REDIRECT_URI = `https://${req.headers.host}/callback`;
|
|
4
32
|
const state = crypto.randomBytes(16).toString("hex");
|
|
5
33
|
global.a0state = state;
|
|
6
|
-
const url = `https://${
|
|
34
|
+
const url = `https://${auth0_domain}/authorize?response_type=code&client_id=${auth0_clientid}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=openid%20profile%20email&state=${encodeURIComponent(state)}`;
|
|
7
35
|
res.writeHead(302, { Location: url });
|
|
8
36
|
res.end();
|
|
9
37
|
},
|
|
@@ -14,32 +42,37 @@ export default {
|
|
|
14
42
|
global.sqlite.prepare("UPDATE users SET token = NULL WHERE token = ?").run(token);
|
|
15
43
|
}
|
|
16
44
|
res.setHeader("Set-Cookie", "token=; Path=/; HttpOnly; Secure; SameSite=Lax");
|
|
17
|
-
res.writeHead(302, { Location:
|
|
45
|
+
res.writeHead(302, { Location: LOGGEDOUT_REDIRECT });
|
|
18
46
|
res.end();
|
|
19
47
|
},
|
|
20
48
|
"GET /callback": async (req, res, data) => {
|
|
21
|
-
const REDIRECT_URI =
|
|
22
|
-
const tokenRes = await fetch(
|
|
49
|
+
const REDIRECT_URI = `https://${req.headers.host}/callback`;
|
|
50
|
+
const tokenRes = await fetch(`https://${auth0_domain}/oauth/token`, {
|
|
23
51
|
method: "POST",
|
|
24
52
|
headers: { "content-type": "application/json" },
|
|
25
53
|
body: JSON.stringify({
|
|
26
54
|
grant_type: "authorization_code",
|
|
27
|
-
client_id:
|
|
28
|
-
client_secret:
|
|
55
|
+
client_id: auth0_clientid,
|
|
56
|
+
client_secret: auth0_clientsecret,
|
|
29
57
|
code: data.code,
|
|
30
58
|
redirect_uri: REDIRECT_URI
|
|
31
59
|
}),
|
|
32
|
-
})
|
|
60
|
+
});
|
|
61
|
+
|
|
33
62
|
const tokens = await tokenRes.json();
|
|
63
|
+
if (tokens.error || !tokens.id_token) {
|
|
64
|
+
console.error("Auth0 Error:", tokens);
|
|
65
|
+
return `Auth0 Error: ${tokens.error_description || tokens.error || "Unknown error"}`;
|
|
66
|
+
}
|
|
34
67
|
// decrypt token
|
|
35
68
|
const id_token = tokens.id_token;
|
|
36
69
|
const payload = JSON.parse(Buffer.from(id_token.split('.')[1], 'base64').toString());
|
|
37
70
|
const auth_token = crypto.randomBytes(32).toString("hex");
|
|
38
|
-
sqlite.prepare("INSERT INTO users (id, username, token) VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET token = ?, name = ?, picture = ?")
|
|
71
|
+
global.sqlite.prepare("INSERT INTO users (id, username, token) VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET token = ?, name = ?, picture = ?")
|
|
39
72
|
.run(payload.email, payload.name, auth_token, auth_token, payload.name, payload.picture);
|
|
40
73
|
res.setHeader("Set-Cookie", `username=${payload.email}; Path=/;`);
|
|
41
74
|
res.setHeader("Set-Cookie", `name=${payload.name}; Path=/;`);
|
|
42
75
|
res.setHeader("Set-Cookie", `token=${auth_token}; Path=/;`);
|
|
43
|
-
return `<script>localStorage.setItem('name', '${payload.name}');localStorage.setItem('pic', '${payload.picture}');window.location.href = '
|
|
76
|
+
return `<script>localStorage.setItem('name', '${payload.name}');localStorage.setItem('pic', '${payload.picture}');window.location.href = '${LOGGEDIN_REDIRECT}';</script>`;
|
|
44
77
|
}
|
|
45
78
|
}
|
package/lib/r2.js
CHANGED
|
@@ -2,6 +2,36 @@ import https from 'https';
|
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import { URL } from 'url';
|
|
4
4
|
|
|
5
|
+
const cloudflareAccessKeyId = process.env.CLOUDFLARE_ACCESS_KEY_ID || process.env.cloudflareAccessKeyId;
|
|
6
|
+
const cloudflareSecretAccessKey = process.env.CLOUDFLARE_SECRET_ACCESS_KEY || process.env.cloudflareSecretAccessKey;
|
|
7
|
+
const cloudflareAccountId = process.env.CLOUDFLARE_ACCOUNT_ID || process.env.cloudflareAccountId;
|
|
8
|
+
|
|
9
|
+
if (!cloudflareAccessKeyId || !cloudflareSecretAccessKey || !cloudflareAccountId) {
|
|
10
|
+
console.warn("Cloudflare environment variables not set! Please set CLOUDFLARE_ACCESS_KEY_ID, CLOUDFLARE_SECRET_ACCESS_KEY, and CLOUDFLARE_ACCOUNT_ID.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Cloudflare R2 / S3-Compatible Storage Module
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* 1. Import desired functions or `fileRoutes` from this file.
|
|
18
|
+
* 2. Requires a `secrets` object with:
|
|
19
|
+
* - cloudflareAccessKeyId
|
|
20
|
+
* - cloudflareSecretAccessKey
|
|
21
|
+
* - cloudflareAccountId
|
|
22
|
+
*
|
|
23
|
+
* Exports:
|
|
24
|
+
* - uploadToR2(bucket, key, body, contentType, secrets)
|
|
25
|
+
* - downloadFromR2(bucket, key, secrets)
|
|
26
|
+
* - deleteFromR2(bucket, key, secrets)
|
|
27
|
+
* - listR2Files(bucket, prefix, secrets)
|
|
28
|
+
* - getSignedDownloadUrl(bucket, key, secrets)
|
|
29
|
+
*
|
|
30
|
+
* fileRoutes:
|
|
31
|
+
* Contains helper handlers for file operations. Note that `upload`, `download`, `delete`
|
|
32
|
+
* are generic async functions awaiting `data` parameters, while `POST /files` is a standard route.
|
|
33
|
+
*/
|
|
34
|
+
|
|
5
35
|
// --- Core SigV4 Utilities ---
|
|
6
36
|
|
|
7
37
|
const hashSHA256 = (str) => crypto.createHash('sha256').update(str).digest('hex');
|
|
@@ -177,6 +207,12 @@ function getSignedDownloadUrl(bucket, key, secrets) {
|
|
|
177
207
|
return signR2('GET', bucket, key, null, ...getSecrets(secrets)).url;
|
|
178
208
|
}
|
|
179
209
|
|
|
210
|
+
const defaultSecrets = {
|
|
211
|
+
cloudflareAccessKeyId,
|
|
212
|
+
cloudflareSecretAccessKey,
|
|
213
|
+
cloudflareAccountId
|
|
214
|
+
};
|
|
215
|
+
|
|
180
216
|
export const fileRoutes = {
|
|
181
217
|
upload: async (req, res, data) => {
|
|
182
218
|
try {
|
|
@@ -188,27 +224,27 @@ export const fileRoutes = {
|
|
|
188
224
|
} else {
|
|
189
225
|
body = Buffer.alloc(0);
|
|
190
226
|
}
|
|
191
|
-
return await uploadToR2(data.bucket, data.key, body, data.contentType, data.secrets);
|
|
227
|
+
return await uploadToR2(data.bucket, data.key, body, data.contentType, data.secrets || defaultSecrets);
|
|
192
228
|
} catch (e) { return { error: e.message }; }
|
|
193
229
|
},
|
|
194
230
|
download: async (req, res, data) => {
|
|
195
231
|
try {
|
|
196
|
-
return await downloadFromR2(data.bucket, data.key, data.secrets);
|
|
232
|
+
return await downloadFromR2(data.bucket, data.key, data.secrets || defaultSecrets);
|
|
197
233
|
} catch (e) { return { error: e.message }; }
|
|
198
234
|
},
|
|
199
235
|
delete: async (req, res, data) => {
|
|
200
236
|
try {
|
|
201
|
-
return await deleteFromR2(data.bucket, data.key, data.secrets);
|
|
237
|
+
return await deleteFromR2(data.bucket, data.key, data.secrets || defaultSecrets);
|
|
202
238
|
} catch (e) { return { error: e.message }; }
|
|
203
239
|
},
|
|
204
240
|
"POST /files": async (req, res, data) => {
|
|
205
241
|
try {
|
|
206
|
-
return await listR2Files(data.bucket, data.prefix, data.secrets);
|
|
242
|
+
return await listR2Files(data.bucket, data.prefix, data.secrets || defaultSecrets);
|
|
207
243
|
} catch (e) { return { error: e.message }; }
|
|
208
244
|
}
|
|
209
245
|
};
|
|
210
246
|
|
|
211
|
-
export {
|
|
247
|
+
export default {
|
|
212
248
|
uploadToR2,
|
|
213
249
|
downloadFromR2,
|
|
214
250
|
deleteFromR2,
|
package/lib/sqlite.js
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Database & Key-Value Store Module
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* 1. Import this module to initialize the database ('data.db') and tables ('users', 'kv').
|
|
6
|
+
* 2. Access the database instance via `global.sqlite`.
|
|
7
|
+
* 3. Exported routes provide a per-user Key-Value store mechanism guarded by the `_auth` middleware.
|
|
8
|
+
*
|
|
9
|
+
* Tables created:
|
|
10
|
+
* - users: id, username, pass, token, picture, name, email
|
|
11
|
+
* - kv: key (format: "username:key"), value
|
|
12
|
+
*
|
|
13
|
+
* Routes provided:
|
|
14
|
+
* - _auth: Middleware for protecting routes (assigns `req.user`).
|
|
15
|
+
* - POST /api: Set a KV pair for the logged-in user.
|
|
16
|
+
* - GET /api: Get a value by key.
|
|
17
|
+
* - GET /all: List all keys/values for the user.
|
|
18
|
+
* - DELETE /api: Delete a key.
|
|
19
|
+
*/
|
|
20
|
+
|
|
1
21
|
import Database from "better-sqlite3";
|
|
2
|
-
import { fileRoutes } from "./lib/file-routes.js";
|
|
3
|
-
import secrets from "./secrets.js";
|
|
4
|
-
import crypto from "crypto";
|
|
5
22
|
|
|
6
23
|
// Initialize database
|
|
7
24
|
global.sqlite = new Database("data.db");
|
|
@@ -11,8 +28,8 @@ sqlite.prepare("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)
|
|
|
11
28
|
// Combine all routes
|
|
12
29
|
export default {
|
|
13
30
|
_auth: (req, res, data) => {
|
|
14
|
-
if (req.url.match(/js|html|css/)) return;
|
|
15
|
-
|
|
31
|
+
if (req.url === '/' || req.url.match(/js|html|css|callback/)) return;
|
|
32
|
+
|
|
16
33
|
if (req.url.startsWith("/register") || req.url.startsWith("/login")) return;
|
|
17
34
|
|
|
18
35
|
const token = req.headers.cookie?.split('token=')[1].split(';')[0];
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instaserve",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"description": "Instant web stack",
|
|
5
5
|
"main": "module.mjs",
|
|
6
6
|
"bin": "./instaserve",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"start": "node instaserve"
|
|
8
|
+
"start": "node instaserve",
|
|
9
|
+
"test": "./test_api.sh"
|
|
9
10
|
},
|
|
10
11
|
"type": "module",
|
|
11
12
|
"dependencies": {
|
package/routes-full.js
ADDED
package/test_api.sh
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Configuration
|
|
4
|
+
PORT=3001
|
|
5
|
+
BASE_URL="https://127.0.0.1:$PORT"
|
|
6
|
+
DB_FILE="data.db"
|
|
7
|
+
TEST_USER="testuser"
|
|
8
|
+
TEST_TOKEN="test_token_12345"
|
|
9
|
+
TEST_KEY="test_key"
|
|
10
|
+
TEST_VALUE="test_value"
|
|
11
|
+
SERVER_PID=""
|
|
12
|
+
|
|
13
|
+
# Helper function to run sqlite commands
|
|
14
|
+
run_sqlite() {
|
|
15
|
+
sqlite3 "$DB_FILE" "$1"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Cleanup function to kill server and remove test data
|
|
19
|
+
cleanup() {
|
|
20
|
+
echo ""
|
|
21
|
+
echo "--- Cleanup ---"
|
|
22
|
+
|
|
23
|
+
if [ -n "$SERVER_PID" ]; then
|
|
24
|
+
echo "Stopping server (PID: $SERVER_PID)..."
|
|
25
|
+
kill "$SERVER_PID" 2>/dev/null
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
echo "Removing test data..."
|
|
29
|
+
run_sqlite "DELETE FROM users WHERE id = 'test_id';"
|
|
30
|
+
# run_sqlite "DELETE FROM kv WHERE key = '$TEST_USER:$TEST_KEY';"
|
|
31
|
+
|
|
32
|
+
rm -f server.log
|
|
33
|
+
echo "Done."
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Register cleanup to run on script exit (success or failure)
|
|
37
|
+
trap cleanup EXIT
|
|
38
|
+
|
|
39
|
+
# 1. Start the Server
|
|
40
|
+
echo "--- Setup ---"
|
|
41
|
+
echo "Starting Instaserve on port $PORT..."
|
|
42
|
+
# Start server in background, directing output to log
|
|
43
|
+
./instaserve -api ./routes-full.js -secure -port "$PORT" > server.log 2>&1 &
|
|
44
|
+
SERVER_PID=$!
|
|
45
|
+
echo "Server process ID: $SERVER_PID"
|
|
46
|
+
|
|
47
|
+
# Wait for server to be ready
|
|
48
|
+
echo "Waiting for server to start..."
|
|
49
|
+
MAX_RETRIES=10
|
|
50
|
+
COUNT=0
|
|
51
|
+
STARTED=false
|
|
52
|
+
|
|
53
|
+
while [ $COUNT -lt $MAX_RETRIES ]; do
|
|
54
|
+
if grep -q "started on:" server.log; then
|
|
55
|
+
STARTED=true
|
|
56
|
+
break
|
|
57
|
+
fi
|
|
58
|
+
sleep 1
|
|
59
|
+
((COUNT++))
|
|
60
|
+
echo -n "."
|
|
61
|
+
done
|
|
62
|
+
echo ""
|
|
63
|
+
|
|
64
|
+
if [ "$STARTED" = false ]; then
|
|
65
|
+
echo "FAIL: Server failed to start within timeout."
|
|
66
|
+
echo "Server Log:"
|
|
67
|
+
cat server.log
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
echo "Server is running!"
|
|
72
|
+
|
|
73
|
+
# 2. Insert Test User
|
|
74
|
+
echo "Setting up test user in database..."
|
|
75
|
+
# Ensure tables exist (the server creation might race with this if it's the very first run,
|
|
76
|
+
# but server is confirmed started above, so tables should be there)
|
|
77
|
+
run_sqlite "INSERT OR REPLACE INTO users (id, username, token, name, email) VALUES ('test_id', '$TEST_USER', '$TEST_TOKEN', 'Test User', 'test@example.com');"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# 3. Validation Tests
|
|
81
|
+
echo ""
|
|
82
|
+
echo "--- Running Tests ---"
|
|
83
|
+
|
|
84
|
+
# Test 1: Unauthenticated Access
|
|
85
|
+
echo "- Testing unauthenticated access to /api (Expect 401)..."
|
|
86
|
+
HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" "$BASE_URL/api")
|
|
87
|
+
if [ "$HTTP_CODE" == "401" ]; then
|
|
88
|
+
echo " ✅ PASS"
|
|
89
|
+
else
|
|
90
|
+
echo " ❌ FAIL: Returned $HTTP_CODE"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Test 2: POST /api (Set Key)
|
|
94
|
+
echo "- Testing POST /api (Set Key)..."
|
|
95
|
+
RESPONSE=$(curl -k -s -X POST "$BASE_URL/api" \
|
|
96
|
+
-H "Cookie: token=$TEST_TOKEN" \
|
|
97
|
+
-H "Content-Type: application/json" \
|
|
98
|
+
-d "{\"key\": \"$TEST_KEY\", \"value\": \"$TEST_VALUE\"}")
|
|
99
|
+
|
|
100
|
+
if [[ "$RESPONSE" == *"$TEST_VALUE"* ]]; then
|
|
101
|
+
echo " ✅ PASS: $RESPONSE"
|
|
102
|
+
else
|
|
103
|
+
echo " ❌ FAIL: $RESPONSE"
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Test 3: GET /api (Get Value)
|
|
107
|
+
echo "- Testing GET /api (Get Key)..."
|
|
108
|
+
RESPONSE=$(curl -k -s -G "$BASE_URL/api" \
|
|
109
|
+
-H "Cookie: token=$TEST_TOKEN" \
|
|
110
|
+
--data-urlencode "key=$TEST_KEY")
|
|
111
|
+
|
|
112
|
+
if [[ "$RESPONSE" == *"$TEST_VALUE"* ]]; then
|
|
113
|
+
echo " ✅ PASS: $RESPONSE"
|
|
114
|
+
else
|
|
115
|
+
echo " ❌ FAIL: $RESPONSE"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# Test 4: GET /all (List Keys)
|
|
119
|
+
echo "- Testing GET /all..."
|
|
120
|
+
RESPONSE=$(curl -k -s "$BASE_URL/all" \
|
|
121
|
+
-H "Cookie: token=$TEST_TOKEN")
|
|
122
|
+
|
|
123
|
+
if [[ "$RESPONSE" == *"$TEST_KEY"* ]]; then
|
|
124
|
+
echo " ✅ PASS: Found key in list"
|
|
125
|
+
else
|
|
126
|
+
echo " ❌ FAIL: $RESPONSE"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# 6. Test Authenticated Access (DELETE /api) - Delete Key
|
|
130
|
+
echo "- Testing DELETE /api..."
|
|
131
|
+
RESPONSE=$(curl -k -s -X DELETE "$BASE_URL/api" \
|
|
132
|
+
-H "Cookie: token=$TEST_TOKEN" \
|
|
133
|
+
-H "Content-Type: application/json" \
|
|
134
|
+
-d "{\"key\": \"$TEST_KEY\"}")
|
|
135
|
+
|
|
136
|
+
if [[ "$RESPONSE" == *"deleted"* ]]; then
|
|
137
|
+
echo " ✅ PASS: $RESPONSE"
|
|
138
|
+
else
|
|
139
|
+
echo " ❌ FAIL: $RESPONSE"
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# 7. Test Login Redirect
|
|
143
|
+
echo "- Testing GET /login (Expect 302 Redirect to Auth0)..."
|
|
144
|
+
LOGIN_HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" "$BASE_URL/login")
|
|
145
|
+
if [ "$LOGIN_HTTP_CODE" == "302" ]; then
|
|
146
|
+
echo " ✅ PASS"
|
|
147
|
+
else
|
|
148
|
+
echo " ❌ FAIL: Returned $LOGIN_HTTP_CODE"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# 8. Test Logout
|
|
152
|
+
echo "- Testing GET /logout (Expect 302 Redirect)..."
|
|
153
|
+
# Use -D - to dump headers, -o /dev/null to discard body
|
|
154
|
+
LOGOUT_HEADERS=$(curl -k -s -D - -o /dev/null "$BASE_URL/logout" -H "Cookie: token=$TEST_TOKEN")
|
|
155
|
+
|
|
156
|
+
if echo "$LOGOUT_HEADERS" | grep -q "302 Found"; then
|
|
157
|
+
echo " ✅ PASS: Redirect confirmed"
|
|
158
|
+
else
|
|
159
|
+
echo " ❌ FAIL: No redirect found in headers"
|
|
160
|
+
echo "$LOGOUT_HEADERS"
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
echo "- Verifying token invalidation (Expect 401 on /api)..."
|
|
164
|
+
# Reuse TEST_TOKEN which should now be invalidated in DB
|
|
165
|
+
HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" "$BASE_URL/api" -H "Cookie: token=$TEST_TOKEN")
|
|
166
|
+
if [ "$HTTP_CODE" == "401" ]; then
|
|
167
|
+
echo " ✅ PASS: Token no longer accepted"
|
|
168
|
+
else
|
|
169
|
+
echo " ❌ FAIL: Token still valid (Returned $HTTP_CODE)"
|
|
170
|
+
fi
|