instaserve 1.1.12 → 1.1.15
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 +18 -1
- package/api.txt +61 -0
- package/instaserve +103 -2
- package/lib/auth0.js +27 -8
- package/lib/r2.js +23 -8
- package/lib/sqlite.js +3 -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
|
|
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/instaserve
CHANGED
|
@@ -4,19 +4,21 @@ import chalk from 'chalk'
|
|
|
4
4
|
import fs from 'node:fs'
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import { pathToFileURL, fileURLToPath } from 'node:url'
|
|
7
|
+
import { execSync } from 'node:child_process'
|
|
7
8
|
|
|
8
9
|
console.log(chalk.cyan('\nInstaserve - Instant Web Stack\n'))
|
|
9
10
|
console.log(chalk.yellow('Usage:'))
|
|
10
11
|
console.log(chalk.green(' npx instaserve [options]'))
|
|
11
12
|
console.log(chalk.green(' npx instaserve generate-routes\n'))
|
|
12
13
|
console.log(chalk.yellow('Commands:'))
|
|
13
|
-
console.log(chalk.green(' generate-routes') + ' Create a sample routes.js file in the current directory
|
|
14
|
+
console.log(chalk.green(' generate-routes') + ' Create a sample routes.js file in the current directory')
|
|
15
|
+
console.log(chalk.green(' generate-certs') + ' Generate self-signed SSL certificates\n')
|
|
14
16
|
console.log(chalk.yellow('Options:'))
|
|
15
17
|
console.log(chalk.green(' -port <number>') + ' Port to listen on (default: 3000)')
|
|
16
18
|
console.log(chalk.green(' -ip <address>') + ' IP address to bind to (default: 127.0.0.1)')
|
|
17
19
|
console.log(chalk.green(' -public <path>') + ' Public directory path (default: ./public)')
|
|
18
20
|
console.log(chalk.green(' -api <file>') + ' Path to routes file (default: ./routes.js)')
|
|
19
|
-
console.log(chalk.green(' -secure') + ' Enable HTTPS (requires cert.pem and key.pem: run generate-certs
|
|
21
|
+
console.log(chalk.green(' -secure') + ' Enable HTTPS (requires cert.pem and key.pem: run "npx instaserve generate-certs")')
|
|
20
22
|
console.log(chalk.green(' -help') + ' Show this help message\n')
|
|
21
23
|
|
|
22
24
|
if (process.argv.includes('-help')) {
|
|
@@ -76,6 +78,17 @@ if (args[0] === 'generate-routes') {
|
|
|
76
78
|
console.log(chalk.green(`✓ Created routes.js`))
|
|
77
79
|
|
|
78
80
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
81
|
+
|
|
82
|
+
// Copy routes-full.js
|
|
83
|
+
const sourceRoutesFull = path.join(__dirname, 'routes-full.js')
|
|
84
|
+
if (fs.existsSync(sourceRoutesFull)) {
|
|
85
|
+
const destRoutesFull = path.join(process.cwd(), 'routes-full.js')
|
|
86
|
+
if (!fs.existsSync(destRoutesFull)) {
|
|
87
|
+
fs.copyFileSync(sourceRoutesFull, destRoutesFull)
|
|
88
|
+
console.log(chalk.green(`✓ Created routes-full.js`))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
79
92
|
const sourceLib = path.join(__dirname, 'lib')
|
|
80
93
|
|
|
81
94
|
if (fs.existsSync(sourceLib)) {
|
|
@@ -91,6 +104,94 @@ if (args[0] === 'generate-routes') {
|
|
|
91
104
|
process.exit(0)
|
|
92
105
|
}
|
|
93
106
|
|
|
107
|
+
// Handle generate-certs command
|
|
108
|
+
if (args[0] === 'generate-certs') {
|
|
109
|
+
console.log(chalk.cyan('Generating self-signed certificates for Instaserve...'))
|
|
110
|
+
|
|
111
|
+
const openSslConfig = `[req]
|
|
112
|
+
distinguished_name = req_distinguished_name
|
|
113
|
+
req_extensions = v3_req
|
|
114
|
+
prompt = no
|
|
115
|
+
|
|
116
|
+
[req_distinguished_name]
|
|
117
|
+
C = US
|
|
118
|
+
ST = State
|
|
119
|
+
L = City
|
|
120
|
+
O = Instaserve
|
|
121
|
+
OU = Development
|
|
122
|
+
CN = localhost
|
|
123
|
+
|
|
124
|
+
[v3_req]
|
|
125
|
+
basicConstraints = CA:FALSE
|
|
126
|
+
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
|
127
|
+
extendedKeyUsage = serverAuth, clientAuth
|
|
128
|
+
subjectAltName = @alt_names
|
|
129
|
+
|
|
130
|
+
[alt_names]
|
|
131
|
+
DNS.1 = localhost
|
|
132
|
+
DNS.2 = *.localhost
|
|
133
|
+
IP.1 = 127.0.0.1
|
|
134
|
+
IP.2 = ::1
|
|
135
|
+
`
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
fs.writeFileSync('openssl.conf', openSslConfig)
|
|
139
|
+
|
|
140
|
+
// Generate private key
|
|
141
|
+
try {
|
|
142
|
+
execSync('openssl genrsa -out key.pem 2048', { stdio: 'inherit' })
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.error(chalk.red('Error running openssl genrsa. Ensure openssl is installed.'))
|
|
145
|
+
throw e
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Generate certificate
|
|
149
|
+
try {
|
|
150
|
+
execSync('openssl req -new -x509 -key key.pem -out cert.pem -days 365 -config openssl.conf -extensions v3_req', { stdio: 'inherit' })
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.error(chalk.red('Error running openssl req.'))
|
|
153
|
+
throw e
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Cleanup
|
|
157
|
+
if (fs.existsSync('openssl.conf')) fs.unlinkSync('openssl.conf')
|
|
158
|
+
|
|
159
|
+
console.log(chalk.green('Certificates generated: cert.pem and key.pem'))
|
|
160
|
+
|
|
161
|
+
// Add to trust store
|
|
162
|
+
if (process.platform === 'darwin') {
|
|
163
|
+
console.log(chalk.yellow('Adding certificate to macOS trust store...'))
|
|
164
|
+
try {
|
|
165
|
+
execSync('sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain cert.pem', { stdio: 'inherit' })
|
|
166
|
+
console.log(chalk.green('Certificate added to macOS trust store'))
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.log(chalk.red('Failed to add to trust store (sudo skipped or failed). You can add it manually.'))
|
|
169
|
+
}
|
|
170
|
+
} else if (process.platform === 'linux') {
|
|
171
|
+
console.log(chalk.yellow('Adding certificate to Linux trust store...'))
|
|
172
|
+
try {
|
|
173
|
+
execSync('sudo cp cert.pem /usr/local/share/ca-certificates/instaserve.crt', { stdio: 'inherit' })
|
|
174
|
+
execSync('sudo update-ca-certificates', { stdio: 'inherit' })
|
|
175
|
+
console.log(chalk.green('Certificate added to Linux trust store'))
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.log(chalk.red('Failed to add to trust store. Manual addition required.'))
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
console.log(chalk.yellow("Please manually add cert.pem to your system's trust store"))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(chalk.green("HTTPS certificates ready! Use 'npx instaserve -secure' to enable HTTPS"))
|
|
184
|
+
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error(chalk.red('Error generating certificates:'), error.message)
|
|
187
|
+
// Cleanup if failed
|
|
188
|
+
if (fs.existsSync('openssl.conf')) fs.unlinkSync('openssl.conf')
|
|
189
|
+
process.exit(1)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
process.exit(0)
|
|
193
|
+
}
|
|
194
|
+
|
|
94
195
|
import server from './module.mjs'
|
|
95
196
|
|
|
96
197
|
const params = {}
|
package/lib/auth0.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
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
|
+
|
|
1
14
|
/**
|
|
2
15
|
* Auth0 Authentication Module
|
|
3
16
|
*
|
|
@@ -12,12 +25,13 @@
|
|
|
12
25
|
* - GET /logout: Clears session and token.
|
|
13
26
|
* - GET /callback: Handles Auth0 callback, validates token, creates user in DB.
|
|
14
27
|
*/
|
|
28
|
+
|
|
15
29
|
export default {
|
|
16
30
|
"GET /login": (req, res) => {
|
|
17
31
|
const REDIRECT_URI = `https://${req.headers.host}/callback`;
|
|
18
32
|
const state = crypto.randomBytes(16).toString("hex");
|
|
19
33
|
global.a0state = state;
|
|
20
|
-
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)}`;
|
|
21
35
|
res.writeHead(302, { Location: url });
|
|
22
36
|
res.end();
|
|
23
37
|
},
|
|
@@ -28,32 +42,37 @@ export default {
|
|
|
28
42
|
global.sqlite.prepare("UPDATE users SET token = NULL WHERE token = ?").run(token);
|
|
29
43
|
}
|
|
30
44
|
res.setHeader("Set-Cookie", "token=; Path=/; HttpOnly; Secure; SameSite=Lax");
|
|
31
|
-
res.writeHead(302, { Location:
|
|
45
|
+
res.writeHead(302, { Location: LOGGEDOUT_REDIRECT });
|
|
32
46
|
res.end();
|
|
33
47
|
},
|
|
34
48
|
"GET /callback": async (req, res, data) => {
|
|
35
49
|
const REDIRECT_URI = `https://${req.headers.host}/callback`;
|
|
36
|
-
const tokenRes = await fetch(`https://${
|
|
50
|
+
const tokenRes = await fetch(`https://${auth0_domain}/oauth/token`, {
|
|
37
51
|
method: "POST",
|
|
38
52
|
headers: { "content-type": "application/json" },
|
|
39
53
|
body: JSON.stringify({
|
|
40
54
|
grant_type: "authorization_code",
|
|
41
|
-
client_id:
|
|
42
|
-
client_secret:
|
|
55
|
+
client_id: auth0_clientid,
|
|
56
|
+
client_secret: auth0_clientsecret,
|
|
43
57
|
code: data.code,
|
|
44
58
|
redirect_uri: REDIRECT_URI
|
|
45
59
|
}),
|
|
46
|
-
})
|
|
60
|
+
});
|
|
61
|
+
|
|
47
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
|
+
}
|
|
48
67
|
// decrypt token
|
|
49
68
|
const id_token = tokens.id_token;
|
|
50
69
|
const payload = JSON.parse(Buffer.from(id_token.split('.')[1], 'base64').toString());
|
|
51
70
|
const auth_token = crypto.randomBytes(32).toString("hex");
|
|
52
|
-
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 = ?")
|
|
53
72
|
.run(payload.email, payload.name, auth_token, auth_token, payload.name, payload.picture);
|
|
54
73
|
res.setHeader("Set-Cookie", `username=${payload.email}; Path=/;`);
|
|
55
74
|
res.setHeader("Set-Cookie", `name=${payload.name}; Path=/;`);
|
|
56
75
|
res.setHeader("Set-Cookie", `token=${auth_token}; Path=/;`);
|
|
57
|
-
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>`;
|
|
58
77
|
}
|
|
59
78
|
}
|
package/lib/r2.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { URL } from 'url';
|
|
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
|
+
|
|
1
13
|
/**
|
|
2
14
|
* Cloudflare R2 / S3-Compatible Storage Module
|
|
3
15
|
*
|
|
@@ -19,9 +31,6 @@
|
|
|
19
31
|
* Contains helper handlers for file operations. Note that `upload`, `download`, `delete`
|
|
20
32
|
* are generic async functions awaiting `data` parameters, while `POST /files` is a standard route.
|
|
21
33
|
*/
|
|
22
|
-
import https from 'https';
|
|
23
|
-
import crypto from 'crypto';
|
|
24
|
-
import { URL } from 'url';
|
|
25
34
|
|
|
26
35
|
// --- Core SigV4 Utilities ---
|
|
27
36
|
|
|
@@ -198,6 +207,12 @@ function getSignedDownloadUrl(bucket, key, secrets) {
|
|
|
198
207
|
return signR2('GET', bucket, key, null, ...getSecrets(secrets)).url;
|
|
199
208
|
}
|
|
200
209
|
|
|
210
|
+
const defaultSecrets = {
|
|
211
|
+
cloudflareAccessKeyId,
|
|
212
|
+
cloudflareSecretAccessKey,
|
|
213
|
+
cloudflareAccountId
|
|
214
|
+
};
|
|
215
|
+
|
|
201
216
|
export const fileRoutes = {
|
|
202
217
|
upload: async (req, res, data) => {
|
|
203
218
|
try {
|
|
@@ -209,27 +224,27 @@ export const fileRoutes = {
|
|
|
209
224
|
} else {
|
|
210
225
|
body = Buffer.alloc(0);
|
|
211
226
|
}
|
|
212
|
-
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);
|
|
213
228
|
} catch (e) { return { error: e.message }; }
|
|
214
229
|
},
|
|
215
230
|
download: async (req, res, data) => {
|
|
216
231
|
try {
|
|
217
|
-
return await downloadFromR2(data.bucket, data.key, data.secrets);
|
|
232
|
+
return await downloadFromR2(data.bucket, data.key, data.secrets || defaultSecrets);
|
|
218
233
|
} catch (e) { return { error: e.message }; }
|
|
219
234
|
},
|
|
220
235
|
delete: async (req, res, data) => {
|
|
221
236
|
try {
|
|
222
|
-
return await deleteFromR2(data.bucket, data.key, data.secrets);
|
|
237
|
+
return await deleteFromR2(data.bucket, data.key, data.secrets || defaultSecrets);
|
|
223
238
|
} catch (e) { return { error: e.message }; }
|
|
224
239
|
},
|
|
225
240
|
"POST /files": async (req, res, data) => {
|
|
226
241
|
try {
|
|
227
|
-
return await listR2Files(data.bucket, data.prefix, data.secrets);
|
|
242
|
+
return await listR2Files(data.bucket, data.prefix, data.secrets || defaultSecrets);
|
|
228
243
|
} catch (e) { return { error: e.message }; }
|
|
229
244
|
}
|
|
230
245
|
};
|
|
231
246
|
|
|
232
|
-
export {
|
|
247
|
+
export default {
|
|
233
248
|
uploadToR2,
|
|
234
249
|
downloadFromR2,
|
|
235
250
|
deleteFromR2,
|
package/lib/sqlite.js
CHANGED
|
@@ -17,10 +17,8 @@
|
|
|
17
17
|
* - GET /all: List all keys/values for the user.
|
|
18
18
|
* - DELETE /api: Delete a key.
|
|
19
19
|
*/
|
|
20
|
+
|
|
20
21
|
import Database from "better-sqlite3";
|
|
21
|
-
import { fileRoutes } from "./lib/file-routes.js";
|
|
22
|
-
import secrets from "./secrets.js";
|
|
23
|
-
import crypto from "crypto";
|
|
24
22
|
|
|
25
23
|
// Initialize database
|
|
26
24
|
global.sqlite = new Database("data.db");
|
|
@@ -30,8 +28,8 @@ sqlite.prepare("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)
|
|
|
30
28
|
// Combine all routes
|
|
31
29
|
export default {
|
|
32
30
|
_auth: (req, res, data) => {
|
|
33
|
-
if (req.url.match(/js|html|css/)) return;
|
|
34
|
-
|
|
31
|
+
if (req.url === '/' || req.url.match(/js|html|css|callback/)) return;
|
|
32
|
+
|
|
35
33
|
if (req.url.startsWith("/register") || req.url.startsWith("/login")) return;
|
|
36
34
|
|
|
37
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.15",
|
|
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
|