shiva-code 0.8.18 → 0.8.19
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.
|
@@ -19,853 +19,6 @@ import {
|
|
|
19
19
|
import { Command } from "commander";
|
|
20
20
|
import inquirer2 from "inquirer";
|
|
21
21
|
|
|
22
|
-
// src/services/auth/auth.ts
|
|
23
|
-
import inquirer from "inquirer";
|
|
24
|
-
import open from "open";
|
|
25
|
-
import http from "http";
|
|
26
|
-
import { URL } from "url";
|
|
27
|
-
|
|
28
|
-
// src/services/auth/two-factor.ts
|
|
29
|
-
import * as crypto from "crypto";
|
|
30
|
-
function generateSecret(length = 20) {
|
|
31
|
-
const buffer = crypto.randomBytes(length);
|
|
32
|
-
return base32Encode(buffer);
|
|
33
|
-
}
|
|
34
|
-
function base32Encode(buffer) {
|
|
35
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
36
|
-
let result = "";
|
|
37
|
-
let bits = 0;
|
|
38
|
-
let value = 0;
|
|
39
|
-
for (let i = 0; i < buffer.length; i++) {
|
|
40
|
-
value = value << 8 | buffer[i];
|
|
41
|
-
bits += 8;
|
|
42
|
-
while (bits >= 5) {
|
|
43
|
-
result += alphabet[value >>> bits - 5 & 31];
|
|
44
|
-
bits -= 5;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (bits > 0) {
|
|
48
|
-
result += alphabet[value << 5 - bits & 31];
|
|
49
|
-
}
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
function base32Decode(encoded) {
|
|
53
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
54
|
-
const lookup = /* @__PURE__ */ new Map();
|
|
55
|
-
for (let i = 0; i < alphabet.length; i++) {
|
|
56
|
-
lookup.set(alphabet[i], i);
|
|
57
|
-
}
|
|
58
|
-
const bytes = [];
|
|
59
|
-
let bits = 0;
|
|
60
|
-
let value = 0;
|
|
61
|
-
for (const char of encoded.toUpperCase()) {
|
|
62
|
-
if (char === "=") continue;
|
|
63
|
-
const index = lookup.get(char);
|
|
64
|
-
if (index === void 0) continue;
|
|
65
|
-
value = value << 5 | index;
|
|
66
|
-
bits += 5;
|
|
67
|
-
if (bits >= 8) {
|
|
68
|
-
bytes.push(value >>> bits - 8 & 255);
|
|
69
|
-
bits -= 8;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return Buffer.from(bytes);
|
|
73
|
-
}
|
|
74
|
-
function generateTOTP(secret, timeStep = 30) {
|
|
75
|
-
const time = Math.floor(Date.now() / 1e3 / timeStep);
|
|
76
|
-
const key = base32Decode(secret);
|
|
77
|
-
const counter = Buffer.alloc(8);
|
|
78
|
-
counter.writeBigUInt64BE(BigInt(time));
|
|
79
|
-
const hmac = crypto.createHmac("sha1", key);
|
|
80
|
-
hmac.update(counter);
|
|
81
|
-
const hash = hmac.digest();
|
|
82
|
-
const offset = hash[hash.length - 1] & 15;
|
|
83
|
-
const code = ((hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255) % 1e6;
|
|
84
|
-
return code.toString().padStart(6, "0");
|
|
85
|
-
}
|
|
86
|
-
function verifyTOTP(secret, code, timeStep = 30) {
|
|
87
|
-
for (let i = -1; i <= 1; i++) {
|
|
88
|
-
const time = Math.floor(Date.now() / 1e3 / timeStep) + i;
|
|
89
|
-
const expectedCode = generateTOTPForTime(secret, time, timeStep);
|
|
90
|
-
if (expectedCode === code) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
function generateTOTPForTime(secret, time, timeStep) {
|
|
97
|
-
const key = base32Decode(secret);
|
|
98
|
-
const counter = Buffer.alloc(8);
|
|
99
|
-
counter.writeBigUInt64BE(BigInt(time));
|
|
100
|
-
const hmac = crypto.createHmac("sha1", key);
|
|
101
|
-
hmac.update(counter);
|
|
102
|
-
const hash = hmac.digest();
|
|
103
|
-
const offset = hash[hash.length - 1] & 15;
|
|
104
|
-
const code = ((hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255) % 1e6;
|
|
105
|
-
return code.toString().padStart(6, "0");
|
|
106
|
-
}
|
|
107
|
-
function generateOtpAuthUrl(secret, email, issuer = "SHIVA Code") {
|
|
108
|
-
const encodedIssuer = encodeURIComponent(issuer);
|
|
109
|
-
const encodedEmail = encodeURIComponent(email);
|
|
110
|
-
return `otpauth://totp/${encodedIssuer}:${encodedEmail}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=30`;
|
|
111
|
-
}
|
|
112
|
-
function generateBackupCodes(count = 10) {
|
|
113
|
-
const codes = [];
|
|
114
|
-
for (let i = 0; i < count; i++) {
|
|
115
|
-
const buffer = crypto.randomBytes(6);
|
|
116
|
-
const code = buffer.toString("hex").toUpperCase();
|
|
117
|
-
codes.push(`${code.slice(0, 3)}-${code.slice(3, 6)}-${code.slice(6, 9)}-${code.slice(9, 12)}`);
|
|
118
|
-
}
|
|
119
|
-
return codes;
|
|
120
|
-
}
|
|
121
|
-
function generateDeviceFingerprint() {
|
|
122
|
-
const os = __require("os");
|
|
123
|
-
const components = [
|
|
124
|
-
os.hostname(),
|
|
125
|
-
os.platform(),
|
|
126
|
-
os.arch(),
|
|
127
|
-
os.cpus()[0]?.model || "unknown",
|
|
128
|
-
os.userInfo().username
|
|
129
|
-
];
|
|
130
|
-
const fingerprint = crypto.createHash("sha256").update(components.join("|")).digest("hex").slice(0, 32);
|
|
131
|
-
return fingerprint;
|
|
132
|
-
}
|
|
133
|
-
function getDeviceName() {
|
|
134
|
-
const os = __require("os");
|
|
135
|
-
const hostname = os.hostname();
|
|
136
|
-
const platform = os.platform();
|
|
137
|
-
const platformNames = {
|
|
138
|
-
darwin: "macOS",
|
|
139
|
-
linux: "Linux",
|
|
140
|
-
win32: "Windows"
|
|
141
|
-
};
|
|
142
|
-
return `${hostname} (${platformNames[platform] || platform})`;
|
|
143
|
-
}
|
|
144
|
-
var TwoFactorService = class {
|
|
145
|
-
/**
|
|
146
|
-
* Start 2FA setup - generate secret and QR code
|
|
147
|
-
*/
|
|
148
|
-
async startSetup(email) {
|
|
149
|
-
try {
|
|
150
|
-
const response = await api.setup2FA();
|
|
151
|
-
if (!response.setup) {
|
|
152
|
-
throw new Error(response.message || "2FA-Setup fehlgeschlagen");
|
|
153
|
-
}
|
|
154
|
-
return response.setup;
|
|
155
|
-
} catch (error) {
|
|
156
|
-
const secret = generateSecret();
|
|
157
|
-
const otpAuthUrl = generateOtpAuthUrl(secret, email);
|
|
158
|
-
const backupCodes = generateBackupCodes();
|
|
159
|
-
return {
|
|
160
|
-
secret,
|
|
161
|
-
qrCodeDataUrl: otpAuthUrl,
|
|
162
|
-
// In real implementation, would be a data URL
|
|
163
|
-
backupCodes
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Verify TOTP code and complete setup
|
|
169
|
-
*/
|
|
170
|
-
async verifySetup(code) {
|
|
171
|
-
try {
|
|
172
|
-
const response = await api.verify2FA(code);
|
|
173
|
-
return response.success;
|
|
174
|
-
} catch (error) {
|
|
175
|
-
throw error;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Verify TOTP code during login
|
|
180
|
-
*/
|
|
181
|
-
async verifyCode(code) {
|
|
182
|
-
try {
|
|
183
|
-
const response = await api.verify2FACode(code);
|
|
184
|
-
return response.success;
|
|
185
|
-
} catch (error) {
|
|
186
|
-
throw error;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Verify backup code
|
|
191
|
-
*/
|
|
192
|
-
async verifyBackupCode(code) {
|
|
193
|
-
try {
|
|
194
|
-
const response = await api.verifyBackupCode(code);
|
|
195
|
-
return response.success;
|
|
196
|
-
} catch (error) {
|
|
197
|
-
throw error;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Get 2FA status
|
|
202
|
-
*/
|
|
203
|
-
async getStatus() {
|
|
204
|
-
try {
|
|
205
|
-
const response = await api.get2FAStatus();
|
|
206
|
-
return response;
|
|
207
|
-
} catch (error) {
|
|
208
|
-
return {
|
|
209
|
-
enabled: false,
|
|
210
|
-
backupCodesRemaining: 0
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Disable 2FA
|
|
216
|
-
*/
|
|
217
|
-
async disable(code) {
|
|
218
|
-
try {
|
|
219
|
-
const response = await api.disable2FA(code);
|
|
220
|
-
return response.success;
|
|
221
|
-
} catch (error) {
|
|
222
|
-
throw error;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Generate new backup codes
|
|
227
|
-
*/
|
|
228
|
-
async regenerateBackupCodes() {
|
|
229
|
-
try {
|
|
230
|
-
const response = await api.regenerateBackupCodes();
|
|
231
|
-
return response.codes;
|
|
232
|
-
} catch (error) {
|
|
233
|
-
throw error;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Verify TOTP code locally (for testing)
|
|
238
|
-
*/
|
|
239
|
-
verifyLocal(secret, code) {
|
|
240
|
-
return verifyTOTP(secret, code);
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Generate TOTP code locally (for testing)
|
|
244
|
-
*/
|
|
245
|
-
generateLocal(secret) {
|
|
246
|
-
return generateTOTP(secret);
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
var DeviceTrustService = class {
|
|
250
|
-
/**
|
|
251
|
-
* Get current device fingerprint
|
|
252
|
-
*/
|
|
253
|
-
getFingerprint() {
|
|
254
|
-
return generateDeviceFingerprint();
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Get current device name
|
|
258
|
-
*/
|
|
259
|
-
getDeviceName() {
|
|
260
|
-
return getDeviceName();
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Register current device as trusted
|
|
264
|
-
*/
|
|
265
|
-
async trustDevice(name) {
|
|
266
|
-
const fingerprint = this.getFingerprint();
|
|
267
|
-
const deviceName = name || this.getDeviceName();
|
|
268
|
-
try {
|
|
269
|
-
const response = await api.trustDevice({
|
|
270
|
-
name: deviceName,
|
|
271
|
-
fingerprint
|
|
272
|
-
});
|
|
273
|
-
return response.device;
|
|
274
|
-
} catch (error) {
|
|
275
|
-
throw error;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Check if current device is trusted
|
|
280
|
-
*/
|
|
281
|
-
async isDeviceTrusted() {
|
|
282
|
-
const fingerprint = this.getFingerprint();
|
|
283
|
-
try {
|
|
284
|
-
const devices = await api.getDevices();
|
|
285
|
-
return devices.devices.some((d) => d.fingerprint === fingerprint);
|
|
286
|
-
} catch (error) {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* List all trusted devices
|
|
292
|
-
*/
|
|
293
|
-
async listDevices() {
|
|
294
|
-
try {
|
|
295
|
-
const response = await api.getDevices();
|
|
296
|
-
const currentFingerprint = this.getFingerprint();
|
|
297
|
-
return response.devices.map((d) => ({
|
|
298
|
-
...d,
|
|
299
|
-
isCurrent: d.fingerprint === currentFingerprint
|
|
300
|
-
}));
|
|
301
|
-
} catch (error) {
|
|
302
|
-
return [];
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Revoke a trusted device
|
|
307
|
-
*/
|
|
308
|
-
async revokeDevice(deviceId) {
|
|
309
|
-
try {
|
|
310
|
-
const response = await api.revokeDevice(deviceId);
|
|
311
|
-
return response.success;
|
|
312
|
-
} catch (error) {
|
|
313
|
-
throw error;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Revoke all devices except current
|
|
318
|
-
*/
|
|
319
|
-
async revokeAllDevices() {
|
|
320
|
-
try {
|
|
321
|
-
const response = await api.revokeAllDevices();
|
|
322
|
-
return response.revokedCount;
|
|
323
|
-
} catch (error) {
|
|
324
|
-
throw error;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
var twoFactorService = new TwoFactorService();
|
|
329
|
-
var deviceTrustService = new DeviceTrustService();
|
|
330
|
-
function displayQRCode(url) {
|
|
331
|
-
log.newline();
|
|
332
|
-
log.info("Scan diesen QR-Code mit deiner Authenticator App:");
|
|
333
|
-
log.newline();
|
|
334
|
-
log.dim("(QR-Code-Anzeige erfordert qrcode-terminal Paket)");
|
|
335
|
-
log.newline();
|
|
336
|
-
log.plain(`URL: ${url}`);
|
|
337
|
-
log.newline();
|
|
338
|
-
}
|
|
339
|
-
function displayBackupCodes(codes) {
|
|
340
|
-
log.newline();
|
|
341
|
-
console.log(colors.orange.bold("Backup-Codes (speichern!)"));
|
|
342
|
-
console.log(colors.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
343
|
-
log.newline();
|
|
344
|
-
for (let i = 0; i < codes.length; i++) {
|
|
345
|
-
console.log(` ${i + 1}. ${codes[i]}`);
|
|
346
|
-
}
|
|
347
|
-
log.newline();
|
|
348
|
-
log.warn("Diese Codes k\xF6nnen nur einmal verwendet werden!");
|
|
349
|
-
log.dim("Speichere sie an einem sicheren Ort.");
|
|
350
|
-
log.newline();
|
|
351
|
-
}
|
|
352
|
-
function displayDevices(devices) {
|
|
353
|
-
log.newline();
|
|
354
|
-
console.log(colors.orange.bold("Vertrauensw\xFCrdige Ger\xE4te"));
|
|
355
|
-
console.log(colors.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
356
|
-
log.newline();
|
|
357
|
-
if (devices.length === 0) {
|
|
358
|
-
log.dim("Keine Ger\xE4te registriert");
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
for (const device of devices) {
|
|
362
|
-
const current = device.isCurrent ? colors.green(" (aktuell)") : "";
|
|
363
|
-
console.log(` ${device.name}${current}`);
|
|
364
|
-
log.dim(` ID: ${device.id}`);
|
|
365
|
-
log.dim(` Zuletzt: ${formatRelativeTime(device.lastUsed)}`);
|
|
366
|
-
if (device.expiresAt) {
|
|
367
|
-
log.dim(` L\xE4uft ab: ${formatRelativeTime(device.expiresAt)}`);
|
|
368
|
-
} else {
|
|
369
|
-
log.dim(" L\xE4uft ab: nie");
|
|
370
|
-
}
|
|
371
|
-
log.newline();
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
function formatRelativeTime(dateStr) {
|
|
375
|
-
const date = new Date(dateStr);
|
|
376
|
-
const now = /* @__PURE__ */ new Date();
|
|
377
|
-
const diffMs = now.getTime() - date.getTime();
|
|
378
|
-
const diffSec = Math.floor(diffMs / 1e3);
|
|
379
|
-
const diffMin = Math.floor(diffSec / 60);
|
|
380
|
-
const diffHour = Math.floor(diffMin / 60);
|
|
381
|
-
const diffDay = Math.floor(diffHour / 24);
|
|
382
|
-
if (diffSec < 60) return "gerade eben";
|
|
383
|
-
if (diffMin < 60) return `vor ${diffMin} Minuten`;
|
|
384
|
-
if (diffHour < 24) return `vor ${diffHour} Stunden`;
|
|
385
|
-
if (diffDay < 30) return `vor ${diffDay} Tagen`;
|
|
386
|
-
return date.toLocaleDateString("de-DE");
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// src/services/auth/auth.ts
|
|
390
|
-
var PORT_RANGE_START = 9876;
|
|
391
|
-
var PORT_RANGE_END = 9886;
|
|
392
|
-
async function loginWithOtp(email) {
|
|
393
|
-
try {
|
|
394
|
-
log.info(`Sende Login-Code an ${email}...`);
|
|
395
|
-
const otpResponse = await api.requestOtp(email);
|
|
396
|
-
if (!otpResponse.success) {
|
|
397
|
-
log.error(otpResponse.message || "Fehler beim Senden des Codes");
|
|
398
|
-
return false;
|
|
399
|
-
}
|
|
400
|
-
log.success("Code wurde gesendet!");
|
|
401
|
-
log.dim(`Verbleibende Versuche: ${otpResponse.remainingAttempts}`);
|
|
402
|
-
log.newline();
|
|
403
|
-
const { otp } = await inquirer.prompt([
|
|
404
|
-
{
|
|
405
|
-
type: "input",
|
|
406
|
-
name: "otp",
|
|
407
|
-
message: "Code eingeben:",
|
|
408
|
-
validate: (input) => {
|
|
409
|
-
if (!/^\d{6}$/.test(input)) {
|
|
410
|
-
return "Bitte gib den 6-stelligen Code ein";
|
|
411
|
-
}
|
|
412
|
-
return true;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
]);
|
|
416
|
-
const authResponse = await api.verifyOtp(otpResponse.token, otp);
|
|
417
|
-
if (authResponse.requires2FA && authResponse.tempToken) {
|
|
418
|
-
log.newline();
|
|
419
|
-
log.info("2FA ist aktiviert. Bitte verifiziere dich.");
|
|
420
|
-
log.newline();
|
|
421
|
-
const result = await handle2FALoginFlow(authResponse.tempToken, authResponse.email || email);
|
|
422
|
-
return result;
|
|
423
|
-
}
|
|
424
|
-
if (!authResponse.success || !authResponse.token || !authResponse.user) {
|
|
425
|
-
log.error(authResponse.message || "Ung\xFCltiger Code");
|
|
426
|
-
return false;
|
|
427
|
-
}
|
|
428
|
-
setAuth(authResponse.token, authResponse.user);
|
|
429
|
-
log.newline();
|
|
430
|
-
log.success(`Angemeldet als ${authResponse.user.email} (${authResponse.user.tier.toUpperCase()})`);
|
|
431
|
-
return true;
|
|
432
|
-
} catch (error) {
|
|
433
|
-
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
|
434
|
-
log.error(`Login fehlgeschlagen: ${message}`);
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
async function handle2FALoginFlow(tempToken, email) {
|
|
439
|
-
const MAX_ATTEMPTS = 3;
|
|
440
|
-
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
441
|
-
const { code } = await inquirer.prompt([{
|
|
442
|
-
type: "input",
|
|
443
|
-
name: "code",
|
|
444
|
-
message: "2FA-Code (6 Ziffern oder Backup-Code):",
|
|
445
|
-
validate: (input) => {
|
|
446
|
-
if (/^\d{6}$/.test(input) || /^[A-Za-z0-9-]+$/.test(input)) {
|
|
447
|
-
return true;
|
|
448
|
-
}
|
|
449
|
-
return "Bitte gib einen g\xFCltigen Code ein";
|
|
450
|
-
}
|
|
451
|
-
}]);
|
|
452
|
-
const { rememberDevice } = await inquirer.prompt([{
|
|
453
|
-
type: "confirm",
|
|
454
|
-
name: "rememberDevice",
|
|
455
|
-
message: "Diesem Ger\xE4t vertrauen?",
|
|
456
|
-
default: true
|
|
457
|
-
}]);
|
|
458
|
-
try {
|
|
459
|
-
const result = await api.validate2FALogin({
|
|
460
|
-
tempToken,
|
|
461
|
-
code,
|
|
462
|
-
rememberDevice
|
|
463
|
-
});
|
|
464
|
-
if (result.success && result.token && result.user) {
|
|
465
|
-
setAuth(result.token, result.user);
|
|
466
|
-
if (result.method === "backup" && result.backupCodesRemaining !== void 0) {
|
|
467
|
-
log.warn(`Backup-Code verwendet. Noch ${result.backupCodesRemaining} Codes \xFCbrig.`);
|
|
468
|
-
}
|
|
469
|
-
log.newline();
|
|
470
|
-
log.success(`Angemeldet als ${result.user.email} (${result.user.tier.toUpperCase()})`);
|
|
471
|
-
return true;
|
|
472
|
-
}
|
|
473
|
-
if (attempt < MAX_ATTEMPTS) {
|
|
474
|
-
log.error(`Ung\xFCltiger Code. Noch ${MAX_ATTEMPTS - attempt} Versuche.`);
|
|
475
|
-
}
|
|
476
|
-
} catch (error) {
|
|
477
|
-
const message = error instanceof Error ? error.message : "Verifizierung fehlgeschlagen";
|
|
478
|
-
if (message.includes("expired") || message.includes("abgelaufen")) {
|
|
479
|
-
log.error("2FA-Token abgelaufen. Bitte erneut anmelden.");
|
|
480
|
-
return false;
|
|
481
|
-
}
|
|
482
|
-
if (attempt < MAX_ATTEMPTS) {
|
|
483
|
-
log.error(`${message}. Noch ${MAX_ATTEMPTS - attempt} Versuche.`);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
log.error("2FA-Verifizierung fehlgeschlagen");
|
|
488
|
-
return false;
|
|
489
|
-
}
|
|
490
|
-
async function findAvailablePort() {
|
|
491
|
-
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
|
|
492
|
-
const isAvailable = await checkPort(port);
|
|
493
|
-
if (isAvailable) {
|
|
494
|
-
return port;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
throw new Error("Kein freier Port f\xFCr Callback-Server gefunden");
|
|
498
|
-
}
|
|
499
|
-
function checkPort(port) {
|
|
500
|
-
return new Promise((resolve) => {
|
|
501
|
-
const server = http.createServer();
|
|
502
|
-
server.once("error", () => resolve(false));
|
|
503
|
-
server.once("listening", () => {
|
|
504
|
-
server.close();
|
|
505
|
-
resolve(true);
|
|
506
|
-
});
|
|
507
|
-
server.listen(port, "127.0.0.1");
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
function getCallbackHtml(type, data) {
|
|
511
|
-
const baseStyles = `
|
|
512
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
513
|
-
body {
|
|
514
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
515
|
-
min-height: 100vh;
|
|
516
|
-
display: flex;
|
|
517
|
-
align-items: center;
|
|
518
|
-
justify-content: center;
|
|
519
|
-
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%);
|
|
520
|
-
color: #fff;
|
|
521
|
-
}
|
|
522
|
-
.container {
|
|
523
|
-
text-align: center;
|
|
524
|
-
padding: 3rem;
|
|
525
|
-
max-width: 420px;
|
|
526
|
-
}
|
|
527
|
-
.logo {
|
|
528
|
-
width: 80px;
|
|
529
|
-
height: 80px;
|
|
530
|
-
margin: 0 auto 1.5rem;
|
|
531
|
-
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
|
532
|
-
border-radius: 20px;
|
|
533
|
-
display: flex;
|
|
534
|
-
align-items: center;
|
|
535
|
-
justify-content: center;
|
|
536
|
-
box-shadow: 0 10px 40px rgba(249, 115, 22, 0.3);
|
|
537
|
-
}
|
|
538
|
-
.logo svg {
|
|
539
|
-
width: 48px;
|
|
540
|
-
height: 48px;
|
|
541
|
-
}
|
|
542
|
-
h1 {
|
|
543
|
-
font-size: 1.75rem;
|
|
544
|
-
font-weight: 700;
|
|
545
|
-
margin-bottom: 0.75rem;
|
|
546
|
-
}
|
|
547
|
-
.subtitle {
|
|
548
|
-
color: #9ca3af;
|
|
549
|
-
font-size: 1rem;
|
|
550
|
-
line-height: 1.5;
|
|
551
|
-
}
|
|
552
|
-
.email {
|
|
553
|
-
color: #f97316;
|
|
554
|
-
font-weight: 600;
|
|
555
|
-
}
|
|
556
|
-
.status-icon {
|
|
557
|
-
width: 64px;
|
|
558
|
-
height: 64px;
|
|
559
|
-
margin: 0 auto 1.5rem;
|
|
560
|
-
border-radius: 50%;
|
|
561
|
-
display: flex;
|
|
562
|
-
align-items: center;
|
|
563
|
-
justify-content: center;
|
|
564
|
-
}
|
|
565
|
-
.status-icon.success {
|
|
566
|
-
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
|
567
|
-
box-shadow: 0 10px 40px rgba(34, 197, 94, 0.3);
|
|
568
|
-
}
|
|
569
|
-
.status-icon.error {
|
|
570
|
-
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
571
|
-
box-shadow: 0 10px 40px rgba(239, 68, 68, 0.3);
|
|
572
|
-
}
|
|
573
|
-
.status-icon svg {
|
|
574
|
-
width: 32px;
|
|
575
|
-
height: 32px;
|
|
576
|
-
}
|
|
577
|
-
.spinner {
|
|
578
|
-
width: 48px;
|
|
579
|
-
height: 48px;
|
|
580
|
-
margin: 0 auto 1.5rem;
|
|
581
|
-
border: 3px solid rgba(249, 115, 22, 0.2);
|
|
582
|
-
border-top-color: #f97316;
|
|
583
|
-
border-radius: 50%;
|
|
584
|
-
animation: spin 1s linear infinite;
|
|
585
|
-
}
|
|
586
|
-
@keyframes spin {
|
|
587
|
-
to { transform: rotate(360deg); }
|
|
588
|
-
}
|
|
589
|
-
.hint {
|
|
590
|
-
margin-top: 1.5rem;
|
|
591
|
-
padding-top: 1.5rem;
|
|
592
|
-
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
593
|
-
color: #6b7280;
|
|
594
|
-
font-size: 0.875rem;
|
|
595
|
-
}
|
|
596
|
-
`;
|
|
597
|
-
const shivaLogo = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`;
|
|
598
|
-
const checkIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
|
|
599
|
-
const xIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
|
600
|
-
if (type === "success") {
|
|
601
|
-
return `<!DOCTYPE html>
|
|
602
|
-
<html lang="de">
|
|
603
|
-
<head>
|
|
604
|
-
<meta charset="UTF-8">
|
|
605
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
606
|
-
<title>SHIVA CLI - Angemeldet</title>
|
|
607
|
-
<style>${baseStyles}</style>
|
|
608
|
-
</head>
|
|
609
|
-
<body>
|
|
610
|
-
<div class="container">
|
|
611
|
-
<div class="status-icon success">${checkIcon}</div>
|
|
612
|
-
<h1>Erfolgreich angemeldet!</h1>
|
|
613
|
-
<p class="subtitle">Willkommen zur\xFCck, <span class="email">${data.email}</span></p>
|
|
614
|
-
<p class="hint">Du kannst dieses Fenster jetzt schlie\xDFen und zum Terminal zur\xFCckkehren.</p>
|
|
615
|
-
</div>
|
|
616
|
-
<script>setTimeout(() => window.close(), 3000);</script>
|
|
617
|
-
</body>
|
|
618
|
-
</html>`;
|
|
619
|
-
}
|
|
620
|
-
if (type === "error") {
|
|
621
|
-
return `<!DOCTYPE html>
|
|
622
|
-
<html lang="de">
|
|
623
|
-
<head>
|
|
624
|
-
<meta charset="UTF-8">
|
|
625
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
626
|
-
<title>SHIVA CLI - Fehler</title>
|
|
627
|
-
<style>${baseStyles}</style>
|
|
628
|
-
</head>
|
|
629
|
-
<body>
|
|
630
|
-
<div class="container">
|
|
631
|
-
<div class="status-icon error">${xIcon}</div>
|
|
632
|
-
<h1>Anmeldung fehlgeschlagen</h1>
|
|
633
|
-
<p class="subtitle">${data.message || "Ein unbekannter Fehler ist aufgetreten."}</p>
|
|
634
|
-
<p class="hint">Du kannst dieses Fenster schlie\xDFen und es erneut versuchen.</p>
|
|
635
|
-
</div>
|
|
636
|
-
</body>
|
|
637
|
-
</html>`;
|
|
638
|
-
}
|
|
639
|
-
return `<!DOCTYPE html>
|
|
640
|
-
<html lang="de">
|
|
641
|
-
<head>
|
|
642
|
-
<meta charset="UTF-8">
|
|
643
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
644
|
-
<title>SHIVA CLI</title>
|
|
645
|
-
<style>${baseStyles}</style>
|
|
646
|
-
</head>
|
|
647
|
-
<body>
|
|
648
|
-
<div class="container">
|
|
649
|
-
<div class="logo">${shivaLogo}</div>
|
|
650
|
-
<div class="spinner"></div>
|
|
651
|
-
<h1>SHIVA CLI</h1>
|
|
652
|
-
<p class="subtitle">Warte auf Login-Callback...</p>
|
|
653
|
-
</div>
|
|
654
|
-
</body>
|
|
655
|
-
</html>`;
|
|
656
|
-
}
|
|
657
|
-
function startCallbackServer(port) {
|
|
658
|
-
return new Promise((resolve, reject) => {
|
|
659
|
-
const server = http.createServer((req, res) => {
|
|
660
|
-
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
|
|
661
|
-
if (url.pathname === "/callback") {
|
|
662
|
-
const token = url.searchParams.get("token");
|
|
663
|
-
const userId = url.searchParams.get("userId");
|
|
664
|
-
const email = url.searchParams.get("email");
|
|
665
|
-
const tier = url.searchParams.get("tier");
|
|
666
|
-
const error = url.searchParams.get("error");
|
|
667
|
-
if (error) {
|
|
668
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
669
|
-
res.end(getCallbackHtml("error", { message: error }));
|
|
670
|
-
server.close();
|
|
671
|
-
reject(new Error(error));
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
if (!token || !userId || !email) {
|
|
675
|
-
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
676
|
-
res.end(getCallbackHtml("error", { message: "Token oder Benutzer-Daten fehlen." }));
|
|
677
|
-
server.close();
|
|
678
|
-
reject(new Error("Ung\xFCltige Callback-Parameter"));
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
682
|
-
res.end(getCallbackHtml("success", { email }));
|
|
683
|
-
setTimeout(() => {
|
|
684
|
-
server.close();
|
|
685
|
-
resolve({
|
|
686
|
-
token,
|
|
687
|
-
user: {
|
|
688
|
-
id: parseInt(userId, 10),
|
|
689
|
-
email,
|
|
690
|
-
tier: tier || "free"
|
|
691
|
-
}
|
|
692
|
-
});
|
|
693
|
-
}, 100);
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
697
|
-
res.end(getCallbackHtml("waiting", {}));
|
|
698
|
-
});
|
|
699
|
-
const timeout = setTimeout(() => {
|
|
700
|
-
server.close();
|
|
701
|
-
reject(new Error("Login-Timeout - keine Antwort erhalten"));
|
|
702
|
-
}, 3e5);
|
|
703
|
-
server.once("close", () => {
|
|
704
|
-
clearTimeout(timeout);
|
|
705
|
-
});
|
|
706
|
-
server.once("error", (err) => {
|
|
707
|
-
clearTimeout(timeout);
|
|
708
|
-
reject(err);
|
|
709
|
-
});
|
|
710
|
-
server.listen(port, "127.0.0.1");
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
async function loginWithBrowser() {
|
|
714
|
-
const config = getConfig();
|
|
715
|
-
try {
|
|
716
|
-
const port = await findAvailablePort();
|
|
717
|
-
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
718
|
-
const baseUrl = config.apiEndpoint.replace("/api", "");
|
|
719
|
-
const loginUrl = `${baseUrl}/auth/cli-login?callback=${encodeURIComponent(callbackUrl)}`;
|
|
720
|
-
const serverPromise = startCallbackServer(port);
|
|
721
|
-
log.newline();
|
|
722
|
-
log.info("\xD6ffne Browser zur Anmeldung...");
|
|
723
|
-
try {
|
|
724
|
-
await open(loginUrl);
|
|
725
|
-
} catch {
|
|
726
|
-
}
|
|
727
|
-
log.newline();
|
|
728
|
-
log.plain("Bitte \xF6ffne diese URL in deinem Browser um die Anmeldung abzuschlie\xDFen:");
|
|
729
|
-
log.newline();
|
|
730
|
-
console.log(` ${colors.cyan(loginUrl)}`);
|
|
731
|
-
log.newline();
|
|
732
|
-
log.dim("Warte auf Anmeldung... (Timeout: 5 Minuten)");
|
|
733
|
-
log.newline();
|
|
734
|
-
const result = await serverPromise;
|
|
735
|
-
setAuth(result.token, result.user);
|
|
736
|
-
try {
|
|
737
|
-
const tfaStatus = await twoFactorService.getStatus();
|
|
738
|
-
if (tfaStatus.enabled) {
|
|
739
|
-
log.newline();
|
|
740
|
-
log.info("2FA ist aktiviert. Bitte verifiziere dich.");
|
|
741
|
-
log.newline();
|
|
742
|
-
const isDeviceTrusted = await deviceTrustService.isDeviceTrusted();
|
|
743
|
-
if (!isDeviceTrusted) {
|
|
744
|
-
const verified = await prompt2FAVerification();
|
|
745
|
-
if (!verified) {
|
|
746
|
-
clearAuth();
|
|
747
|
-
log.error("2FA-Verifizierung fehlgeschlagen");
|
|
748
|
-
return false;
|
|
749
|
-
}
|
|
750
|
-
const { trustDevice } = await inquirer.prompt([{
|
|
751
|
-
type: "confirm",
|
|
752
|
-
name: "trustDevice",
|
|
753
|
-
message: "Diesem Ger\xE4t vertrauen?",
|
|
754
|
-
default: true
|
|
755
|
-
}]);
|
|
756
|
-
if (trustDevice) {
|
|
757
|
-
try {
|
|
758
|
-
await deviceTrustService.trustDevice();
|
|
759
|
-
log.success("Ger\xE4t als vertrauensw\xFCrdig markiert");
|
|
760
|
-
} catch {
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
} else {
|
|
764
|
-
log.dim("Ger\xE4t ist vertrauensw\xFCrdig - 2FA \xFCbersprungen");
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
} catch {
|
|
768
|
-
}
|
|
769
|
-
log.success(`Angemeldet als ${result.user.email} (${result.user.tier.toUpperCase()})`);
|
|
770
|
-
return true;
|
|
771
|
-
} catch (error) {
|
|
772
|
-
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
|
773
|
-
log.error(`Login fehlgeschlagen: ${message}`);
|
|
774
|
-
log.newline();
|
|
775
|
-
log.info("Fallback: Manueller Token-Eintrag");
|
|
776
|
-
const loginUrl = `${config.apiEndpoint.replace("/api", "")}/auth/cli-login`;
|
|
777
|
-
log.dim(`URL: ${loginUrl}`);
|
|
778
|
-
log.newline();
|
|
779
|
-
const { useManual } = await inquirer.prompt([{
|
|
780
|
-
type: "confirm",
|
|
781
|
-
name: "useManual",
|
|
782
|
-
message: "Token manuell eingeben?",
|
|
783
|
-
default: true
|
|
784
|
-
}]);
|
|
785
|
-
if (!useManual) {
|
|
786
|
-
return false;
|
|
787
|
-
}
|
|
788
|
-
try {
|
|
789
|
-
await open(loginUrl);
|
|
790
|
-
} catch {
|
|
791
|
-
log.dim("Browser konnte nicht ge\xF6ffnet werden");
|
|
792
|
-
log.dim(`\xD6ffne manuell: ${loginUrl}`);
|
|
793
|
-
}
|
|
794
|
-
const { token } = await inquirer.prompt([{
|
|
795
|
-
type: "password",
|
|
796
|
-
name: "token",
|
|
797
|
-
message: "Token:",
|
|
798
|
-
mask: "*"
|
|
799
|
-
}]);
|
|
800
|
-
if (!token) {
|
|
801
|
-
log.error("Kein Token eingegeben");
|
|
802
|
-
return false;
|
|
803
|
-
}
|
|
804
|
-
setAuth(token, { id: 0, email: "", tier: "free" });
|
|
805
|
-
try {
|
|
806
|
-
const response = await api.getCurrentUser();
|
|
807
|
-
setAuth(token, response.user);
|
|
808
|
-
log.success(`Angemeldet als ${response.user.email}`);
|
|
809
|
-
return true;
|
|
810
|
-
} catch {
|
|
811
|
-
clearAuth();
|
|
812
|
-
log.error("Ung\xFCltiger Token");
|
|
813
|
-
return false;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
function logout() {
|
|
818
|
-
clearAuth();
|
|
819
|
-
log.success("Abgemeldet");
|
|
820
|
-
}
|
|
821
|
-
async function prompt2FAVerification() {
|
|
822
|
-
const MAX_ATTEMPTS = 3;
|
|
823
|
-
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
824
|
-
const { method } = await inquirer.prompt([{
|
|
825
|
-
type: "list",
|
|
826
|
-
name: "method",
|
|
827
|
-
message: "Verifizierungsmethode:",
|
|
828
|
-
choices: [
|
|
829
|
-
{ name: "Authenticator App Code", value: "totp" },
|
|
830
|
-
{ name: "Backup Code", value: "backup" },
|
|
831
|
-
{ name: "Abbrechen", value: "cancel" }
|
|
832
|
-
]
|
|
833
|
-
}]);
|
|
834
|
-
if (method === "cancel") {
|
|
835
|
-
return false;
|
|
836
|
-
}
|
|
837
|
-
const promptMessage = method === "totp" ? "6-stelliger Code aus deiner Authenticator App:" : "Backup-Code (Format: XXX-XXX-XXX-XXX):";
|
|
838
|
-
const { code } = await inquirer.prompt([{
|
|
839
|
-
type: "input",
|
|
840
|
-
name: "code",
|
|
841
|
-
message: promptMessage,
|
|
842
|
-
validate: (input) => {
|
|
843
|
-
if (method === "totp") {
|
|
844
|
-
return /^\d{6}$/.test(input) || "Bitte gib einen 6-stelligen Code ein";
|
|
845
|
-
}
|
|
846
|
-
return input.length > 0 || "Bitte gib einen Backup-Code ein";
|
|
847
|
-
}
|
|
848
|
-
}]);
|
|
849
|
-
try {
|
|
850
|
-
let verified = false;
|
|
851
|
-
if (method === "totp") {
|
|
852
|
-
verified = await twoFactorService.verifyCode(code);
|
|
853
|
-
} else {
|
|
854
|
-
verified = await twoFactorService.verifyBackupCode(code);
|
|
855
|
-
}
|
|
856
|
-
if (verified) {
|
|
857
|
-
log.success("2FA-Verifizierung erfolgreich");
|
|
858
|
-
return true;
|
|
859
|
-
}
|
|
860
|
-
} catch (error) {
|
|
861
|
-
}
|
|
862
|
-
if (attempt < MAX_ATTEMPTS) {
|
|
863
|
-
log.error(`Ung\xFCltiger Code. Noch ${MAX_ATTEMPTS - attempt} Versuche.`);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
return false;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
22
|
// src/i18n/locales/de.json
|
|
870
23
|
var de_default = {
|
|
871
24
|
common: {
|
|
@@ -928,7 +81,20 @@ var de_default = {
|
|
|
928
81
|
invalidEmail: "Bitte gib eine gultige Email-Adresse ein",
|
|
929
82
|
twoFactorEnabled: "2FA aktiviert.",
|
|
930
83
|
twoFactorDisabled: "2FA deaktiviert.",
|
|
931
|
-
invalidToken: "Ungultiger Token."
|
|
84
|
+
invalidToken: "Ungultiger Token.",
|
|
85
|
+
callback: {
|
|
86
|
+
successTitle: "Erfolgreich angemeldet!",
|
|
87
|
+
successSubtitle: "Willkommen zur\xFCck,",
|
|
88
|
+
successHint: "Du kannst dieses Fenster jetzt schlie\xDFen und zum Terminal zur\xFCckkehren.",
|
|
89
|
+
errorTitle: "Anmeldung fehlgeschlagen",
|
|
90
|
+
errorDefault: "Ein unbekannter Fehler ist aufgetreten.",
|
|
91
|
+
errorHint: "Du kannst dieses Fenster schlie\xDFen und es erneut versuchen.",
|
|
92
|
+
waitingTitle: "Authentifizierung",
|
|
93
|
+
waitingSubtitle: "Warte auf Login-Best\xE4tigung...",
|
|
94
|
+
pageTitle: "SHIVA Code",
|
|
95
|
+
pageTitleSuccess: "SHIVA Code - Angemeldet",
|
|
96
|
+
pageTitleError: "SHIVA Code - Fehler"
|
|
97
|
+
}
|
|
932
98
|
},
|
|
933
99
|
errors: {
|
|
934
100
|
unknown: "Ein unbekannter Fehler ist aufgetreten",
|
|
@@ -2977,7 +2143,20 @@ var en_default = {
|
|
|
2977
2143
|
invalidEmail: "Please enter a valid email address",
|
|
2978
2144
|
twoFactorEnabled: "2FA enabled.",
|
|
2979
2145
|
twoFactorDisabled: "2FA disabled.",
|
|
2980
|
-
invalidToken: "Invalid token."
|
|
2146
|
+
invalidToken: "Invalid token.",
|
|
2147
|
+
callback: {
|
|
2148
|
+
successTitle: "Successfully logged in!",
|
|
2149
|
+
successSubtitle: "Welcome back,",
|
|
2150
|
+
successHint: "You can now close this window and return to the terminal.",
|
|
2151
|
+
errorTitle: "Login failed",
|
|
2152
|
+
errorDefault: "An unknown error occurred.",
|
|
2153
|
+
errorHint: "You can close this window and try again.",
|
|
2154
|
+
waitingTitle: "Authentication",
|
|
2155
|
+
waitingSubtitle: "Waiting for login confirmation...",
|
|
2156
|
+
pageTitle: "SHIVA Code",
|
|
2157
|
+
pageTitleSuccess: "SHIVA Code - Logged in",
|
|
2158
|
+
pageTitleError: "SHIVA Code - Error"
|
|
2159
|
+
}
|
|
2981
2160
|
},
|
|
2982
2161
|
errors: {
|
|
2983
2162
|
unknown: "An unknown error occurred",
|
|
@@ -4656,7 +3835,20 @@ var fr_default = {
|
|
|
4656
3835
|
invalidEmail: "Veuillez entrer une adresse email valide",
|
|
4657
3836
|
twoFactorEnabled: "2FA active.",
|
|
4658
3837
|
twoFactorDisabled: "2FA desactive.",
|
|
4659
|
-
invalidToken: "Token invalide."
|
|
3838
|
+
invalidToken: "Token invalide.",
|
|
3839
|
+
callback: {
|
|
3840
|
+
successTitle: "Connexion r\xE9ussie!",
|
|
3841
|
+
successSubtitle: "Bienvenue,",
|
|
3842
|
+
successHint: "Vous pouvez maintenant fermer cette fen\xEAtre et retourner au terminal.",
|
|
3843
|
+
errorTitle: "\xC9chec de la connexion",
|
|
3844
|
+
errorDefault: "Une erreur inconnue s'est produite.",
|
|
3845
|
+
errorHint: "Vous pouvez fermer cette fen\xEAtre et r\xE9essayer.",
|
|
3846
|
+
waitingTitle: "Authentification",
|
|
3847
|
+
waitingSubtitle: "En attente de la confirmation...",
|
|
3848
|
+
pageTitle: "SHIVA Code",
|
|
3849
|
+
pageTitleSuccess: "SHIVA Code - Connect\xE9",
|
|
3850
|
+
pageTitleError: "SHIVA Code - Erreur"
|
|
3851
|
+
}
|
|
4660
3852
|
},
|
|
4661
3853
|
errors: {
|
|
4662
3854
|
unknown: "Une erreur inconnue s'est produite",
|
|
@@ -6284,7 +5476,20 @@ var es_default = {
|
|
|
6284
5476
|
invalidEmail: "Por favor introduce una direccion de email valida",
|
|
6285
5477
|
twoFactorEnabled: "2FA habilitado.",
|
|
6286
5478
|
twoFactorDisabled: "2FA deshabilitado.",
|
|
6287
|
-
invalidToken: "Token invalido."
|
|
5479
|
+
invalidToken: "Token invalido.",
|
|
5480
|
+
callback: {
|
|
5481
|
+
successTitle: "\xA1Sesi\xF3n iniciada con \xE9xito!",
|
|
5482
|
+
successSubtitle: "Bienvenido de nuevo,",
|
|
5483
|
+
successHint: "Ahora puedes cerrar esta ventana y volver al terminal.",
|
|
5484
|
+
errorTitle: "Error de inicio de sesi\xF3n",
|
|
5485
|
+
errorDefault: "Ha ocurrido un error desconocido.",
|
|
5486
|
+
errorHint: "Puedes cerrar esta ventana e intentarlo de nuevo.",
|
|
5487
|
+
waitingTitle: "Autenticaci\xF3n",
|
|
5488
|
+
waitingSubtitle: "Esperando confirmaci\xF3n de inicio de sesi\xF3n...",
|
|
5489
|
+
pageTitle: "SHIVA Code",
|
|
5490
|
+
pageTitleSuccess: "SHIVA Code - Conectado",
|
|
5491
|
+
pageTitleError: "SHIVA Code - Error"
|
|
5492
|
+
}
|
|
6288
5493
|
},
|
|
6289
5494
|
errors: {
|
|
6290
5495
|
unknown: "Ha ocurrido un error desconocido",
|
|
@@ -8227,94 +7432,1004 @@ var es_default = {
|
|
|
8227
7432
|
deletedInfo: "Todos los datos del proyecto han sido eliminados de la nube."
|
|
8228
7433
|
}
|
|
8229
7434
|
};
|
|
8230
|
-
|
|
8231
|
-
// src/i18n/types.ts
|
|
8232
|
-
var SUPPORTED_LANGUAGES = ["de", "en", "fr", "es"];
|
|
8233
|
-
var LANGUAGE_NAMES = {
|
|
8234
|
-
de: "Deutsch",
|
|
8235
|
-
en: "English",
|
|
8236
|
-
fr: "Francais",
|
|
8237
|
-
es: "Espanol"
|
|
7435
|
+
|
|
7436
|
+
// src/i18n/types.ts
|
|
7437
|
+
var SUPPORTED_LANGUAGES = ["de", "en", "fr", "es"];
|
|
7438
|
+
var LANGUAGE_NAMES = {
|
|
7439
|
+
de: "Deutsch",
|
|
7440
|
+
en: "English",
|
|
7441
|
+
fr: "Francais",
|
|
7442
|
+
es: "Espanol"
|
|
7443
|
+
};
|
|
7444
|
+
|
|
7445
|
+
// src/i18n/index.ts
|
|
7446
|
+
var translations = {
|
|
7447
|
+
de: de_default,
|
|
7448
|
+
en: en_default,
|
|
7449
|
+
fr: fr_default,
|
|
7450
|
+
es: es_default
|
|
7451
|
+
};
|
|
7452
|
+
var currentLanguage = "de";
|
|
7453
|
+
function setLanguage(lang) {
|
|
7454
|
+
currentLanguage = lang;
|
|
7455
|
+
}
|
|
7456
|
+
function getLanguage() {
|
|
7457
|
+
return currentLanguage;
|
|
7458
|
+
}
|
|
7459
|
+
function isValidLanguage(lang) {
|
|
7460
|
+
return ["de", "en", "fr", "es"].includes(lang);
|
|
7461
|
+
}
|
|
7462
|
+
function t(key, params) {
|
|
7463
|
+
const keys = key.split(".");
|
|
7464
|
+
let value = translations[currentLanguage];
|
|
7465
|
+
for (const k of keys) {
|
|
7466
|
+
if (value && typeof value === "object" && k in value) {
|
|
7467
|
+
value = value[k];
|
|
7468
|
+
} else {
|
|
7469
|
+
value = void 0;
|
|
7470
|
+
break;
|
|
7471
|
+
}
|
|
7472
|
+
}
|
|
7473
|
+
if (typeof value !== "string") {
|
|
7474
|
+
value = keys.reduce(
|
|
7475
|
+
(obj, k) => obj && typeof obj === "object" && k in obj ? obj[k] : void 0,
|
|
7476
|
+
translations.de
|
|
7477
|
+
);
|
|
7478
|
+
}
|
|
7479
|
+
if (typeof value !== "string") {
|
|
7480
|
+
return key;
|
|
7481
|
+
}
|
|
7482
|
+
if (params) {
|
|
7483
|
+
for (const [param, val] of Object.entries(params)) {
|
|
7484
|
+
value = value.replace(new RegExp(`\\{${param}\\}`, "g"), String(val));
|
|
7485
|
+
}
|
|
7486
|
+
}
|
|
7487
|
+
return value;
|
|
7488
|
+
}
|
|
7489
|
+
function tArray(key) {
|
|
7490
|
+
const keys = key.split(".");
|
|
7491
|
+
let value = translations[currentLanguage];
|
|
7492
|
+
for (const k of keys) {
|
|
7493
|
+
if (value && typeof value === "object" && k in value) {
|
|
7494
|
+
value = value[k];
|
|
7495
|
+
} else {
|
|
7496
|
+
value = void 0;
|
|
7497
|
+
break;
|
|
7498
|
+
}
|
|
7499
|
+
}
|
|
7500
|
+
if (!Array.isArray(value)) {
|
|
7501
|
+
value = keys.reduce(
|
|
7502
|
+
(obj, k) => obj && typeof obj === "object" && k in obj ? obj[k] : void 0,
|
|
7503
|
+
translations.de
|
|
7504
|
+
);
|
|
7505
|
+
}
|
|
7506
|
+
return Array.isArray(value) ? value : [];
|
|
7507
|
+
}
|
|
7508
|
+
function initializeLanguage(storedLang) {
|
|
7509
|
+
if (storedLang && isValidLanguage(storedLang)) {
|
|
7510
|
+
setLanguage(storedLang);
|
|
7511
|
+
return;
|
|
7512
|
+
}
|
|
7513
|
+
try {
|
|
7514
|
+
const systemLocale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
7515
|
+
const langCode = systemLocale.split("-")[0].toLowerCase();
|
|
7516
|
+
if (isValidLanguage(langCode)) {
|
|
7517
|
+
setLanguage(langCode);
|
|
7518
|
+
return;
|
|
7519
|
+
}
|
|
7520
|
+
} catch {
|
|
7521
|
+
}
|
|
7522
|
+
setLanguage("de");
|
|
7523
|
+
}
|
|
7524
|
+
|
|
7525
|
+
// src/services/auth/auth.ts
|
|
7526
|
+
import inquirer from "inquirer";
|
|
7527
|
+
import open from "open";
|
|
7528
|
+
import http from "http";
|
|
7529
|
+
import { URL } from "url";
|
|
7530
|
+
|
|
7531
|
+
// src/services/auth/two-factor.ts
|
|
7532
|
+
import * as crypto from "crypto";
|
|
7533
|
+
function generateSecret(length = 20) {
|
|
7534
|
+
const buffer = crypto.randomBytes(length);
|
|
7535
|
+
return base32Encode(buffer);
|
|
7536
|
+
}
|
|
7537
|
+
function base32Encode(buffer) {
|
|
7538
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
7539
|
+
let result = "";
|
|
7540
|
+
let bits = 0;
|
|
7541
|
+
let value = 0;
|
|
7542
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
7543
|
+
value = value << 8 | buffer[i];
|
|
7544
|
+
bits += 8;
|
|
7545
|
+
while (bits >= 5) {
|
|
7546
|
+
result += alphabet[value >>> bits - 5 & 31];
|
|
7547
|
+
bits -= 5;
|
|
7548
|
+
}
|
|
7549
|
+
}
|
|
7550
|
+
if (bits > 0) {
|
|
7551
|
+
result += alphabet[value << 5 - bits & 31];
|
|
7552
|
+
}
|
|
7553
|
+
return result;
|
|
7554
|
+
}
|
|
7555
|
+
function base32Decode(encoded) {
|
|
7556
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
7557
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
7558
|
+
for (let i = 0; i < alphabet.length; i++) {
|
|
7559
|
+
lookup.set(alphabet[i], i);
|
|
7560
|
+
}
|
|
7561
|
+
const bytes = [];
|
|
7562
|
+
let bits = 0;
|
|
7563
|
+
let value = 0;
|
|
7564
|
+
for (const char of encoded.toUpperCase()) {
|
|
7565
|
+
if (char === "=") continue;
|
|
7566
|
+
const index = lookup.get(char);
|
|
7567
|
+
if (index === void 0) continue;
|
|
7568
|
+
value = value << 5 | index;
|
|
7569
|
+
bits += 5;
|
|
7570
|
+
if (bits >= 8) {
|
|
7571
|
+
bytes.push(value >>> bits - 8 & 255);
|
|
7572
|
+
bits -= 8;
|
|
7573
|
+
}
|
|
7574
|
+
}
|
|
7575
|
+
return Buffer.from(bytes);
|
|
7576
|
+
}
|
|
7577
|
+
function generateTOTP(secret, timeStep = 30) {
|
|
7578
|
+
const time = Math.floor(Date.now() / 1e3 / timeStep);
|
|
7579
|
+
const key = base32Decode(secret);
|
|
7580
|
+
const counter = Buffer.alloc(8);
|
|
7581
|
+
counter.writeBigUInt64BE(BigInt(time));
|
|
7582
|
+
const hmac = crypto.createHmac("sha1", key);
|
|
7583
|
+
hmac.update(counter);
|
|
7584
|
+
const hash = hmac.digest();
|
|
7585
|
+
const offset = hash[hash.length - 1] & 15;
|
|
7586
|
+
const code = ((hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255) % 1e6;
|
|
7587
|
+
return code.toString().padStart(6, "0");
|
|
7588
|
+
}
|
|
7589
|
+
function verifyTOTP(secret, code, timeStep = 30) {
|
|
7590
|
+
for (let i = -1; i <= 1; i++) {
|
|
7591
|
+
const time = Math.floor(Date.now() / 1e3 / timeStep) + i;
|
|
7592
|
+
const expectedCode = generateTOTPForTime(secret, time, timeStep);
|
|
7593
|
+
if (expectedCode === code) {
|
|
7594
|
+
return true;
|
|
7595
|
+
}
|
|
7596
|
+
}
|
|
7597
|
+
return false;
|
|
7598
|
+
}
|
|
7599
|
+
function generateTOTPForTime(secret, time, timeStep) {
|
|
7600
|
+
const key = base32Decode(secret);
|
|
7601
|
+
const counter = Buffer.alloc(8);
|
|
7602
|
+
counter.writeBigUInt64BE(BigInt(time));
|
|
7603
|
+
const hmac = crypto.createHmac("sha1", key);
|
|
7604
|
+
hmac.update(counter);
|
|
7605
|
+
const hash = hmac.digest();
|
|
7606
|
+
const offset = hash[hash.length - 1] & 15;
|
|
7607
|
+
const code = ((hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255) % 1e6;
|
|
7608
|
+
return code.toString().padStart(6, "0");
|
|
7609
|
+
}
|
|
7610
|
+
function generateOtpAuthUrl(secret, email, issuer = "SHIVA Code") {
|
|
7611
|
+
const encodedIssuer = encodeURIComponent(issuer);
|
|
7612
|
+
const encodedEmail = encodeURIComponent(email);
|
|
7613
|
+
return `otpauth://totp/${encodedIssuer}:${encodedEmail}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=30`;
|
|
7614
|
+
}
|
|
7615
|
+
function generateBackupCodes(count = 10) {
|
|
7616
|
+
const codes = [];
|
|
7617
|
+
for (let i = 0; i < count; i++) {
|
|
7618
|
+
const buffer = crypto.randomBytes(6);
|
|
7619
|
+
const code = buffer.toString("hex").toUpperCase();
|
|
7620
|
+
codes.push(`${code.slice(0, 3)}-${code.slice(3, 6)}-${code.slice(6, 9)}-${code.slice(9, 12)}`);
|
|
7621
|
+
}
|
|
7622
|
+
return codes;
|
|
7623
|
+
}
|
|
7624
|
+
function generateDeviceFingerprint() {
|
|
7625
|
+
const os = __require("os");
|
|
7626
|
+
const components = [
|
|
7627
|
+
os.hostname(),
|
|
7628
|
+
os.platform(),
|
|
7629
|
+
os.arch(),
|
|
7630
|
+
os.cpus()[0]?.model || "unknown",
|
|
7631
|
+
os.userInfo().username
|
|
7632
|
+
];
|
|
7633
|
+
const fingerprint = crypto.createHash("sha256").update(components.join("|")).digest("hex").slice(0, 32);
|
|
7634
|
+
return fingerprint;
|
|
7635
|
+
}
|
|
7636
|
+
function getDeviceName() {
|
|
7637
|
+
const os = __require("os");
|
|
7638
|
+
const hostname = os.hostname();
|
|
7639
|
+
const platform = os.platform();
|
|
7640
|
+
const platformNames = {
|
|
7641
|
+
darwin: "macOS",
|
|
7642
|
+
linux: "Linux",
|
|
7643
|
+
win32: "Windows"
|
|
7644
|
+
};
|
|
7645
|
+
return `${hostname} (${platformNames[platform] || platform})`;
|
|
7646
|
+
}
|
|
7647
|
+
var TwoFactorService = class {
|
|
7648
|
+
/**
|
|
7649
|
+
* Start 2FA setup - generate secret and QR code
|
|
7650
|
+
*/
|
|
7651
|
+
async startSetup(email) {
|
|
7652
|
+
try {
|
|
7653
|
+
const response = await api.setup2FA();
|
|
7654
|
+
if (!response.setup) {
|
|
7655
|
+
throw new Error(response.message || "2FA-Setup fehlgeschlagen");
|
|
7656
|
+
}
|
|
7657
|
+
return response.setup;
|
|
7658
|
+
} catch (error) {
|
|
7659
|
+
const secret = generateSecret();
|
|
7660
|
+
const otpAuthUrl = generateOtpAuthUrl(secret, email);
|
|
7661
|
+
const backupCodes = generateBackupCodes();
|
|
7662
|
+
return {
|
|
7663
|
+
secret,
|
|
7664
|
+
qrCodeDataUrl: otpAuthUrl,
|
|
7665
|
+
// In real implementation, would be a data URL
|
|
7666
|
+
backupCodes
|
|
7667
|
+
};
|
|
7668
|
+
}
|
|
7669
|
+
}
|
|
7670
|
+
/**
|
|
7671
|
+
* Verify TOTP code and complete setup
|
|
7672
|
+
*/
|
|
7673
|
+
async verifySetup(code) {
|
|
7674
|
+
try {
|
|
7675
|
+
const response = await api.verify2FA(code);
|
|
7676
|
+
return response.success;
|
|
7677
|
+
} catch (error) {
|
|
7678
|
+
throw error;
|
|
7679
|
+
}
|
|
7680
|
+
}
|
|
7681
|
+
/**
|
|
7682
|
+
* Verify TOTP code during login
|
|
7683
|
+
*/
|
|
7684
|
+
async verifyCode(code) {
|
|
7685
|
+
try {
|
|
7686
|
+
const response = await api.verify2FACode(code);
|
|
7687
|
+
return response.success;
|
|
7688
|
+
} catch (error) {
|
|
7689
|
+
throw error;
|
|
7690
|
+
}
|
|
7691
|
+
}
|
|
7692
|
+
/**
|
|
7693
|
+
* Verify backup code
|
|
7694
|
+
*/
|
|
7695
|
+
async verifyBackupCode(code) {
|
|
7696
|
+
try {
|
|
7697
|
+
const response = await api.verifyBackupCode(code);
|
|
7698
|
+
return response.success;
|
|
7699
|
+
} catch (error) {
|
|
7700
|
+
throw error;
|
|
7701
|
+
}
|
|
7702
|
+
}
|
|
7703
|
+
/**
|
|
7704
|
+
* Get 2FA status
|
|
7705
|
+
*/
|
|
7706
|
+
async getStatus() {
|
|
7707
|
+
try {
|
|
7708
|
+
const response = await api.get2FAStatus();
|
|
7709
|
+
return response;
|
|
7710
|
+
} catch (error) {
|
|
7711
|
+
return {
|
|
7712
|
+
enabled: false,
|
|
7713
|
+
backupCodesRemaining: 0
|
|
7714
|
+
};
|
|
7715
|
+
}
|
|
7716
|
+
}
|
|
7717
|
+
/**
|
|
7718
|
+
* Disable 2FA
|
|
7719
|
+
*/
|
|
7720
|
+
async disable(code) {
|
|
7721
|
+
try {
|
|
7722
|
+
const response = await api.disable2FA(code);
|
|
7723
|
+
return response.success;
|
|
7724
|
+
} catch (error) {
|
|
7725
|
+
throw error;
|
|
7726
|
+
}
|
|
7727
|
+
}
|
|
7728
|
+
/**
|
|
7729
|
+
* Generate new backup codes
|
|
7730
|
+
*/
|
|
7731
|
+
async regenerateBackupCodes() {
|
|
7732
|
+
try {
|
|
7733
|
+
const response = await api.regenerateBackupCodes();
|
|
7734
|
+
return response.codes;
|
|
7735
|
+
} catch (error) {
|
|
7736
|
+
throw error;
|
|
7737
|
+
}
|
|
7738
|
+
}
|
|
7739
|
+
/**
|
|
7740
|
+
* Verify TOTP code locally (for testing)
|
|
7741
|
+
*/
|
|
7742
|
+
verifyLocal(secret, code) {
|
|
7743
|
+
return verifyTOTP(secret, code);
|
|
7744
|
+
}
|
|
7745
|
+
/**
|
|
7746
|
+
* Generate TOTP code locally (for testing)
|
|
7747
|
+
*/
|
|
7748
|
+
generateLocal(secret) {
|
|
7749
|
+
return generateTOTP(secret);
|
|
7750
|
+
}
|
|
7751
|
+
};
|
|
7752
|
+
var DeviceTrustService = class {
|
|
7753
|
+
/**
|
|
7754
|
+
* Get current device fingerprint
|
|
7755
|
+
*/
|
|
7756
|
+
getFingerprint() {
|
|
7757
|
+
return generateDeviceFingerprint();
|
|
7758
|
+
}
|
|
7759
|
+
/**
|
|
7760
|
+
* Get current device name
|
|
7761
|
+
*/
|
|
7762
|
+
getDeviceName() {
|
|
7763
|
+
return getDeviceName();
|
|
7764
|
+
}
|
|
7765
|
+
/**
|
|
7766
|
+
* Register current device as trusted
|
|
7767
|
+
*/
|
|
7768
|
+
async trustDevice(name) {
|
|
7769
|
+
const fingerprint = this.getFingerprint();
|
|
7770
|
+
const deviceName = name || this.getDeviceName();
|
|
7771
|
+
try {
|
|
7772
|
+
const response = await api.trustDevice({
|
|
7773
|
+
name: deviceName,
|
|
7774
|
+
fingerprint
|
|
7775
|
+
});
|
|
7776
|
+
return response.device;
|
|
7777
|
+
} catch (error) {
|
|
7778
|
+
throw error;
|
|
7779
|
+
}
|
|
7780
|
+
}
|
|
7781
|
+
/**
|
|
7782
|
+
* Check if current device is trusted
|
|
7783
|
+
*/
|
|
7784
|
+
async isDeviceTrusted() {
|
|
7785
|
+
const fingerprint = this.getFingerprint();
|
|
7786
|
+
try {
|
|
7787
|
+
const devices = await api.getDevices();
|
|
7788
|
+
return devices.devices.some((d) => d.fingerprint === fingerprint);
|
|
7789
|
+
} catch (error) {
|
|
7790
|
+
return false;
|
|
7791
|
+
}
|
|
7792
|
+
}
|
|
7793
|
+
/**
|
|
7794
|
+
* List all trusted devices
|
|
7795
|
+
*/
|
|
7796
|
+
async listDevices() {
|
|
7797
|
+
try {
|
|
7798
|
+
const response = await api.getDevices();
|
|
7799
|
+
const currentFingerprint = this.getFingerprint();
|
|
7800
|
+
return response.devices.map((d) => ({
|
|
7801
|
+
...d,
|
|
7802
|
+
isCurrent: d.fingerprint === currentFingerprint
|
|
7803
|
+
}));
|
|
7804
|
+
} catch (error) {
|
|
7805
|
+
return [];
|
|
7806
|
+
}
|
|
7807
|
+
}
|
|
7808
|
+
/**
|
|
7809
|
+
* Revoke a trusted device
|
|
7810
|
+
*/
|
|
7811
|
+
async revokeDevice(deviceId) {
|
|
7812
|
+
try {
|
|
7813
|
+
const response = await api.revokeDevice(deviceId);
|
|
7814
|
+
return response.success;
|
|
7815
|
+
} catch (error) {
|
|
7816
|
+
throw error;
|
|
7817
|
+
}
|
|
7818
|
+
}
|
|
7819
|
+
/**
|
|
7820
|
+
* Revoke all devices except current
|
|
7821
|
+
*/
|
|
7822
|
+
async revokeAllDevices() {
|
|
7823
|
+
try {
|
|
7824
|
+
const response = await api.revokeAllDevices();
|
|
7825
|
+
return response.revokedCount;
|
|
7826
|
+
} catch (error) {
|
|
7827
|
+
throw error;
|
|
7828
|
+
}
|
|
7829
|
+
}
|
|
8238
7830
|
};
|
|
7831
|
+
var twoFactorService = new TwoFactorService();
|
|
7832
|
+
var deviceTrustService = new DeviceTrustService();
|
|
7833
|
+
function displayQRCode(url) {
|
|
7834
|
+
log.newline();
|
|
7835
|
+
log.info("Scan diesen QR-Code mit deiner Authenticator App:");
|
|
7836
|
+
log.newline();
|
|
7837
|
+
log.dim("(QR-Code-Anzeige erfordert qrcode-terminal Paket)");
|
|
7838
|
+
log.newline();
|
|
7839
|
+
log.plain(`URL: ${url}`);
|
|
7840
|
+
log.newline();
|
|
7841
|
+
}
|
|
7842
|
+
function displayBackupCodes(codes) {
|
|
7843
|
+
log.newline();
|
|
7844
|
+
console.log(colors.orange.bold("Backup-Codes (speichern!)"));
|
|
7845
|
+
console.log(colors.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7846
|
+
log.newline();
|
|
7847
|
+
for (let i = 0; i < codes.length; i++) {
|
|
7848
|
+
console.log(` ${i + 1}. ${codes[i]}`);
|
|
7849
|
+
}
|
|
7850
|
+
log.newline();
|
|
7851
|
+
log.warn("Diese Codes k\xF6nnen nur einmal verwendet werden!");
|
|
7852
|
+
log.dim("Speichere sie an einem sicheren Ort.");
|
|
7853
|
+
log.newline();
|
|
7854
|
+
}
|
|
7855
|
+
function displayDevices(devices) {
|
|
7856
|
+
log.newline();
|
|
7857
|
+
console.log(colors.orange.bold("Vertrauensw\xFCrdige Ger\xE4te"));
|
|
7858
|
+
console.log(colors.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7859
|
+
log.newline();
|
|
7860
|
+
if (devices.length === 0) {
|
|
7861
|
+
log.dim("Keine Ger\xE4te registriert");
|
|
7862
|
+
return;
|
|
7863
|
+
}
|
|
7864
|
+
for (const device of devices) {
|
|
7865
|
+
const current = device.isCurrent ? colors.green(" (aktuell)") : "";
|
|
7866
|
+
console.log(` ${device.name}${current}`);
|
|
7867
|
+
log.dim(` ID: ${device.id}`);
|
|
7868
|
+
log.dim(` Zuletzt: ${formatRelativeTime(device.lastUsed)}`);
|
|
7869
|
+
if (device.expiresAt) {
|
|
7870
|
+
log.dim(` L\xE4uft ab: ${formatRelativeTime(device.expiresAt)}`);
|
|
7871
|
+
} else {
|
|
7872
|
+
log.dim(" L\xE4uft ab: nie");
|
|
7873
|
+
}
|
|
7874
|
+
log.newline();
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
function formatRelativeTime(dateStr) {
|
|
7878
|
+
const date = new Date(dateStr);
|
|
7879
|
+
const now = /* @__PURE__ */ new Date();
|
|
7880
|
+
const diffMs = now.getTime() - date.getTime();
|
|
7881
|
+
const diffSec = Math.floor(diffMs / 1e3);
|
|
7882
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
7883
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
7884
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
7885
|
+
if (diffSec < 60) return "gerade eben";
|
|
7886
|
+
if (diffMin < 60) return `vor ${diffMin} Minuten`;
|
|
7887
|
+
if (diffHour < 24) return `vor ${diffHour} Stunden`;
|
|
7888
|
+
if (diffDay < 30) return `vor ${diffDay} Tagen`;
|
|
7889
|
+
return date.toLocaleDateString("de-DE");
|
|
7890
|
+
}
|
|
8239
7891
|
|
|
8240
|
-
// src/
|
|
8241
|
-
var
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
7892
|
+
// src/services/auth/auth.ts
|
|
7893
|
+
var PORT_RANGE_START = 9876;
|
|
7894
|
+
var PORT_RANGE_END = 9886;
|
|
7895
|
+
async function loginWithOtp(email) {
|
|
7896
|
+
try {
|
|
7897
|
+
log.info(`Sende Login-Code an ${email}...`);
|
|
7898
|
+
const otpResponse = await api.requestOtp(email);
|
|
7899
|
+
if (!otpResponse.success) {
|
|
7900
|
+
log.error(otpResponse.message || "Fehler beim Senden des Codes");
|
|
7901
|
+
return false;
|
|
7902
|
+
}
|
|
7903
|
+
log.success("Code wurde gesendet!");
|
|
7904
|
+
log.dim(`Verbleibende Versuche: ${otpResponse.remainingAttempts}`);
|
|
7905
|
+
log.newline();
|
|
7906
|
+
const { otp } = await inquirer.prompt([
|
|
7907
|
+
{
|
|
7908
|
+
type: "input",
|
|
7909
|
+
name: "otp",
|
|
7910
|
+
message: "Code eingeben:",
|
|
7911
|
+
validate: (input) => {
|
|
7912
|
+
if (!/^\d{6}$/.test(input)) {
|
|
7913
|
+
return "Bitte gib den 6-stelligen Code ein";
|
|
7914
|
+
}
|
|
7915
|
+
return true;
|
|
7916
|
+
}
|
|
7917
|
+
}
|
|
7918
|
+
]);
|
|
7919
|
+
const authResponse = await api.verifyOtp(otpResponse.token, otp);
|
|
7920
|
+
if (authResponse.requires2FA && authResponse.tempToken) {
|
|
7921
|
+
log.newline();
|
|
7922
|
+
log.info("2FA ist aktiviert. Bitte verifiziere dich.");
|
|
7923
|
+
log.newline();
|
|
7924
|
+
const result = await handle2FALoginFlow(authResponse.tempToken, authResponse.email || email);
|
|
7925
|
+
return result;
|
|
7926
|
+
}
|
|
7927
|
+
if (!authResponse.success || !authResponse.token || !authResponse.user) {
|
|
7928
|
+
log.error(authResponse.message || "Ung\xFCltiger Code");
|
|
7929
|
+
return false;
|
|
7930
|
+
}
|
|
7931
|
+
setAuth(authResponse.token, authResponse.user);
|
|
7932
|
+
log.newline();
|
|
7933
|
+
log.success(`Angemeldet als ${authResponse.user.email} (${authResponse.user.tier.toUpperCase()})`);
|
|
7934
|
+
return true;
|
|
7935
|
+
} catch (error) {
|
|
7936
|
+
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
|
7937
|
+
log.error(`Login fehlgeschlagen: ${message}`);
|
|
7938
|
+
return false;
|
|
7939
|
+
}
|
|
8250
7940
|
}
|
|
8251
|
-
function
|
|
8252
|
-
|
|
7941
|
+
async function handle2FALoginFlow(tempToken, email) {
|
|
7942
|
+
const MAX_ATTEMPTS = 3;
|
|
7943
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
7944
|
+
const { code } = await inquirer.prompt([{
|
|
7945
|
+
type: "input",
|
|
7946
|
+
name: "code",
|
|
7947
|
+
message: "2FA-Code (6 Ziffern oder Backup-Code):",
|
|
7948
|
+
validate: (input) => {
|
|
7949
|
+
if (/^\d{6}$/.test(input) || /^[A-Za-z0-9-]+$/.test(input)) {
|
|
7950
|
+
return true;
|
|
7951
|
+
}
|
|
7952
|
+
return "Bitte gib einen g\xFCltigen Code ein";
|
|
7953
|
+
}
|
|
7954
|
+
}]);
|
|
7955
|
+
const { rememberDevice } = await inquirer.prompt([{
|
|
7956
|
+
type: "confirm",
|
|
7957
|
+
name: "rememberDevice",
|
|
7958
|
+
message: "Diesem Ger\xE4t vertrauen?",
|
|
7959
|
+
default: true
|
|
7960
|
+
}]);
|
|
7961
|
+
try {
|
|
7962
|
+
const result = await api.validate2FALogin({
|
|
7963
|
+
tempToken,
|
|
7964
|
+
code,
|
|
7965
|
+
rememberDevice
|
|
7966
|
+
});
|
|
7967
|
+
if (result.success && result.token && result.user) {
|
|
7968
|
+
setAuth(result.token, result.user);
|
|
7969
|
+
if (result.method === "backup" && result.backupCodesRemaining !== void 0) {
|
|
7970
|
+
log.warn(`Backup-Code verwendet. Noch ${result.backupCodesRemaining} Codes \xFCbrig.`);
|
|
7971
|
+
}
|
|
7972
|
+
log.newline();
|
|
7973
|
+
log.success(`Angemeldet als ${result.user.email} (${result.user.tier.toUpperCase()})`);
|
|
7974
|
+
return true;
|
|
7975
|
+
}
|
|
7976
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
7977
|
+
log.error(`Ung\xFCltiger Code. Noch ${MAX_ATTEMPTS - attempt} Versuche.`);
|
|
7978
|
+
}
|
|
7979
|
+
} catch (error) {
|
|
7980
|
+
const message = error instanceof Error ? error.message : "Verifizierung fehlgeschlagen";
|
|
7981
|
+
if (message.includes("expired") || message.includes("abgelaufen")) {
|
|
7982
|
+
log.error("2FA-Token abgelaufen. Bitte erneut anmelden.");
|
|
7983
|
+
return false;
|
|
7984
|
+
}
|
|
7985
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
7986
|
+
log.error(`${message}. Noch ${MAX_ATTEMPTS - attempt} Versuche.`);
|
|
7987
|
+
}
|
|
7988
|
+
}
|
|
7989
|
+
}
|
|
7990
|
+
log.error("2FA-Verifizierung fehlgeschlagen");
|
|
7991
|
+
return false;
|
|
7992
|
+
}
|
|
7993
|
+
async function findAvailablePort() {
|
|
7994
|
+
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
|
|
7995
|
+
const isAvailable = await checkPort(port);
|
|
7996
|
+
if (isAvailable) {
|
|
7997
|
+
return port;
|
|
7998
|
+
}
|
|
7999
|
+
}
|
|
8000
|
+
throw new Error("Kein freier Port f\xFCr Callback-Server gefunden");
|
|
8001
|
+
}
|
|
8002
|
+
function checkPort(port) {
|
|
8003
|
+
return new Promise((resolve) => {
|
|
8004
|
+
const server = http.createServer();
|
|
8005
|
+
server.once("error", () => resolve(false));
|
|
8006
|
+
server.once("listening", () => {
|
|
8007
|
+
server.close();
|
|
8008
|
+
resolve(true);
|
|
8009
|
+
});
|
|
8010
|
+
server.listen(port, "127.0.0.1");
|
|
8011
|
+
});
|
|
8012
|
+
}
|
|
8013
|
+
function getCallbackHtml(type, data) {
|
|
8014
|
+
const baseStyles = `
|
|
8015
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
|
8016
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
8017
|
+
body {
|
|
8018
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
8019
|
+
min-height: 100vh;
|
|
8020
|
+
display: flex;
|
|
8021
|
+
align-items: center;
|
|
8022
|
+
justify-content: center;
|
|
8023
|
+
background: #000000;
|
|
8024
|
+
color: #fafafa;
|
|
8025
|
+
position: relative;
|
|
8026
|
+
}
|
|
8027
|
+
body::before {
|
|
8028
|
+
content: '';
|
|
8029
|
+
position: fixed;
|
|
8030
|
+
inset: 0;
|
|
8031
|
+
background:
|
|
8032
|
+
radial-gradient(ellipse at 20% 0%, rgba(255, 107, 44, 0.12) 0%, transparent 50%),
|
|
8033
|
+
radial-gradient(ellipse at 80% 100%, rgba(0, 212, 255, 0.08) 0%, transparent 50%);
|
|
8034
|
+
pointer-events: none;
|
|
8035
|
+
}
|
|
8036
|
+
.container {
|
|
8037
|
+
text-align: center;
|
|
8038
|
+
padding: 3rem;
|
|
8039
|
+
max-width: 420px;
|
|
8040
|
+
position: relative;
|
|
8041
|
+
z-index: 1;
|
|
8042
|
+
}
|
|
8043
|
+
.logo {
|
|
8044
|
+
width: 72px;
|
|
8045
|
+
height: 72px;
|
|
8046
|
+
margin: 0 auto 2rem;
|
|
8047
|
+
color: #FF6B2C;
|
|
8048
|
+
filter: drop-shadow(0 0 30px rgba(255, 107, 44, 0.4));
|
|
8049
|
+
}
|
|
8050
|
+
.logo svg {
|
|
8051
|
+
width: 100%;
|
|
8052
|
+
height: 100%;
|
|
8053
|
+
}
|
|
8054
|
+
.brand {
|
|
8055
|
+
font-size: 0.875rem;
|
|
8056
|
+
font-weight: 600;
|
|
8057
|
+
letter-spacing: 0.1em;
|
|
8058
|
+
text-transform: uppercase;
|
|
8059
|
+
color: #a0a0a0;
|
|
8060
|
+
margin-bottom: 2rem;
|
|
8061
|
+
}
|
|
8062
|
+
h1 {
|
|
8063
|
+
font-size: 2rem;
|
|
8064
|
+
font-weight: 700;
|
|
8065
|
+
margin-bottom: 0.75rem;
|
|
8066
|
+
background: linear-gradient(135deg, #fafafa 0%, #a0a0a0 100%);
|
|
8067
|
+
-webkit-background-clip: text;
|
|
8068
|
+
-webkit-text-fill-color: transparent;
|
|
8069
|
+
background-clip: text;
|
|
8070
|
+
}
|
|
8071
|
+
.subtitle {
|
|
8072
|
+
color: #a0a0a0;
|
|
8073
|
+
font-size: 1rem;
|
|
8074
|
+
line-height: 1.6;
|
|
8075
|
+
}
|
|
8076
|
+
.email {
|
|
8077
|
+
color: #FF6B2C;
|
|
8078
|
+
font-weight: 600;
|
|
8079
|
+
}
|
|
8080
|
+
.status-icon {
|
|
8081
|
+
width: 72px;
|
|
8082
|
+
height: 72px;
|
|
8083
|
+
margin: 0 auto 1.5rem;
|
|
8084
|
+
border-radius: 50%;
|
|
8085
|
+
display: flex;
|
|
8086
|
+
align-items: center;
|
|
8087
|
+
justify-content: center;
|
|
8088
|
+
position: relative;
|
|
8089
|
+
}
|
|
8090
|
+
.status-icon::before {
|
|
8091
|
+
content: '';
|
|
8092
|
+
position: absolute;
|
|
8093
|
+
inset: -4px;
|
|
8094
|
+
border-radius: 50%;
|
|
8095
|
+
padding: 4px;
|
|
8096
|
+
background: linear-gradient(135deg, var(--icon-color) 0%, var(--icon-color-dark) 100%);
|
|
8097
|
+
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
8098
|
+
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
8099
|
+
-webkit-mask-composite: xor;
|
|
8100
|
+
mask-composite: exclude;
|
|
8101
|
+
opacity: 0.5;
|
|
8102
|
+
}
|
|
8103
|
+
.status-icon.success {
|
|
8104
|
+
--icon-color: #10B981;
|
|
8105
|
+
--icon-color-dark: #059669;
|
|
8106
|
+
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
|
|
8107
|
+
box-shadow: 0 0 40px rgba(16, 185, 129, 0.4);
|
|
8108
|
+
}
|
|
8109
|
+
.status-icon.error {
|
|
8110
|
+
--icon-color: #EF4444;
|
|
8111
|
+
--icon-color-dark: #DC2626;
|
|
8112
|
+
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
|
|
8113
|
+
box-shadow: 0 0 40px rgba(239, 68, 68, 0.4);
|
|
8114
|
+
}
|
|
8115
|
+
.status-icon svg {
|
|
8116
|
+
width: 36px;
|
|
8117
|
+
height: 36px;
|
|
8118
|
+
}
|
|
8119
|
+
.spinner {
|
|
8120
|
+
width: 56px;
|
|
8121
|
+
height: 56px;
|
|
8122
|
+
margin: 0 auto 1.5rem;
|
|
8123
|
+
border: 3px solid rgba(255, 107, 44, 0.15);
|
|
8124
|
+
border-top-color: #FF6B2C;
|
|
8125
|
+
border-radius: 50%;
|
|
8126
|
+
animation: spin 1s linear infinite;
|
|
8127
|
+
}
|
|
8128
|
+
@keyframes spin {
|
|
8129
|
+
to { transform: rotate(360deg); }
|
|
8130
|
+
}
|
|
8131
|
+
.hint {
|
|
8132
|
+
margin-top: 2rem;
|
|
8133
|
+
padding-top: 2rem;
|
|
8134
|
+
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
8135
|
+
color: #666666;
|
|
8136
|
+
font-size: 0.875rem;
|
|
8137
|
+
}
|
|
8138
|
+
.card {
|
|
8139
|
+
background: rgba(13, 13, 13, 0.8);
|
|
8140
|
+
border: 1px solid #1a1a1a;
|
|
8141
|
+
border-radius: 16px;
|
|
8142
|
+
padding: 2.5rem;
|
|
8143
|
+
backdrop-filter: blur(10px);
|
|
8144
|
+
}
|
|
8145
|
+
`;
|
|
8146
|
+
const shivaEye = `<svg viewBox="0 -32 576 576" fill="currentColor"><defs><linearGradient id="eyeGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#FF6B2C"/><stop offset="100%" style="stop-color:#FF8F5C"/></linearGradient></defs><path fill="url(#eyeGrad)" d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"/></svg>`;
|
|
8147
|
+
const checkIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
|
|
8148
|
+
const xIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
|
8149
|
+
const lang = getLanguage();
|
|
8150
|
+
if (type === "success") {
|
|
8151
|
+
return `<!DOCTYPE html>
|
|
8152
|
+
<html lang="${lang}">
|
|
8153
|
+
<head>
|
|
8154
|
+
<meta charset="UTF-8">
|
|
8155
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8156
|
+
<title>${t("auth.callback.pageTitleSuccess")}</title>
|
|
8157
|
+
<link rel="icon" type="image/svg+xml" href="https://shiva.li/favicon.svg">
|
|
8158
|
+
<style>${baseStyles}</style>
|
|
8159
|
+
</head>
|
|
8160
|
+
<body>
|
|
8161
|
+
<div class="container">
|
|
8162
|
+
<div class="card">
|
|
8163
|
+
<div class="logo">${shivaEye}</div>
|
|
8164
|
+
<p class="brand">SHIVA Code</p>
|
|
8165
|
+
<div class="status-icon success">${checkIcon}</div>
|
|
8166
|
+
<h1>${t("auth.callback.successTitle")}</h1>
|
|
8167
|
+
<p class="subtitle">${t("auth.callback.successSubtitle")}<br><span class="email">${data.email}</span></p>
|
|
8168
|
+
<p class="hint">${t("auth.callback.successHint")}</p>
|
|
8169
|
+
</div>
|
|
8170
|
+
</div>
|
|
8171
|
+
<script>setTimeout(() => window.close(), 3000);</script>
|
|
8172
|
+
</body>
|
|
8173
|
+
</html>`;
|
|
8174
|
+
}
|
|
8175
|
+
if (type === "error") {
|
|
8176
|
+
return `<!DOCTYPE html>
|
|
8177
|
+
<html lang="${lang}">
|
|
8178
|
+
<head>
|
|
8179
|
+
<meta charset="UTF-8">
|
|
8180
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8181
|
+
<title>${t("auth.callback.pageTitleError")}</title>
|
|
8182
|
+
<link rel="icon" type="image/svg+xml" href="https://shiva.li/favicon.svg">
|
|
8183
|
+
<style>${baseStyles}</style>
|
|
8184
|
+
</head>
|
|
8185
|
+
<body>
|
|
8186
|
+
<div class="container">
|
|
8187
|
+
<div class="card">
|
|
8188
|
+
<div class="logo">${shivaEye}</div>
|
|
8189
|
+
<p class="brand">SHIVA Code</p>
|
|
8190
|
+
<div class="status-icon error">${xIcon}</div>
|
|
8191
|
+
<h1>${t("auth.callback.errorTitle")}</h1>
|
|
8192
|
+
<p class="subtitle">${data.message || t("auth.callback.errorDefault")}</p>
|
|
8193
|
+
<p class="hint">${t("auth.callback.errorHint")}</p>
|
|
8194
|
+
</div>
|
|
8195
|
+
</div>
|
|
8196
|
+
</body>
|
|
8197
|
+
</html>`;
|
|
8198
|
+
}
|
|
8199
|
+
return `<!DOCTYPE html>
|
|
8200
|
+
<html lang="${lang}">
|
|
8201
|
+
<head>
|
|
8202
|
+
<meta charset="UTF-8">
|
|
8203
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8204
|
+
<title>${t("auth.callback.pageTitle")}</title>
|
|
8205
|
+
<link rel="icon" type="image/svg+xml" href="https://shiva.li/favicon.svg">
|
|
8206
|
+
<style>${baseStyles}</style>
|
|
8207
|
+
</head>
|
|
8208
|
+
<body>
|
|
8209
|
+
<div class="container">
|
|
8210
|
+
<div class="card">
|
|
8211
|
+
<div class="logo">${shivaEye}</div>
|
|
8212
|
+
<p class="brand">SHIVA Code</p>
|
|
8213
|
+
<div class="spinner"></div>
|
|
8214
|
+
<h1>${t("auth.callback.waitingTitle")}</h1>
|
|
8215
|
+
<p class="subtitle">${t("auth.callback.waitingSubtitle")}</p>
|
|
8216
|
+
</div>
|
|
8217
|
+
</div>
|
|
8218
|
+
</body>
|
|
8219
|
+
</html>`;
|
|
8253
8220
|
}
|
|
8254
|
-
function
|
|
8255
|
-
return
|
|
8221
|
+
function startCallbackServer(port) {
|
|
8222
|
+
return new Promise((resolve, reject) => {
|
|
8223
|
+
const server = http.createServer((req, res) => {
|
|
8224
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
|
|
8225
|
+
if (url.pathname === "/callback") {
|
|
8226
|
+
const token = url.searchParams.get("token");
|
|
8227
|
+
const userId = url.searchParams.get("userId");
|
|
8228
|
+
const email = url.searchParams.get("email");
|
|
8229
|
+
const tier = url.searchParams.get("tier");
|
|
8230
|
+
const error = url.searchParams.get("error");
|
|
8231
|
+
if (error) {
|
|
8232
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
8233
|
+
res.end(getCallbackHtml("error", { message: error }));
|
|
8234
|
+
server.close();
|
|
8235
|
+
reject(new Error(error));
|
|
8236
|
+
return;
|
|
8237
|
+
}
|
|
8238
|
+
if (!token || !userId || !email) {
|
|
8239
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
8240
|
+
res.end(getCallbackHtml("error", { message: "Token oder Benutzer-Daten fehlen." }));
|
|
8241
|
+
server.close();
|
|
8242
|
+
reject(new Error("Ung\xFCltige Callback-Parameter"));
|
|
8243
|
+
return;
|
|
8244
|
+
}
|
|
8245
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
8246
|
+
res.end(getCallbackHtml("success", { email }));
|
|
8247
|
+
setTimeout(() => {
|
|
8248
|
+
server.close();
|
|
8249
|
+
resolve({
|
|
8250
|
+
token,
|
|
8251
|
+
user: {
|
|
8252
|
+
id: parseInt(userId, 10),
|
|
8253
|
+
email,
|
|
8254
|
+
tier: tier || "free"
|
|
8255
|
+
}
|
|
8256
|
+
});
|
|
8257
|
+
}, 100);
|
|
8258
|
+
return;
|
|
8259
|
+
}
|
|
8260
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
8261
|
+
res.end(getCallbackHtml("waiting", {}));
|
|
8262
|
+
});
|
|
8263
|
+
const timeout = setTimeout(() => {
|
|
8264
|
+
server.close();
|
|
8265
|
+
reject(new Error("Login-Timeout - keine Antwort erhalten"));
|
|
8266
|
+
}, 3e5);
|
|
8267
|
+
server.once("close", () => {
|
|
8268
|
+
clearTimeout(timeout);
|
|
8269
|
+
});
|
|
8270
|
+
server.once("error", (err) => {
|
|
8271
|
+
clearTimeout(timeout);
|
|
8272
|
+
reject(err);
|
|
8273
|
+
});
|
|
8274
|
+
server.listen(port, "127.0.0.1");
|
|
8275
|
+
});
|
|
8256
8276
|
}
|
|
8257
|
-
function
|
|
8258
|
-
const
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8277
|
+
async function loginWithBrowser() {
|
|
8278
|
+
const config = getConfig();
|
|
8279
|
+
try {
|
|
8280
|
+
const port = await findAvailablePort();
|
|
8281
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
8282
|
+
const baseUrl = config.apiEndpoint.replace("/api", "");
|
|
8283
|
+
const lang = getLanguage();
|
|
8284
|
+
const loginUrl = `${baseUrl}/auth/cli-login?callback=${encodeURIComponent(callbackUrl)}&lang=${lang}`;
|
|
8285
|
+
const serverPromise = startCallbackServer(port);
|
|
8286
|
+
log.newline();
|
|
8287
|
+
log.info("\xD6ffne Browser zur Anmeldung...");
|
|
8288
|
+
try {
|
|
8289
|
+
await open(loginUrl);
|
|
8290
|
+
} catch {
|
|
8266
8291
|
}
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
);
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8292
|
+
log.newline();
|
|
8293
|
+
log.plain("Bitte \xF6ffne diese URL in deinem Browser um die Anmeldung abzuschlie\xDFen:");
|
|
8294
|
+
log.newline();
|
|
8295
|
+
console.log(` ${colors.cyan(loginUrl)}`);
|
|
8296
|
+
log.newline();
|
|
8297
|
+
log.dim("Warte auf Anmeldung... (Timeout: 5 Minuten)");
|
|
8298
|
+
log.newline();
|
|
8299
|
+
const result = await serverPromise;
|
|
8300
|
+
setAuth(result.token, result.user);
|
|
8301
|
+
try {
|
|
8302
|
+
const tfaStatus = await twoFactorService.getStatus();
|
|
8303
|
+
if (tfaStatus.enabled) {
|
|
8304
|
+
log.newline();
|
|
8305
|
+
log.info("2FA ist aktiviert. Bitte verifiziere dich.");
|
|
8306
|
+
log.newline();
|
|
8307
|
+
const isDeviceTrusted = await deviceTrustService.isDeviceTrusted();
|
|
8308
|
+
if (!isDeviceTrusted) {
|
|
8309
|
+
const verified = await prompt2FAVerification();
|
|
8310
|
+
if (!verified) {
|
|
8311
|
+
clearAuth();
|
|
8312
|
+
log.error("2FA-Verifizierung fehlgeschlagen");
|
|
8313
|
+
return false;
|
|
8314
|
+
}
|
|
8315
|
+
const { trustDevice } = await inquirer.prompt([{
|
|
8316
|
+
type: "confirm",
|
|
8317
|
+
name: "trustDevice",
|
|
8318
|
+
message: "Diesem Ger\xE4t vertrauen?",
|
|
8319
|
+
default: true
|
|
8320
|
+
}]);
|
|
8321
|
+
if (trustDevice) {
|
|
8322
|
+
try {
|
|
8323
|
+
await deviceTrustService.trustDevice();
|
|
8324
|
+
log.success("Ger\xE4t als vertrauensw\xFCrdig markiert");
|
|
8325
|
+
} catch {
|
|
8326
|
+
}
|
|
8327
|
+
}
|
|
8328
|
+
} else {
|
|
8329
|
+
log.dim("Ger\xE4t ist vertrauensw\xFCrdig - 2FA \xFCbersprungen");
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
8332
|
+
} catch {
|
|
8280
8333
|
}
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
}
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8334
|
+
log.success(`Angemeldet als ${result.user.email} (${result.user.tier.toUpperCase()})`);
|
|
8335
|
+
return true;
|
|
8336
|
+
} catch (error) {
|
|
8337
|
+
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
|
8338
|
+
log.error(`Login fehlgeschlagen: ${message}`);
|
|
8339
|
+
log.newline();
|
|
8340
|
+
log.info("Fallback: Manueller Token-Eintrag");
|
|
8341
|
+
const fallbackLang = getLanguage();
|
|
8342
|
+
const loginUrl = `${config.apiEndpoint.replace("/api", "")}/auth/cli-login?lang=${fallbackLang}`;
|
|
8343
|
+
log.dim(`URL: ${loginUrl}`);
|
|
8344
|
+
log.newline();
|
|
8345
|
+
const { useManual } = await inquirer.prompt([{
|
|
8346
|
+
type: "confirm",
|
|
8347
|
+
name: "useManual",
|
|
8348
|
+
message: "Token manuell eingeben?",
|
|
8349
|
+
default: true
|
|
8350
|
+
}]);
|
|
8351
|
+
if (!useManual) {
|
|
8352
|
+
return false;
|
|
8353
|
+
}
|
|
8354
|
+
try {
|
|
8355
|
+
await open(loginUrl);
|
|
8356
|
+
} catch {
|
|
8357
|
+
log.dim("Browser konnte nicht ge\xF6ffnet werden");
|
|
8358
|
+
log.dim(`\xD6ffne manuell: ${loginUrl}`);
|
|
8359
|
+
}
|
|
8360
|
+
const { token } = await inquirer.prompt([{
|
|
8361
|
+
type: "password",
|
|
8362
|
+
name: "token",
|
|
8363
|
+
message: "Token:",
|
|
8364
|
+
mask: "*"
|
|
8365
|
+
}]);
|
|
8366
|
+
if (!token) {
|
|
8367
|
+
log.error("Kein Token eingegeben");
|
|
8368
|
+
return false;
|
|
8369
|
+
}
|
|
8370
|
+
setAuth(token, { id: 0, email: "", tier: "free" });
|
|
8371
|
+
try {
|
|
8372
|
+
const response = await api.getCurrentUser();
|
|
8373
|
+
setAuth(token, response.user);
|
|
8374
|
+
log.success(`Angemeldet als ${response.user.email}`);
|
|
8375
|
+
return true;
|
|
8376
|
+
} catch {
|
|
8377
|
+
clearAuth();
|
|
8378
|
+
log.error("Ung\xFCltiger Token");
|
|
8379
|
+
return false;
|
|
8293
8380
|
}
|
|
8294
8381
|
}
|
|
8295
|
-
if (!Array.isArray(value)) {
|
|
8296
|
-
value = keys.reduce(
|
|
8297
|
-
(obj, k) => obj && typeof obj === "object" && k in obj ? obj[k] : void 0,
|
|
8298
|
-
translations.de
|
|
8299
|
-
);
|
|
8300
|
-
}
|
|
8301
|
-
return Array.isArray(value) ? value : [];
|
|
8302
8382
|
}
|
|
8303
|
-
function
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
const
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8383
|
+
function logout() {
|
|
8384
|
+
clearAuth();
|
|
8385
|
+
log.success("Abgemeldet");
|
|
8386
|
+
}
|
|
8387
|
+
async function prompt2FAVerification() {
|
|
8388
|
+
const MAX_ATTEMPTS = 3;
|
|
8389
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
8390
|
+
const { method } = await inquirer.prompt([{
|
|
8391
|
+
type: "list",
|
|
8392
|
+
name: "method",
|
|
8393
|
+
message: "Verifizierungsmethode:",
|
|
8394
|
+
choices: [
|
|
8395
|
+
{ name: "Authenticator App Code", value: "totp" },
|
|
8396
|
+
{ name: "Backup Code", value: "backup" },
|
|
8397
|
+
{ name: "Abbrechen", value: "cancel" }
|
|
8398
|
+
]
|
|
8399
|
+
}]);
|
|
8400
|
+
if (method === "cancel") {
|
|
8401
|
+
return false;
|
|
8402
|
+
}
|
|
8403
|
+
const promptMessage = method === "totp" ? "6-stelliger Code aus deiner Authenticator App:" : "Backup-Code (Format: XXX-XXX-XXX-XXX):";
|
|
8404
|
+
const { code } = await inquirer.prompt([{
|
|
8405
|
+
type: "input",
|
|
8406
|
+
name: "code",
|
|
8407
|
+
message: promptMessage,
|
|
8408
|
+
validate: (input) => {
|
|
8409
|
+
if (method === "totp") {
|
|
8410
|
+
return /^\d{6}$/.test(input) || "Bitte gib einen 6-stelligen Code ein";
|
|
8411
|
+
}
|
|
8412
|
+
return input.length > 0 || "Bitte gib einen Backup-Code ein";
|
|
8413
|
+
}
|
|
8414
|
+
}]);
|
|
8415
|
+
try {
|
|
8416
|
+
let verified = false;
|
|
8417
|
+
if (method === "totp") {
|
|
8418
|
+
verified = await twoFactorService.verifyCode(code);
|
|
8419
|
+
} else {
|
|
8420
|
+
verified = await twoFactorService.verifyBackupCode(code);
|
|
8421
|
+
}
|
|
8422
|
+
if (verified) {
|
|
8423
|
+
log.success("2FA-Verifizierung erfolgreich");
|
|
8424
|
+
return true;
|
|
8425
|
+
}
|
|
8426
|
+
} catch (error) {
|
|
8427
|
+
}
|
|
8428
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
8429
|
+
log.error(`Ung\xFCltiger Code. Noch ${MAX_ATTEMPTS - attempt} Versuche.`);
|
|
8314
8430
|
}
|
|
8315
|
-
} catch {
|
|
8316
8431
|
}
|
|
8317
|
-
|
|
8432
|
+
return false;
|
|
8318
8433
|
}
|
|
8319
8434
|
|
|
8320
8435
|
// src/commands/auth/login.ts
|