mantenimento-app 2.2.8 → 2.3.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/README.md +97 -34
- package/app.js +558 -102
- package/backend/server.js +346 -23
- package/frontend/public/app.js +558 -102
- package/frontend/public/autologin.html +40 -0
- package/frontend/public/index.html +29 -6
- package/frontend/public/styles.css +78 -5
- package/frontend/public/supabase-config.js +4 -11
- package/package.json +5 -1
- package/scripts/auth-url-check.mjs +166 -0
- package/scripts/create-url-login-token.mjs +52 -0
- package/scripts/manage-donor-users.mjs +229 -0
- package/scripts/sql/grant-donor.sql +22 -0
- package/scripts/sql/revoke-donor.sql +19 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
function loadDotEnvFromWorkspaceRoot() {
|
|
6
|
+
try {
|
|
7
|
+
const root = process.cwd();
|
|
8
|
+
const envPath = path.join(root, '.env');
|
|
9
|
+
if (!fs.existsSync(envPath)) return;
|
|
10
|
+
|
|
11
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
12
|
+
const lines = String(content || '').split(/\r?\n/);
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
const trimmed = String(line || '').trim();
|
|
15
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
16
|
+
const idx = trimmed.indexOf('=');
|
|
17
|
+
if (idx <= 0) continue;
|
|
18
|
+
const key = trimmed.slice(0, idx).trim();
|
|
19
|
+
const value = trimmed.slice(idx + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
20
|
+
if (!key || process.env[key] != null) continue;
|
|
21
|
+
process.env[key] = value;
|
|
22
|
+
}
|
|
23
|
+
} catch (_) {
|
|
24
|
+
// best effort only
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
loadDotEnvFromWorkspaceRoot();
|
|
29
|
+
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const getArg = (name, fallback = '') => {
|
|
32
|
+
const prefix = `--${name}=`;
|
|
33
|
+
const found = args.find((arg) => arg.startsWith(prefix));
|
|
34
|
+
return found ? found.slice(prefix.length) : fallback;
|
|
35
|
+
};
|
|
36
|
+
const hasFlag = (name) => args.includes(`--${name}`);
|
|
37
|
+
|
|
38
|
+
const mode = String(getArg('mode', '') || '').trim().toLowerCase();
|
|
39
|
+
const usernamesRaw = String(getArg('users', '') || '').trim();
|
|
40
|
+
const emailsRaw = String(getArg('emails', '') || '').trim();
|
|
41
|
+
const dryRun = hasFlag('dry-run');
|
|
42
|
+
|
|
43
|
+
const supabaseUrl = String(process.env.DONOR_ADMIN_SUPABASE_URL || process.env.AUTH_URL_LOGIN_SUPABASE_URL || '').trim().replace(/\/+$/, '');
|
|
44
|
+
const serviceRoleKey = String(process.env.DONOR_ADMIN_SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || '').trim();
|
|
45
|
+
|
|
46
|
+
function fail(message) {
|
|
47
|
+
console.error(message);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseCsv(value) {
|
|
52
|
+
return String(value || '')
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((v) => v.trim().toLowerCase())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function usage() {
|
|
59
|
+
console.log([
|
|
60
|
+
'Usage:',
|
|
61
|
+
' node scripts/manage-donor-users.mjs --mode=grant --users=favagit,fabio.vacchino',
|
|
62
|
+
' node scripts/manage-donor-users.mjs --mode=revoke --users=favagit',
|
|
63
|
+
' node scripts/manage-donor-users.mjs --mode=grant --emails=user1@example.com,user2@example.com',
|
|
64
|
+
'',
|
|
65
|
+
'Options:',
|
|
66
|
+
' --mode=grant|revoke Required',
|
|
67
|
+
' --users=a,b,c Comma-separated username local parts (email before @)',
|
|
68
|
+
' --emails=a@x.com,b@y.it Comma-separated full emails',
|
|
69
|
+
' --dry-run Print actions without writing changes',
|
|
70
|
+
'',
|
|
71
|
+
'Environment variables:',
|
|
72
|
+
' DONOR_ADMIN_SUPABASE_URL',
|
|
73
|
+
' DONOR_ADMIN_SUPABASE_SERVICE_ROLE_KEY'
|
|
74
|
+
].join('\n'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!mode || !['grant', 'revoke'].includes(mode)) {
|
|
78
|
+
usage();
|
|
79
|
+
fail('\nMissing or invalid --mode. Use grant or revoke.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const targetUsers = new Set(parseCsv(usernamesRaw));
|
|
83
|
+
const targetEmails = new Set(parseCsv(emailsRaw));
|
|
84
|
+
if (!targetUsers.size && !targetEmails.size) {
|
|
85
|
+
usage();
|
|
86
|
+
fail('\nSpecify at least one target via --users or --emails.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!supabaseUrl) {
|
|
90
|
+
fail('Missing DONOR_ADMIN_SUPABASE_URL (or AUTH_URL_LOGIN_SUPABASE_URL).');
|
|
91
|
+
}
|
|
92
|
+
if (!serviceRoleKey) {
|
|
93
|
+
fail('Missing DONOR_ADMIN_SUPABASE_SERVICE_ROLE_KEY (or SUPABASE_SERVICE_ROLE_KEY).');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const headers = {
|
|
97
|
+
apikey: serviceRoleKey,
|
|
98
|
+
Authorization: `Bearer ${serviceRoleKey}`,
|
|
99
|
+
'Content-Type': 'application/json'
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
async function fetchJson(url, options = {}) {
|
|
103
|
+
const res = await fetch(url, options);
|
|
104
|
+
const body = await res.json().catch(() => ({}));
|
|
105
|
+
return { ok: res.ok, status: res.status, body };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function listAllUsers() {
|
|
109
|
+
const out = [];
|
|
110
|
+
let page = 1;
|
|
111
|
+
while (true) {
|
|
112
|
+
const url = `${supabaseUrl}/auth/v1/admin/users?page=${page}&per_page=1000`;
|
|
113
|
+
const response = await fetchJson(url, { headers });
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const err = String(response.body?.msg || response.body?.error || `HTTP ${response.status}`);
|
|
116
|
+
fail(`Cannot list users: ${err}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const users = Array.isArray(response.body?.users) ? response.body.users : [];
|
|
120
|
+
out.push(...users);
|
|
121
|
+
if (!users.length || users.length < 1000) break;
|
|
122
|
+
page += 1;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function emailLocalPart(email) {
|
|
128
|
+
return String(email || '').trim().toLowerCase().split('@')[0] || '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function shouldTargetUser(user) {
|
|
132
|
+
const email = String(user?.email || '').trim().toLowerCase();
|
|
133
|
+
const local = emailLocalPart(email);
|
|
134
|
+
if (targetEmails.has(email)) return true;
|
|
135
|
+
if (targetUsers.has(local)) return true;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function truthy(value) {
|
|
140
|
+
if (value === true || value === 1) return true;
|
|
141
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
142
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function patchUserMetadata(existingMeta, selectedMode) {
|
|
146
|
+
const next = { ...(existingMeta && typeof existingMeta === 'object' ? existingMeta : {}) };
|
|
147
|
+
if (selectedMode === 'grant') {
|
|
148
|
+
next.is_donor = true;
|
|
149
|
+
next.role = 'donor';
|
|
150
|
+
return next;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
delete next.is_donor;
|
|
154
|
+
delete next.isDonor;
|
|
155
|
+
delete next.premium;
|
|
156
|
+
if (String(next.role || '').trim().toLowerCase() === 'donor') {
|
|
157
|
+
delete next.role;
|
|
158
|
+
}
|
|
159
|
+
return next;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isAlreadyAligned(existingMeta, selectedMode) {
|
|
163
|
+
const meta = existingMeta && typeof existingMeta === 'object' ? existingMeta : {};
|
|
164
|
+
if (selectedMode === 'grant') {
|
|
165
|
+
return truthy(meta.is_donor) && String(meta.role || '').trim().toLowerCase() === 'donor';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return !truthy(meta.is_donor)
|
|
169
|
+
&& !truthy(meta.isDonor)
|
|
170
|
+
&& !truthy(meta.premium)
|
|
171
|
+
&& String(meta.role || '').trim().toLowerCase() !== 'donor';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function updateUserMetadata(userId, nextMeta) {
|
|
175
|
+
const url = `${supabaseUrl}/auth/v1/admin/users/${encodeURIComponent(userId)}`;
|
|
176
|
+
const response = await fetchJson(url, {
|
|
177
|
+
method: 'PUT',
|
|
178
|
+
headers,
|
|
179
|
+
body: JSON.stringify({ user_metadata: nextMeta })
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
const err = String(response.body?.msg || response.body?.error || `HTTP ${response.status}`);
|
|
184
|
+
throw new Error(err);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
(async () => {
|
|
189
|
+
const allUsers = await listAllUsers();
|
|
190
|
+
const targets = allUsers.filter(shouldTargetUser);
|
|
191
|
+
|
|
192
|
+
if (!targets.length) {
|
|
193
|
+
console.log('No matching users found.');
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let changed = 0;
|
|
198
|
+
let skipped = 0;
|
|
199
|
+
|
|
200
|
+
for (const user of targets) {
|
|
201
|
+
const email = String(user?.email || '').trim().toLowerCase();
|
|
202
|
+
const userId = String(user?.id || '').trim();
|
|
203
|
+
const currentMeta = user?.user_metadata && typeof user.user_metadata === 'object' ? user.user_metadata : {};
|
|
204
|
+
|
|
205
|
+
if (!userId || !email) {
|
|
206
|
+
skipped += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (isAlreadyAligned(currentMeta, mode)) {
|
|
211
|
+
console.log(`[skip] ${email} already aligned for mode=${mode}`);
|
|
212
|
+
skipped += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const nextMeta = patchUserMetadata(currentMeta, mode);
|
|
217
|
+
if (dryRun) {
|
|
218
|
+
console.log(`[dry-run] would ${mode} donor for ${email}`);
|
|
219
|
+
changed += 1;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await updateUserMetadata(userId, nextMeta);
|
|
224
|
+
console.log(`[ok] ${mode} donor for ${email}`);
|
|
225
|
+
changed += 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log(`Done. targets=${targets.length} changed=${changed} skipped=${skipped} dryRun=${dryRun}`);
|
|
229
|
+
})();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Grant donor/premium entitlement globally (server-side) to selected users.
|
|
2
|
+
-- Usage in Supabase SQL Editor:
|
|
3
|
+
-- 1) Edit the usernames/emails in the WHERE clause.
|
|
4
|
+
-- 2) Run the script.
|
|
5
|
+
|
|
6
|
+
update auth.users
|
|
7
|
+
set raw_user_meta_data =
|
|
8
|
+
coalesce(raw_user_meta_data, '{}'::jsonb)
|
|
9
|
+
|| jsonb_build_object(
|
|
10
|
+
'is_donor', true,
|
|
11
|
+
'role', 'donor'
|
|
12
|
+
)
|
|
13
|
+
where lower(split_part(email, '@', 1)) in ('favagit', 'fabio.vacchino');
|
|
14
|
+
|
|
15
|
+
-- Verification
|
|
16
|
+
select
|
|
17
|
+
id,
|
|
18
|
+
email,
|
|
19
|
+
raw_user_meta_data->>'is_donor' as is_donor,
|
|
20
|
+
raw_user_meta_data->>'role' as role
|
|
21
|
+
from auth.users
|
|
22
|
+
where lower(split_part(email, '@', 1)) in ('favagit', 'fabio.vacchino');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Revoke donor/premium entitlement globally (server-side) from selected users.
|
|
2
|
+
-- Usage in Supabase SQL Editor:
|
|
3
|
+
-- 1) Edit the usernames/emails in the WHERE clause.
|
|
4
|
+
-- 2) Run the script.
|
|
5
|
+
|
|
6
|
+
update auth.users
|
|
7
|
+
set raw_user_meta_data =
|
|
8
|
+
(coalesce(raw_user_meta_data, '{}'::jsonb) - 'is_donor' - 'isDonor' - 'premium' - 'role')
|
|
9
|
+
where lower(split_part(email, '@', 1)) in ('favagit', 'fabio.vacchino');
|
|
10
|
+
|
|
11
|
+
-- Verification
|
|
12
|
+
select
|
|
13
|
+
id,
|
|
14
|
+
email,
|
|
15
|
+
raw_user_meta_data->>'is_donor' as is_donor,
|
|
16
|
+
raw_user_meta_data->>'role' as role,
|
|
17
|
+
raw_user_meta_data->>'premium' as premium
|
|
18
|
+
from auth.users
|
|
19
|
+
where lower(split_part(email, '@', 1)) in ('favagit', 'fabio.vacchino');
|