vaultfs 1.0.1 → 1.0.3

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.
@@ -1,471 +1,267 @@
1
- package auth;
2
-
3
-
4
-
5
- import com.sun.net.httpserver.HttpExchange;
6
-
7
- import com.sun.net.httpserver.HttpHandler;
8
-
9
- import com.sun.net.httpserver.HttpServer;
10
-
11
- import java.awt.Desktop;
12
- import java.io.BufferedReader;
13
- import java.io.File;
14
- import java.io.FileReader;
15
- import java.io.FileWriter;
16
- import java.io.IOException;
17
- import java.io.OutputStream;
18
- import java.net.InetSocketAddress;
19
- import java.net.URI;
20
- import java.net.URLDecoder;
21
- import java.nio.file.Files;
22
- import java.util.UUID;
23
- import java.util.concurrent.CountDownLatch;
24
- import java.util.concurrent.TimeUnit;
25
-
26
- import utils.Colors;
27
-
28
-
29
-
30
- /** Manages local AuthFS login state, device identity, and account display. */
31
-
32
- public class AuthManager {
33
-
34
- private static final String TOKEN_DIR = System.getProperty("user.home") + "/.authfs";
35
-
36
- private static final String TOKEN_FILE = TOKEN_DIR + "/token";
37
-
38
- private static final String EMAIL_FILE = TOKEN_DIR + "/email";
39
-
40
- private static final String NAME_FILE = TOKEN_DIR + "/name";
41
-
42
- private static final String CONFIG_FILE = TOKEN_DIR + "/config";
43
-
44
-
45
-
46
- /** Returns whether a non-empty token file exists. */
47
-
48
- public static boolean isLoggedIn() {
49
-
50
- File token = new File(TOKEN_FILE);
51
-
52
- return token.exists() && token.length() > 0;
53
-
54
- }
55
-
56
-
57
-
58
- /** Returns the saved user email or Unknown when not available. */
59
-
60
- public static String getUserEmail() {
61
-
62
- String email = readFile(EMAIL_FILE);
63
-
64
- if (email == null || email.isEmpty()) {
65
-
66
- return "Unknown";
67
-
68
- }
69
-
70
- return email;
71
-
72
- }
73
-
74
-
75
-
76
- /** Returns the saved user name or Unknown when not available. */
77
-
78
- public static String getUserName() {
79
-
80
- String name = readFile(NAME_FILE);
81
-
82
- if (name == null || name.isEmpty()) {
83
-
84
- return "Unknown";
85
-
86
- }
87
-
88
- return name;
89
-
90
- }
91
-
92
-
93
-
94
- /** Returns the device ID from config or generates and persists a new UUID. */
95
-
96
- public static String getDeviceId() {
97
-
98
- String deviceId = readFile(CONFIG_FILE);
99
-
100
- if (deviceId != null && !deviceId.isEmpty()) {
101
-
102
- return deviceId;
103
-
104
- }
105
-
106
-
107
-
108
- new File(TOKEN_DIR).mkdirs();
109
-
110
- String generated = UUID.randomUUID().toString();
111
-
112
- writeFile(CONFIG_FILE, generated);
113
-
114
- return generated;
115
-
116
- }
117
-
118
-
119
-
120
- /** Starts browser-based login, waits for callback, and stores token and email. */
121
-
122
- public static void startLoginFlow() {
123
-
124
- try {
125
-
126
- final String sessionToken = UUID.randomUUID().toString();
127
-
128
- String authURL = "http://localhost:9000/login?session=" + sessionToken;
129
-
130
-
131
-
132
- System.out.println(Colors.c(Colors.WHITE, "Opening browser for authentication..."));
133
-
134
- System.out.println(Colors.c(Colors.GRAY, "→ " + authURL));
135
-
136
-
137
-
138
- if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
139
- Desktop.getDesktop().browse(new URI(authURL));
140
- } else {
141
- System.out.println(Colors.c(Colors.YELLOW, "Could not open browser automatically."));
142
- System.out.println(Colors.c(Colors.WHITE, "Please open this URL manually: ") + authURL);
143
- }
144
-
145
-
146
-
147
- System.out.println(Colors.c(Colors.GRAY, "Waiting for authentication... (timeout: 120s)"));
148
-
149
-
150
-
151
- final CountDownLatch loginLatch = new CountDownLatch(1);
152
- final HttpServer server = HttpServer.create(new InetSocketAddress(9000), 0);
153
-
154
-
155
-
156
- // Serve the React build index.html at /login
157
- server.createContext("/login", new HttpHandler() {
158
- @Override
159
- public void handle(HttpExchange exchange) throws IOException {
160
- File file = new File(System.getProperty("user.dir") + "/frontend/dist/index.html");
161
- String response = "";
162
- if (file.exists()) {
163
- response = readFile(file.getAbsolutePath());
164
- } else {
165
- response = "<html><body><h1>Error: frontend build not found. Run npm run build in frontend/</h1></body></html>";
166
- }
167
- exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
168
- exchange.sendResponseHeaders(200, response.getBytes("UTF-8").length);
169
- OutputStream os = exchange.getResponseBody();
170
- os.write(response.getBytes("UTF-8"));
171
- os.close();
172
- }
173
- });
174
-
175
- // Serve static assets (JS, CSS) from frontend/dist/assets/
176
- server.createContext("/assets", new HttpHandler() {
177
- @Override
178
- public void handle(HttpExchange exchange) throws IOException {
179
- String path = exchange.getRequestURI().getPath();
180
- File file = new File(System.getProperty("user.dir") + "/frontend/dist" + path);
181
- if (file.exists() && file.isFile()) {
182
- String contentType = "application/octet-stream";
183
- if (path.endsWith(".js")) contentType = "application/javascript; charset=UTF-8";
184
- else if (path.endsWith(".css")) contentType = "text/css; charset=UTF-8";
185
- else if (path.endsWith(".svg")) contentType = "image/svg+xml";
186
- byte[] bytes = Files.readAllBytes(file.toPath());
187
- exchange.getResponseHeaders().set("Content-Type", contentType);
188
- exchange.sendResponseHeaders(200, bytes.length);
189
- OutputStream os = exchange.getResponseBody();
190
- os.write(bytes);
191
- os.close();
192
- } else {
193
- String msg = "Not found";
194
- exchange.sendResponseHeaders(404, msg.length());
195
- OutputStream os = exchange.getResponseBody();
196
- os.write(msg.getBytes());
197
- os.close();
198
- }
199
- }
200
- });
201
-
202
- // Redirect to Google OAuth
203
- server.createContext("/auth/google", new HttpHandler() {
204
- @Override
205
- public void handle(HttpExchange exchange) throws IOException {
206
- if (!OAuthConfig.isGoogleConfigured()) {
207
- String msg = "<html><body style='font-family:sans-serif;background:#000;color:#fff;display:flex;align-items:center;justify-content:center;height:100vh'><div style='text-align:center'><h2>Google OAuth not configured</h2><p style='color:#86868b;margin-top:12px'>Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in .env</p></div></div></body></html>";
208
- exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
209
- exchange.sendResponseHeaders(200, msg.getBytes("UTF-8").length);
210
- OutputStream os = exchange.getResponseBody();
211
- os.write(msg.getBytes("UTF-8"));
212
- os.close();
213
- return;
214
- }
215
- String url = OAuthHandler.getGoogleAuthUrl(sessionToken);
216
- exchange.getResponseHeaders().set("Location", url);
217
- exchange.sendResponseHeaders(302, -1);
218
- exchange.close();
219
- }
220
- });
221
-
222
- // Redirect to GitHub OAuth
223
- server.createContext("/auth/github", new HttpHandler() {
224
- @Override
225
- public void handle(HttpExchange exchange) throws IOException {
226
- if (!OAuthConfig.isGitHubConfigured()) {
227
- String msg = "<html><body style='font-family:sans-serif;background:#000;color:#fff;display:flex;align-items:center;justify-content:center;height:100vh'><div style='text-align:center'><h2>GitHub OAuth not configured</h2><p style='color:#86868b;margin-top:12px'>Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET in .env</p></div></div></body></html>";
228
- exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
229
- exchange.sendResponseHeaders(200, msg.getBytes("UTF-8").length);
230
- OutputStream os = exchange.getResponseBody();
231
- os.write(msg.getBytes("UTF-8"));
232
- os.close();
233
- return;
234
- }
235
- String url = OAuthHandler.getGitHubAuthUrl(sessionToken);
236
- exchange.getResponseHeaders().set("Location", url);
237
- exchange.sendResponseHeaders(302, -1);
238
- exchange.close();
239
- }
240
- });
241
-
242
- // Google OAuth callback — exchange code for user info
243
- server.createContext("/callback/google", new HttpHandler() {
244
- @Override
245
- public void handle(HttpExchange exchange) throws IOException {
246
- String state = extractQueryParam(exchange.getRequestURI().getQuery(), "state");
247
- if (state == null || !state.equals(sessionToken)) {
248
- serveError(exchange, "Invalid session. Please restart login.");
249
- exchange.close();
250
- return;
251
- }
252
-
253
- String code = extractQueryParam(exchange.getRequestURI().getQuery(), "code");
254
- String error = extractQueryParam(exchange.getRequestURI().getQuery(), "error");
255
-
256
- if (error != null || code == null) {
257
- serveError(exchange, "Google authentication was cancelled or failed.");
258
- return;
259
- }
260
-
261
- String[] result = OAuthHandler.handleGoogleCallback(code);
262
- if (result == null) {
263
- serveError(exchange, "Failed to verify Google credentials. Please try again.");
264
- return;
265
- }
266
-
267
- persistLogin(result[2], result[1], result[0]);
268
- serveSuccess(exchange, result[0]);
269
- System.out.println(Colors.c(Colors.GREEN, "✓") + " Logged in via Google as "
270
- + Colors.c(Colors.YELLOW, result[0]) + " (" + result[1] + ")");
271
- loginLatch.countDown();
272
- server.stop(0);
273
- }
274
- });
275
-
276
- // GitHub OAuth callback — exchange code for user info
277
- server.createContext("/callback/github", new HttpHandler() {
278
- @Override
279
- public void handle(HttpExchange exchange) throws IOException {
280
- String state = extractQueryParam(exchange.getRequestURI().getQuery(), "state");
281
- if (state == null || !state.equals(sessionToken)) {
282
- serveError(exchange, "Invalid session. Please restart login.");
283
- exchange.close();
284
- return;
285
- }
286
-
287
- String code = extractQueryParam(exchange.getRequestURI().getQuery(), "code");
288
- String error = extractQueryParam(exchange.getRequestURI().getQuery(), "error");
289
-
290
- if (error != null || code == null) {
291
- serveError(exchange, "GitHub authentication was cancelled or failed.");
292
- return;
293
- }
294
-
295
- String[] result = OAuthHandler.handleGitHubCallback(code);
296
- if (result == null) {
297
- serveError(exchange, "Failed to verify GitHub credentials. Please try again.");
298
- return;
299
- }
300
-
301
- persistLogin(result[2], result[1], result[0]);
302
- serveSuccess(exchange, result[0]);
303
- System.out.println(Colors.c(Colors.GREEN, "✓") + " Logged in via GitHub as "
304
- + Colors.c(Colors.YELLOW, result[0]) + " (" + result[1] + ")");
305
- loginLatch.countDown();
306
- server.stop(0);
307
- }
308
- });
309
-
310
- // Guest login callback
311
- server.createContext("/callback", new HttpHandler() {
312
- @Override
313
- public void handle(HttpExchange exchange) throws IOException {
314
- persistLogin(UUID.randomUUID().toString(), "guest@local", "Guest");
315
- serveSuccess(exchange, "Guest");
316
- System.out.println(Colors.c(Colors.GREEN, "✓") + " Logged in as " + Colors.c(Colors.YELLOW, "Guest"));
317
- loginLatch.countDown();
318
- server.stop(0);
319
- }
320
- });
321
-
322
- server.setExecutor(null);
323
- server.start();
324
-
325
- boolean completed = loginLatch.await(120, TimeUnit.SECONDS);
326
-
327
- if (!completed && !isLoggedIn()) {
328
- System.out.println(Colors.c(Colors.RED, "Login timeout. Please try again."));
329
- server.stop(0);
330
- }
331
-
332
- } catch (Exception e) {
333
-
334
- System.out.println(Colors.c(Colors.RED, "Login failed: " + e.getMessage()));
335
-
336
- }
337
-
338
- }
339
-
340
-
341
-
342
- /** Clears local auth files and logs the user out. */
343
-
344
- public static void logout() {
345
-
346
- new File(TOKEN_FILE).delete();
347
-
348
- new File(EMAIL_FILE).delete();
349
-
350
- new File(NAME_FILE).delete();
351
-
352
- System.out.println(Colors.c(Colors.GREEN, "✓") + " Logged out successfully");
353
-
354
- }
355
-
356
-
357
-
358
- /** Prints formatted account details when logged in. */
359
- public static void whoami() {
360
- if (!isLoggedIn()) {
361
- System.out.println(Colors.c(Colors.RED, "Not logged in."));
362
- return;
363
- }
364
- System.out.println(Colors.c(Colors.GRAY, "==== Account Details ===="));
365
- System.out.println("Email : " + Colors.c(Colors.YELLOW, getUserEmail()));
366
- System.out.println("Device ID: " + Colors.c(Colors.CYAN, getDeviceId()));
367
- System.out.println("Status : " + Colors.c(Colors.GREEN, "● Online"));
368
- System.out.println(Colors.c(Colors.GRAY, "========================="));
369
- }
370
-
371
-
372
-
373
- /** Writes file content to the given path and reports failures. */
374
-
375
- private static void writeFile(String path, String content) {
376
-
377
- try (FileWriter writer = new FileWriter(path)) {
378
-
379
- writer.write(content == null ? "" : content);
380
-
381
- } catch (IOException e) {
382
-
383
- System.out.println(Colors.c(Colors.RED, "Failed to write file: " + path));
384
-
385
- }
386
-
387
- }
388
-
389
-
390
-
391
- /** Reads and trims file content or returns null on failure. */
392
- private static String readFile(String path) {
393
- try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
394
- StringBuilder sb = new StringBuilder();
395
- String line;
396
- while ((line = reader.readLine()) != null) {
397
- sb.append(line);
398
- }
399
- return sb.toString().trim();
400
- } catch (IOException e) {
401
- return null;
402
- }
403
- }
404
-
405
- /** Extracts a query parameter value by key from a URL query string. */
406
- private static String extractQueryParam(String query, String key) {
407
- if (query == null || query.isEmpty()) return null;
408
- for (String part : query.split("&")) {
409
- String[] kv = part.split("=", 2);
410
- if (kv.length == 2 && key.equals(kv[0])) {
411
- try {
412
- return URLDecoder.decode(kv[1], "UTF-8");
413
- } catch (Exception e) {
414
- return kv[1];
415
- }
416
- }
417
- }
418
- return null;
419
- }
420
-
421
- /** Persists login credentials to local auth files. */
422
- private static void persistLogin(String token, String email, String name) {
423
- new File(TOKEN_DIR).mkdirs();
424
- writeFile(TOKEN_FILE, token);
425
- writeFile(EMAIL_FILE, email);
426
- writeFile(NAME_FILE, name);
427
- }
428
-
429
- /** Serves the success HTML page after authentication. */
430
- private static void serveSuccess(HttpExchange exchange, String name) throws IOException {
431
- File successPage = new File(System.getProperty("user.dir") + "/frontend/success.html");
432
- String response;
433
- if (successPage.exists()) {
434
- response = readFile(successPage.getAbsolutePath());
435
- } else {
436
- response = "<html><head><style>*{margin:0;padding:0;box-sizing:border-box}"
437
- + "body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#000;"
438
- + "color:#fff;height:100vh;display:flex;align-items:center;justify-content:center;"
439
- + "text-align:center}h1{font-size:28px;font-weight:600;margin-bottom:12px}"
440
- + "p{color:#86868b;font-size:17px}</style></head><body><div>"
441
- + "<h1>You're all set.</h1>"
442
- + "<p>Welcome, " + name + ". You can close this tab.</p>"
443
- + "</div></body></html>";
444
- }
445
- exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
446
- exchange.sendResponseHeaders(200, response.getBytes("UTF-8").length);
447
- OutputStream os = exchange.getResponseBody();
448
- os.write(response.getBytes("UTF-8"));
449
- os.close();
450
- }
451
-
452
- /** Serves a styled error page when authentication fails. */
453
- private static void serveError(HttpExchange exchange, String message) throws IOException {
454
- String response = "<html><head><style>*{margin:0;padding:0;box-sizing:border-box}"
455
- + "body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#000;"
456
- + "color:#fff;height:100vh;display:flex;align-items:center;justify-content:center;"
457
- + "text-align:center}h1{font-size:24px;font-weight:600;margin-bottom:12px}"
458
- + "p{color:#86868b;font-size:17px;max-width:360px}"
459
- + "a{color:#fff;margin-top:24px;display:inline-block;font-size:15px}</style></head>"
460
- + "<body><div><h1>Authentication Failed</h1>"
461
- + "<p>" + message + "</p>"
462
- + "<a href='/login'>Try again</a>"
463
- + "</div></body></html>";
464
- exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
465
- exchange.sendResponseHeaders(200, response.getBytes("UTF-8").length);
466
- OutputStream os = exchange.getResponseBody();
467
- os.write(response.getBytes("UTF-8"));
468
- os.close();
469
- }
470
- }
471
-
1
+ package auth;
2
+
3
+ import java.io.BufferedReader;
4
+ import java.io.File;
5
+ import java.io.FileReader;
6
+ import java.io.FileWriter;
7
+ import java.io.IOException;
8
+ import java.io.InputStream;
9
+ import java.net.HttpURLConnection;
10
+ import java.net.URI;
11
+ import java.net.URL;
12
+ import java.util.UUID;
13
+
14
+ import utils.Colors;
15
+
16
+ /** Manages VaultFS login state, device identity, and account display.
17
+ * Authentication is delegated to a remote auth server; the CLI polls for results. */
18
+ public class AuthManager {
19
+
20
+ private static final String TOKEN_DIR = System.getProperty("user.home") + "/.authfs";
21
+ private static final String TOKEN_FILE = TOKEN_DIR + "/token";
22
+ private static final String EMAIL_FILE = TOKEN_DIR + "/email";
23
+ private static final String NAME_FILE = TOKEN_DIR + "/name";
24
+ private static final String CONFIG_FILE = TOKEN_DIR + "/config";
25
+
26
+ /** Returns whether a non-empty token file exists. */
27
+ public static boolean isLoggedIn() {
28
+ File token = new File(TOKEN_FILE);
29
+ return token.exists() && token.length() > 0;
30
+ }
31
+
32
+ /** Returns the saved user email or Unknown when not available. */
33
+ public static String getUserEmail() {
34
+ String email = readFile(EMAIL_FILE);
35
+ if (email == null || email.isEmpty()) {
36
+ return "Unknown";
37
+ }
38
+ return email;
39
+ }
40
+
41
+ /** Returns the saved user name or Unknown when not available. */
42
+ public static String getUserName() {
43
+ String name = readFile(NAME_FILE);
44
+ if (name == null || name.isEmpty()) {
45
+ return "Unknown";
46
+ }
47
+ return name;
48
+ }
49
+
50
+ /** Returns the device ID from config or generates and persists a new UUID. */
51
+ public static String getDeviceId() {
52
+ String deviceId = readFile(CONFIG_FILE);
53
+ if (deviceId != null && !deviceId.isEmpty()) {
54
+ return deviceId;
55
+ }
56
+ new File(TOKEN_DIR).mkdirs();
57
+ String generated = UUID.randomUUID().toString();
58
+ writeFile(CONFIG_FILE, generated);
59
+ return generated;
60
+ }
61
+
62
+ /** Starts the remote auth-server login flow with browser + polling. */
63
+ public static void startLoginFlow() {
64
+ try {
65
+ // Step 1 — Get a session ID from auth server
66
+ String sessionId = getNewSession();
67
+ if (sessionId == null) {
68
+ System.out.println(Colors.c(Colors.YELLOW, "\u26A0\uFE0F Auth server unreachable. Continuing as Guest."));
69
+ persistLogin(UUID.randomUUID().toString(), "guest@vaultfs.local", "Guest");
70
+ return;
71
+ }
72
+
73
+ // Step 2 — Build login URL
74
+ String loginUrl = AuthConfig.AUTH_SERVER_URL + "/auth/login?sessionId=" + sessionId;
75
+
76
+ // Step 3 Print login box
77
+ System.out.println();
78
+ System.out.println(Colors.c(Colors.CYAN, " \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
79
+ System.out.println(Colors.c(Colors.CYAN, " \u2551 VaultFS \u2014 Login \u2551"));
80
+ System.out.println(Colors.c(Colors.CYAN, " \u2551 \u2551"));
81
+ System.out.println(Colors.c(Colors.CYAN, " \u2551 Opening browser for login... \u2551"));
82
+ System.out.println(Colors.c(Colors.CYAN, " \u2551 \u2551"));
83
+ System.out.println(Colors.c(Colors.CYAN, " \u2551 If browser doesn't open, visit: \u2551"));
84
+ System.out.println(Colors.c(Colors.CYAN, " \u2551 ") + Colors.c(Colors.GREEN, loginUrl));
85
+ System.out.println(Colors.c(Colors.CYAN, " \u2551 \u2551"));
86
+ System.out.println(Colors.c(Colors.CYAN, " \u2551 Press ENTER to skip and continue as Guest \u2551"));
87
+ System.out.println(Colors.c(Colors.CYAN, " \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
88
+ System.out.println();
89
+
90
+ // Step 4 — Open browser
91
+ openBrowser(loginUrl);
92
+
93
+ // Step 5 — Poll auth server for result
94
+ pollForLogin(sessionId);
95
+
96
+ } catch (Exception e) {
97
+ System.out.println(Colors.c(Colors.YELLOW, "\u26A0\uFE0F Login failed: " + e.getMessage()));
98
+ persistLogin(UUID.randomUUID().toString(), "guest@vaultfs.local", "Guest");
99
+ }
100
+ }
101
+
102
+ /** Requests a new auth session from the remote server. */
103
+ private static String getNewSession() {
104
+ try {
105
+ URL url = URI.create(AuthConfig.AUTH_SERVER_URL + "/auth/session/new").toURL();
106
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
107
+ conn.setRequestMethod("GET");
108
+ conn.setConnectTimeout(5000);
109
+ conn.setReadTimeout(5000);
110
+ String response = readStream(conn.getInputStream());
111
+ return extractJsonValue(response, "sessionId");
112
+ } catch (Exception e) {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ /** Polls the auth server for login completion, with ENTER-to-skip support. */
118
+ private static void pollForLogin(String sessionId) {
119
+ final boolean[] skipLogin = {false};
120
+ Thread inputThread = new Thread(() -> {
121
+ try {
122
+ System.in.read();
123
+ skipLogin[0] = true;
124
+ } catch (Exception ignored) {}
125
+ });
126
+ inputThread.setDaemon(true);
127
+ inputThread.start();
128
+
129
+ System.out.println(Colors.c(Colors.GRAY, "\uD83D\uDD10 Waiting for login... (press ENTER to skip)"));
130
+ System.out.println();
131
+
132
+ int waited = 0;
133
+ while (waited < AuthConfig.LOGIN_TIMEOUT_SECONDS && !skipLogin[0]) {
134
+ try {
135
+ Thread.sleep(AuthConfig.POLL_INTERVAL_MS);
136
+ waited += 2;
137
+
138
+ URL url = URI.create(AuthConfig.AUTH_SERVER_URL + "/auth/poll?sessionId=" + sessionId).toURL();
139
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
140
+ conn.setConnectTimeout(5000);
141
+ conn.setReadTimeout(5000);
142
+ String response = readStream(conn.getInputStream());
143
+
144
+ if (response.contains("\"status\":\"done\"")) {
145
+ String name = extractJsonValue(response, "name");
146
+ String email = extractJsonValue(response, "email");
147
+ String provider = extractJsonValue(response, "provider");
148
+ System.out.println(Colors.c(Colors.GREEN, "\u2713") + " Logged in as "
149
+ + Colors.c(Colors.YELLOW, name) + " via " + provider);
150
+ persistLogin(UUID.randomUUID().toString(), email, name);
151
+ return;
152
+ }
153
+
154
+ if (response.contains("\"status\":\"expired\"")) {
155
+ break;
156
+ }
157
+
158
+ if (waited % 10 == 0) {
159
+ System.out.println(Colors.c(Colors.GRAY, " Still waiting... ("
160
+ + (AuthConfig.LOGIN_TIMEOUT_SECONDS - waited) + "s remaining, press ENTER to skip)"));
161
+ }
162
+
163
+ } catch (Exception ignored) {}
164
+ }
165
+
166
+ System.out.println(Colors.c(Colors.YELLOW, "\u26A0\uFE0F Login skipped. Continuing as Guest."));
167
+ persistLogin(UUID.randomUUID().toString(), "guest@vaultfs.local", "Guest");
168
+ }
169
+
170
+ /** Opens a URL in the default browser using platform-specific commands. */
171
+ private static void openBrowser(String url) {
172
+ String os = System.getProperty("os.name").toLowerCase();
173
+ Runtime rt = Runtime.getRuntime();
174
+ try {
175
+ if (os.contains("win")) {
176
+ rt.exec(new String[]{"rundll32", "url.dll,FileProtocolHandler", url});
177
+ } else if (os.contains("mac")) {
178
+ rt.exec(new String[]{"open", url});
179
+ } else {
180
+ String[] browsers = {"xdg-open", "firefox", "google-chrome", "chromium-browser"};
181
+ boolean opened = false;
182
+ for (String browser : browsers) {
183
+ try {
184
+ rt.exec(new String[]{browser, url});
185
+ opened = true;
186
+ break;
187
+ } catch (Exception ignored) {}
188
+ }
189
+ if (!opened) {
190
+ System.out.println("Please open this URL manually: " + url);
191
+ }
192
+ }
193
+ } catch (Exception e) {
194
+ System.out.println("Could not open browser automatically.");
195
+ System.out.println("Please open this URL manually: " + url);
196
+ }
197
+ }
198
+
199
+ /** Clears local auth files and logs the user out. */
200
+ public static void logout() {
201
+ new File(TOKEN_FILE).delete();
202
+ new File(EMAIL_FILE).delete();
203
+ new File(NAME_FILE).delete();
204
+ System.out.println(Colors.c(Colors.GREEN, "\u2713") + " Logged out successfully");
205
+ }
206
+
207
+ /** Prints formatted account details when logged in. */
208
+ public static void whoami() {
209
+ if (!isLoggedIn()) {
210
+ System.out.println(Colors.c(Colors.RED, "Not logged in."));
211
+ return;
212
+ }
213
+ System.out.println(Colors.c(Colors.GRAY, "==== Account Details ===="));
214
+ System.out.println("Email : " + Colors.c(Colors.YELLOW, getUserEmail()));
215
+ System.out.println("Device ID: " + Colors.c(Colors.CYAN, getDeviceId()));
216
+ System.out.println("Status : " + Colors.c(Colors.GREEN, "\u25CF Online"));
217
+ System.out.println(Colors.c(Colors.GRAY, "========================="));
218
+ }
219
+
220
+ /** Writes file content to the given path and reports failures. */
221
+ private static void writeFile(String path, String content) {
222
+ try (FileWriter writer = new FileWriter(path)) {
223
+ writer.write(content == null ? "" : content);
224
+ } catch (IOException e) {
225
+ System.out.println(Colors.c(Colors.RED, "Failed to write file: " + path));
226
+ }
227
+ }
228
+
229
+ /** Reads and trims file content or returns null on failure. */
230
+ private static String readFile(String path) {
231
+ try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
232
+ StringBuilder sb = new StringBuilder();
233
+ String line;
234
+ while ((line = reader.readLine()) != null) {
235
+ sb.append(line);
236
+ }
237
+ return sb.toString().trim();
238
+ } catch (IOException e) {
239
+ return null;
240
+ }
241
+ }
242
+
243
+ /** Reads all bytes from an InputStream and returns as a String. */
244
+ private static String readStream(InputStream is) throws IOException {
245
+ return new String(is.readAllBytes());
246
+ }
247
+
248
+ /** Extracts a simple JSON string value by key. */
249
+ private static String extractJsonValue(String json, String key) {
250
+ try {
251
+ String search = "\"" + key + "\":\"";
252
+ int start = json.indexOf(search) + search.length();
253
+ int end = json.indexOf("\"", start);
254
+ return json.substring(start, end);
255
+ } catch (Exception e) {
256
+ return "unknown";
257
+ }
258
+ }
259
+
260
+ /** Persists login credentials to local auth files. */
261
+ private static void persistLogin(String token, String email, String name) {
262
+ new File(TOKEN_DIR).mkdirs();
263
+ writeFile(TOKEN_FILE, token);
264
+ writeFile(EMAIL_FILE, email);
265
+ writeFile(NAME_FILE, name);
266
+ }
267
+ }