instaserve 1.1.18 → 1.1.21
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/api.txt +4 -4
- package/lib/auth0.js +1 -1
- package/lib/r2.js +30 -2
- package/package.json +2 -2
- package/public/index.html +346 -0
- package/routes.js +57 -0
- package/test_api.sh +59 -0
package/api.txt
CHANGED
|
@@ -41,7 +41,7 @@ POST /upload
|
|
|
41
41
|
Upload a file.
|
|
42
42
|
Body (JSON):
|
|
43
43
|
{
|
|
44
|
-
"bucket": "my-bucket-name",
|
|
44
|
+
"bucket": "my-bucket-name", // Optional if env var set
|
|
45
45
|
"key": "filename.jpg",
|
|
46
46
|
"body": "base64_encoded_string_contents",
|
|
47
47
|
"contentType": "image/jpeg"
|
|
@@ -49,13 +49,13 @@ POST /upload
|
|
|
49
49
|
|
|
50
50
|
POST /files
|
|
51
51
|
List files in a bucket.
|
|
52
|
-
Body (JSON): { "bucket": "my-bucket-name" }
|
|
52
|
+
Body (JSON): { "bucket": "my-bucket-name" } // Bucket optional if env var set
|
|
53
53
|
Returns: List of file objects (key, size, lastModified).
|
|
54
54
|
|
|
55
55
|
GET /download?bucket=my-bucket-name&key=filename.jpg
|
|
56
|
-
Download a file directly.
|
|
56
|
+
Download a file directly. Bucket optional if env var set.
|
|
57
57
|
(Available via the 'download' route handler)
|
|
58
58
|
|
|
59
59
|
GET /delete?bucket=my-bucket-name&key=filename.jpg
|
|
60
|
-
Delete a file.
|
|
60
|
+
Delete a file. Bucket optional if env var set.
|
|
61
61
|
(Available via the 'delete' route handler)
|
package/lib/auth0.js
CHANGED
|
@@ -41,7 +41,7 @@ export default {
|
|
|
41
41
|
if (token) {
|
|
42
42
|
global.sqlite.prepare("UPDATE users SET token = NULL WHERE token = ?").run(token);
|
|
43
43
|
}
|
|
44
|
-
res.setHeader("Set-Cookie", "token=; Path=/; HttpOnly; Secure; SameSite=
|
|
44
|
+
res.setHeader("Set-Cookie", "token=; Path=/; HttpOnly; Secure; SameSite=Strict");
|
|
45
45
|
res.writeHead(302, { Location: LOGGEDOUT_REDIRECT });
|
|
46
46
|
res.end();
|
|
47
47
|
},
|
package/lib/r2.js
CHANGED
|
@@ -5,6 +5,7 @@ import { URL } from 'url';
|
|
|
5
5
|
const cloudflareAccessKeyId = process.env.CLOUDFLARE_ACCESS_KEY_ID || process.env.cloudflareAccessKeyId;
|
|
6
6
|
const cloudflareSecretAccessKey = process.env.CLOUDFLARE_SECRET_ACCESS_KEY || process.env.cloudflareSecretAccessKey;
|
|
7
7
|
const cloudflareAccountId = process.env.CLOUDFLARE_ACCOUNT_ID || process.env.cloudflareAccountId;
|
|
8
|
+
const cloudflareBucket = process.env.CLOUDFLARE_BUCKET || process.env.cloudflareBucket || process.env.CLOUDFLARE_BUCKET_NAME || process.env.cloudflareBucketName;
|
|
8
9
|
|
|
9
10
|
if (!cloudflareAccessKeyId || !cloudflareSecretAccessKey || !cloudflareAccountId) {
|
|
10
11
|
console.warn("Cloudflare environment variables not set! Please set CLOUDFLARE_ACCESS_KEY_ID, CLOUDFLARE_SECRET_ACCESS_KEY, and CLOUDFLARE_ACCOUNT_ID.");
|
|
@@ -152,6 +153,7 @@ const getSecrets = ({ cloudflareAccessKeyId, cloudflareSecretAccessKey, cloudfla
|
|
|
152
153
|
);
|
|
153
154
|
|
|
154
155
|
async function uploadToR2(bucket, key, body, contentType, secrets) {
|
|
156
|
+
bucket = bucket || cloudflareBucket;
|
|
155
157
|
if (!body) body = Buffer.alloc(0);
|
|
156
158
|
const signed = signR2('PUT', bucket, key, body, ...getSecrets(secrets));
|
|
157
159
|
signed.headers['Content-Type'] = contentType || 'application/octet-stream';
|
|
@@ -160,6 +162,7 @@ async function uploadToR2(bucket, key, body, contentType, secrets) {
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
async function downloadFromR2(bucket, key, secrets) {
|
|
165
|
+
bucket = bucket || cloudflareBucket;
|
|
163
166
|
const signed = signR2('GET', bucket, key, null, ...getSecrets(secrets));
|
|
164
167
|
const { hostname, pathname, search } = new URL(signed.url);
|
|
165
168
|
|
|
@@ -187,11 +190,13 @@ async function downloadFromR2(bucket, key, secrets) {
|
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
async function deleteFromR2(bucket, key, secrets) {
|
|
193
|
+
bucket = bucket || cloudflareBucket;
|
|
190
194
|
await requestR2(signR2('DELETE', bucket, key, null, ...getSecrets(secrets)));
|
|
191
195
|
return { success: true };
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
async function listR2Files(bucket, prefix, secrets) {
|
|
199
|
+
bucket = bucket || cloudflareBucket;
|
|
195
200
|
const qs = prefix ? `list-type=2&prefix=${encodeURIComponent(prefix)}` : 'list-type=2';
|
|
196
201
|
|
|
197
202
|
const data = await requestR2(signR2('GET', bucket, '', null, ...getSecrets(secrets), qs));
|
|
@@ -204,6 +209,7 @@ async function listR2Files(bucket, prefix, secrets) {
|
|
|
204
209
|
}
|
|
205
210
|
|
|
206
211
|
function getSignedDownloadUrl(bucket, key, secrets) {
|
|
212
|
+
bucket = bucket || cloudflareBucket;
|
|
207
213
|
return signR2('GET', bucket, key, null, ...getSecrets(secrets)).url;
|
|
208
214
|
}
|
|
209
215
|
|
|
@@ -229,8 +235,30 @@ export const fileRoutes = {
|
|
|
229
235
|
},
|
|
230
236
|
download: async (req, res, data) => {
|
|
231
237
|
try {
|
|
232
|
-
|
|
233
|
-
|
|
238
|
+
const r2Msg = await downloadFromR2(data.bucket, data.key, data.secrets || defaultSecrets);
|
|
239
|
+
res.writeHead(r2Msg.statusCode, r2Msg.statusMessage, r2Msg.headers);
|
|
240
|
+
|
|
241
|
+
await new Promise((resolve, reject) => {
|
|
242
|
+
r2Msg.pipe(res);
|
|
243
|
+
r2Msg.on('error', (err) => {
|
|
244
|
+
res.destroy(err);
|
|
245
|
+
reject(err);
|
|
246
|
+
});
|
|
247
|
+
res.on('finish', resolve);
|
|
248
|
+
res.on('error', reject);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// returning nothing to prevent sendResponse from interfering
|
|
252
|
+
return;
|
|
253
|
+
} catch (e) {
|
|
254
|
+
// If headers weren't sent yet, we can return error JSON
|
|
255
|
+
if (!res.headersSent) {
|
|
256
|
+
return { error: e.message };
|
|
257
|
+
}
|
|
258
|
+
// If headers were sent, we can't cleanly return JSON,
|
|
259
|
+
// but the stream error handling above usually takes care of destroying the socket.
|
|
260
|
+
console.error("Download stream error:", e);
|
|
261
|
+
}
|
|
234
262
|
},
|
|
235
263
|
delete: async (req, res, data) => {
|
|
236
264
|
try {
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instaserve",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.21",
|
|
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 -api ./routes-full.js -secure -port 3001 -public ./public",
|
|
9
9
|
"test": "./test_api.sh"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Instaserve App</title>
|
|
7
|
+
<!-- Google Fonts for Modern Typography -->
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
|
|
11
|
+
<style>
|
|
12
|
+
:root {
|
|
13
|
+
/* HSL Color Palette - Vibrant & Premium */
|
|
14
|
+
--primary: 220 90% 56%; /* #2563eb */
|
|
15
|
+
--primary-dark: 220 90% 46%;
|
|
16
|
+
--bg-color: 220 30% 98%;
|
|
17
|
+
--card-bg: 0 0% 100%;
|
|
18
|
+
--text-main: 220 20% 15%;
|
|
19
|
+
--text-secondary: 220 15% 40%;
|
|
20
|
+
--success: 150 70% 40%;
|
|
21
|
+
--radius-md: 16px;
|
|
22
|
+
--radius-sm: 8px;
|
|
23
|
+
--shadow-soft: 0 10px 40px -10px rgba(37, 99, 235, 0.15);
|
|
24
|
+
--shadow-hover: 0 20px 50px -12px rgba(37, 99, 235, 0.25);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Dark Mode Support */
|
|
28
|
+
@media (prefers-color-scheme: dark) {
|
|
29
|
+
:root {
|
|
30
|
+
--bg-color: 220 30% 8%;
|
|
31
|
+
--card-bg: 220 25% 12%;
|
|
32
|
+
--text-main: 220 30% 95%;
|
|
33
|
+
--text-secondary: 220 15% 60%;
|
|
34
|
+
--shadow-soft: 0 10px 40px -10px rgba(0, 0, 0, 0.5);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Outfit', sans-serif; }
|
|
39
|
+
|
|
40
|
+
body {
|
|
41
|
+
background-color: hsl(var(--bg-color));
|
|
42
|
+
color: hsl(var(--text-main));
|
|
43
|
+
min-height: 100vh;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
49
|
+
overflow-x: hidden;
|
|
50
|
+
position: relative;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Subtle Background Gradient Orb */
|
|
54
|
+
.bg-orb {
|
|
55
|
+
position: absolute;
|
|
56
|
+
width: 600px;
|
|
57
|
+
height: 600px;
|
|
58
|
+
border-radius: 50%;
|
|
59
|
+
background: radial-gradient(circle, hsla(var(--primary), 0.15), transparent 70%);
|
|
60
|
+
top: -100px;
|
|
61
|
+
left: 50%;
|
|
62
|
+
transform: translateX(-50%);
|
|
63
|
+
z-index: -1;
|
|
64
|
+
pointer-events: none;
|
|
65
|
+
animation: pulse-orb 10s ease-in-out infinite alternate;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@keyframes pulse-orb {
|
|
69
|
+
0% { transform: translateX(-50%) scale(0.9); opacity: 0.8; }
|
|
70
|
+
100% { transform: translateX(-50%) scale(1.1); opacity: 1; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.container {
|
|
74
|
+
width: 100%;
|
|
75
|
+
max-width: 480px;
|
|
76
|
+
padding: 2rem;
|
|
77
|
+
text-align: center;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.auth-card {
|
|
81
|
+
background: hsl(var(--card-bg));
|
|
82
|
+
padding: 3rem 2rem;
|
|
83
|
+
border-radius: var(--radius-md);
|
|
84
|
+
box-shadow: var(--shadow-soft);
|
|
85
|
+
backdrop-filter: blur(10px);
|
|
86
|
+
border: 1px solid hsla(var(--primary), 0.1);
|
|
87
|
+
transform: translateY(0);
|
|
88
|
+
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.auth-card:hover {
|
|
92
|
+
transform: translateY(-5px);
|
|
93
|
+
box-shadow: var(--shadow-hover);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
h1 {
|
|
97
|
+
font-size: 2.5rem;
|
|
98
|
+
font-weight: 700;
|
|
99
|
+
margin-bottom: 0.5rem;
|
|
100
|
+
background: linear-gradient(135deg, hsl(var(--primary)), hsl(260 80% 60%));
|
|
101
|
+
-webkit-background-clip: text;
|
|
102
|
+
-webkit-text-fill-color: transparent;
|
|
103
|
+
letter-spacing: -0.03em;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
p.subtitle {
|
|
107
|
+
font-size: 1.1rem;
|
|
108
|
+
color: hsl(var(--text-secondary));
|
|
109
|
+
margin-bottom: 2rem;
|
|
110
|
+
line-height: 1.5;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* User Profile State */
|
|
114
|
+
.profile-view {
|
|
115
|
+
display: none; /* Hidden by default */
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
align-items: center;
|
|
118
|
+
gap: 1rem;
|
|
119
|
+
animation: fadeIn 0.5s ease;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.profile-pic {
|
|
123
|
+
width: 80px;
|
|
124
|
+
height: 80px;
|
|
125
|
+
border-radius: 50%;
|
|
126
|
+
object-fit: cover;
|
|
127
|
+
border: 3px solid hsl(var(--primary));
|
|
128
|
+
margin-bottom: 0.5rem;
|
|
129
|
+
box-shadow: 0 4px 12px hsla(var(--primary), 0.3);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.welcome-text {
|
|
133
|
+
font-size: 1.5rem;
|
|
134
|
+
font-weight: 600;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.status-badge {
|
|
138
|
+
background-color: hsla(var(--success), 0.1);
|
|
139
|
+
color: hsl(var(--success));
|
|
140
|
+
padding: 6px 16px;
|
|
141
|
+
border-radius: 20px;
|
|
142
|
+
font-size: 0.85rem;
|
|
143
|
+
font-weight: 600;
|
|
144
|
+
display: inline-flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
gap: 6px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.status-badge::before {
|
|
150
|
+
content: '';
|
|
151
|
+
width: 8px;
|
|
152
|
+
height: 8px;
|
|
153
|
+
background-color: currentColor;
|
|
154
|
+
border-radius: 50%;
|
|
155
|
+
display: inline-block;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Buttons */
|
|
159
|
+
.btn {
|
|
160
|
+
display: inline-block;
|
|
161
|
+
text-decoration: none;
|
|
162
|
+
padding: 14px 28px;
|
|
163
|
+
border-radius: var(--radius-sm);
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
font-size: 1rem;
|
|
166
|
+
transition: all 0.2s ease;
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
border: none;
|
|
169
|
+
width: 100%;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.btn-primary {
|
|
173
|
+
background-color: hsl(var(--primary));
|
|
174
|
+
color: white;
|
|
175
|
+
box-shadow: 0 4px 12px hsla(var(--primary), 0.4);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.btn-primary:hover {
|
|
179
|
+
background-color: hsl(var(--primary-dark));
|
|
180
|
+
transform: translateY(-2px);
|
|
181
|
+
box-shadow: 0 8px 16px hsla(var(--primary), 0.5);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.btn-secondary {
|
|
185
|
+
background-color: hsla(var(--text-main), 0.05);
|
|
186
|
+
color: hsl(var(--text-main));
|
|
187
|
+
margin-top: 1rem;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.btn-secondary:hover {
|
|
191
|
+
background-color: hsla(var(--text-main), 0.1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Loading/Guest State */
|
|
195
|
+
.guest-view {
|
|
196
|
+
display: none;
|
|
197
|
+
animation: fadeIn 0.5s ease;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Testing Controls Styles */
|
|
201
|
+
.btn-sm {
|
|
202
|
+
padding: 6px 12px;
|
|
203
|
+
border-radius: 6px;
|
|
204
|
+
border: 1px solid #ddd;
|
|
205
|
+
background: white;
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
font-size: 0.9rem;
|
|
208
|
+
transition: all 0.2s;
|
|
209
|
+
color: #333;
|
|
210
|
+
}
|
|
211
|
+
.btn-sm:hover {
|
|
212
|
+
background: #f9fafb;
|
|
213
|
+
border-color: #ccc;
|
|
214
|
+
}
|
|
215
|
+
.section-title { font-size: 1rem; color: hsl(var(--text-main)); font-weight: 600; margin-top: 1.5rem; text-align: left; }
|
|
216
|
+
.input-field { width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 0.5rem; }
|
|
217
|
+
|
|
218
|
+
@keyframes fadeIn {
|
|
219
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
220
|
+
to { opacity: 1; transform: translateY(0); }
|
|
221
|
+
}
|
|
222
|
+
</style>
|
|
223
|
+
</head>
|
|
224
|
+
<body>
|
|
225
|
+
<div class="bg-orb"></div>
|
|
226
|
+
<div class="container">
|
|
227
|
+
<div class="auth-card">
|
|
228
|
+
<h1>Instaserve</h1>
|
|
229
|
+
|
|
230
|
+
<!-- Guest View -->
|
|
231
|
+
<div id="guest-view" class="guest-view">
|
|
232
|
+
<p class="subtitle">Secure, fast, and instant web stack.<br>Sign in to access your dashboard.</p>
|
|
233
|
+
<a href="/login" class="btn btn-primary">Sign In with Auth0</a>
|
|
234
|
+
<div style="margin-top: 1.5rem; font-size: 0.9rem; color: hsl(var(--text-secondary));">
|
|
235
|
+
Powered by Node.js & SQLite
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<!-- Profile View -->
|
|
240
|
+
<div id="profile-view" class="profile-view">
|
|
241
|
+
<img id="user-pic" src="" alt="Profile Picture" class="profile-pic">
|
|
242
|
+
<div class="welcome-text">Hi, <span id="user-name">User</span>!</div>
|
|
243
|
+
<div class="status-badge">Authenticated</div>
|
|
244
|
+
|
|
245
|
+
<!-- Testing Controls -->
|
|
246
|
+
<div style="width: 100%; text-align: left; margin-top: 0.5rem; border-top: 1px solid hsla(var(--text-main), 0.1); padding-top: 1rem;">
|
|
247
|
+
<h3 class="section-title">SQLite KV Store</h3>
|
|
248
|
+
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
|
249
|
+
<input type="text" id="kv-key" placeholder="Key" style="flex: 1; padding: 8px; border-radius: 6px; border: 1px solid #ddd;">
|
|
250
|
+
<input type="text" id="kv-value" placeholder="Value" style="flex: 1; padding: 8px; border-radius: 6px; border: 1px solid #ddd;">
|
|
251
|
+
</div>
|
|
252
|
+
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem; flex-wrap: wrap;">
|
|
253
|
+
<button onclick="api('POST', '/api', { key: val('kv-key'), value: val('kv-value') })" class="btn-sm">Set</button>
|
|
254
|
+
<button onclick="api('GET', '/api?key=' + val('kv-key'))" class="btn-sm">Get</button>
|
|
255
|
+
<button onclick="api('DELETE', '/api', { key: val('kv-key') })" class="btn-sm">Delete</button>
|
|
256
|
+
<button onclick="api('GET', '/all')" class="btn-sm">List All</button>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<h3 class="section-title">R2 Storage</h3>
|
|
260
|
+
<div style="margin-top: 0.5rem;">
|
|
261
|
+
<input type="text" id="r2-bucket" placeholder="Bucket Name (Optional)" class="input-field">
|
|
262
|
+
<input type="file" id="r2-file" style="margin-bottom: 0.5rem; width: 100%;">
|
|
263
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
264
|
+
<button onclick="uploadFile()" class="btn-sm">Upload File</button>
|
|
265
|
+
<button onclick="listFiles()" class="btn-sm">List Files</button>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div id="output" style="margin-top: 1rem; background: #f3f4f6; padding: 10px; border-radius: 6px; font-family: monospace; font-size: 0.85rem; min-height: 60px; max-height: 200px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; color: #333;">Result output...</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<a href="/logout" class="btn btn-secondary" onclick="localStorage.clear()" style="margin-top: 1.5rem;">Sign Out</a>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<script>
|
|
279
|
+
// Check for user session in localStorage (set by auth0.js callback)
|
|
280
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
281
|
+
const name = localStorage.getItem('name');
|
|
282
|
+
const pic = localStorage.getItem('pic');
|
|
283
|
+
|
|
284
|
+
if (name) {
|
|
285
|
+
// User is logged in
|
|
286
|
+
document.getElementById('profile-view').style.display = 'flex';
|
|
287
|
+
document.getElementById('user-name').textContent = name;
|
|
288
|
+
if (pic) {
|
|
289
|
+
document.getElementById('user-pic').src = pic;
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
// User is guest
|
|
293
|
+
document.getElementById('guest-view').style.display = 'block';
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const val = (id) => document.getElementById(id).value;
|
|
298
|
+
const log = (data) => document.getElementById('output').textContent = typeof data === 'object' ? JSON.stringify(data, null, 2) : data;
|
|
299
|
+
|
|
300
|
+
async function api(method, url, body) {
|
|
301
|
+
try {
|
|
302
|
+
const opts = { method };
|
|
303
|
+
if (body && method !== 'GET') {
|
|
304
|
+
opts.headers = { 'Content-Type': 'application/json' };
|
|
305
|
+
opts.body = JSON.stringify(body);
|
|
306
|
+
}
|
|
307
|
+
const res = await fetch(url, opts);
|
|
308
|
+
// Try parsing JSON, fallback to text if fail or empty
|
|
309
|
+
const text = await res.text();
|
|
310
|
+
try {
|
|
311
|
+
log(JSON.parse(text));
|
|
312
|
+
} catch {
|
|
313
|
+
log(text || 'OK');
|
|
314
|
+
}
|
|
315
|
+
} catch (e) {
|
|
316
|
+
log('Error: ' + e.message);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function uploadFile() {
|
|
321
|
+
const bucket = val('r2-bucket');
|
|
322
|
+
const fileInput = document.getElementById('r2-file');
|
|
323
|
+
const file = fileInput.files[0];
|
|
324
|
+
if (!file) return log('Please select a file.');
|
|
325
|
+
|
|
326
|
+
const reader = new FileReader();
|
|
327
|
+
reader.onload = async () => {
|
|
328
|
+
const base64 = reader.result.split(',')[1];
|
|
329
|
+
await api('POST', '/upload', {
|
|
330
|
+
bucket,
|
|
331
|
+
key: file.name,
|
|
332
|
+
body: base64,
|
|
333
|
+
contentType: file.type
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
reader.readAsDataURL(file);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function listFiles() {
|
|
340
|
+
const bucket = val('r2-bucket');
|
|
341
|
+
|
|
342
|
+
await api('POST', '/files', { bucket });
|
|
343
|
+
}
|
|
344
|
+
</script>
|
|
345
|
+
</body>
|
|
346
|
+
</html>
|
package/routes.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
// Middleware functions (prefixed with _) run on every request
|
|
3
|
+
// Return false to continue processing, or a value to use as response
|
|
4
|
+
|
|
5
|
+
// Example: Log all requests
|
|
6
|
+
_log: (req, res, data) => {
|
|
7
|
+
console.log(`${req.method} ${req.url}`)
|
|
8
|
+
return false // Continue to next middleware or route
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
// Example: Basic authentication (commented out)
|
|
12
|
+
// _auth: (req, res, data) => {
|
|
13
|
+
// if (!data.token) {
|
|
14
|
+
// res.writeHead(401)
|
|
15
|
+
// return 'Unauthorized'
|
|
16
|
+
// }
|
|
17
|
+
// return false // Continue if authorized
|
|
18
|
+
// },
|
|
19
|
+
|
|
20
|
+
// Regular route handlers
|
|
21
|
+
hello: async (req, res, data) => {
|
|
22
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
23
|
+
res.setHeader('some', 'head')
|
|
24
|
+
return { message: 'Hello World' }
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
api: (req, res, data) => {
|
|
28
|
+
return { message: 'API endpoint', data }
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
example429: (req, res, data) => {
|
|
32
|
+
return 429; // This will return a status code 429
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
"POST /examplepost": (req, res, data) => {
|
|
36
|
+
return { message: 'Example POST endpoint', data }
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
"GET /sse": (req, res, data) => {
|
|
40
|
+
res.writeHead(200, {
|
|
41
|
+
'Content-Type': 'text/event-stream',
|
|
42
|
+
'Cache-Control': 'no-cache',
|
|
43
|
+
'Connection': 'keep-alive'
|
|
44
|
+
})
|
|
45
|
+
var x = 3;
|
|
46
|
+
const interval = setInterval(() => {
|
|
47
|
+
if (x <= 0) {
|
|
48
|
+
clearInterval(interval)
|
|
49
|
+
res.end()
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
res.write(`data: Hello World ${x}\n\n`)
|
|
53
|
+
x--
|
|
54
|
+
}, 1000)
|
|
55
|
+
return "SSE"
|
|
56
|
+
},
|
|
57
|
+
}
|
package/test_api.sh
CHANGED
|
@@ -139,6 +139,65 @@ else
|
|
|
139
139
|
echo " ❌ FAIL: $RESPONSE"
|
|
140
140
|
fi
|
|
141
141
|
|
|
142
|
+
# File API Tests
|
|
143
|
+
TEST_FILE_KEY="test_file.txt"
|
|
144
|
+
TEST_FILE_CONTENT="Hello R2 File Test"
|
|
145
|
+
# Use base64 without newlines
|
|
146
|
+
TEST_FILE_B64=$(echo -n "$TEST_FILE_CONTENT" | base64 | tr -d '\n')
|
|
147
|
+
|
|
148
|
+
echo ""
|
|
149
|
+
echo "--- File API Tests ---"
|
|
150
|
+
|
|
151
|
+
# Test 5.1: Upload File
|
|
152
|
+
echo "- Testing POST /upload..."
|
|
153
|
+
RESPONSE=$(curl -k -s -X POST "$BASE_URL/upload" \
|
|
154
|
+
-H "Cookie: token=$TEST_TOKEN" \
|
|
155
|
+
-H "Content-Type: application/json" \
|
|
156
|
+
-d "{\"key\": \"$TEST_FILE_KEY\", \"body\": \"$TEST_FILE_B64\", \"contentType\": \"text/plain\"}")
|
|
157
|
+
|
|
158
|
+
if [[ "$RESPONSE" == *"true"* ]]; then
|
|
159
|
+
echo " ✅ PASS"
|
|
160
|
+
else
|
|
161
|
+
echo " ❌ FAIL: $RESPONSE"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# Test 5.2: Download File
|
|
165
|
+
echo "- Testing GET /download..."
|
|
166
|
+
RESPONSE=$(curl -k -s "$BASE_URL/download?key=$TEST_FILE_KEY" \
|
|
167
|
+
-H "Cookie: token=$TEST_TOKEN")
|
|
168
|
+
|
|
169
|
+
if [[ "$RESPONSE" == *"$TEST_FILE_CONTENT"* ]]; then
|
|
170
|
+
echo " ✅ PASS: Content matches"
|
|
171
|
+
else
|
|
172
|
+
echo " ❌ FAIL: Content mismatch or error. Got: $RESPONSE"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Test 5.3: List Files
|
|
176
|
+
echo "- Testing POST /files (List)..."
|
|
177
|
+
RESPONSE=$(curl -k -s -X POST "$BASE_URL/files" \
|
|
178
|
+
-H "Cookie: token=$TEST_TOKEN" \
|
|
179
|
+
-H "Content-Type: application/json" \
|
|
180
|
+
-d "{}")
|
|
181
|
+
|
|
182
|
+
if [[ "$RESPONSE" == *"$TEST_FILE_KEY"* ]]; then
|
|
183
|
+
echo " ✅ PASS: Found file in list"
|
|
184
|
+
else
|
|
185
|
+
echo " ❌ FAIL: $RESPONSE"
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
# Test 5.4: Delete File
|
|
189
|
+
echo "- Testing POST /delete..."
|
|
190
|
+
RESPONSE=$(curl -k -s -X POST "$BASE_URL/delete" \
|
|
191
|
+
-H "Cookie: token=$TEST_TOKEN" \
|
|
192
|
+
-H "Content-Type: application/json" \
|
|
193
|
+
-d "{\"key\": \"$TEST_FILE_KEY\"}")
|
|
194
|
+
|
|
195
|
+
if [[ "$RESPONSE" == *"true"* ]]; then
|
|
196
|
+
echo " ✅ PASS"
|
|
197
|
+
else
|
|
198
|
+
echo " ❌ FAIL: $RESPONSE"
|
|
199
|
+
fi
|
|
200
|
+
|
|
142
201
|
# 7. Test Login Redirect
|
|
143
202
|
echo "- Testing GET /login (Expect 302 Redirect to Auth0)..."
|
|
144
203
|
LOGIN_HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" "$BASE_URL/login")
|