vaultfs 1.0.2 → 1.0.4

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,350 +1,170 @@
1
1
  package auth;
2
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
3
  import java.io.BufferedReader;
12
4
  import java.io.File;
13
5
  import java.io.FileReader;
14
6
  import java.io.FileWriter;
15
7
  import java.io.IOException;
16
- import java.io.OutputStream;
17
- import java.net.InetSocketAddress;
18
- import java.net.URLDecoder;
19
- import java.nio.file.Files;
8
+ import java.io.InputStream;
9
+ import java.net.HttpURLConnection;
10
+ import java.net.URI;
11
+ import java.net.URL;
20
12
  import java.util.UUID;
21
- import java.util.concurrent.CountDownLatch;
22
- import java.util.concurrent.TimeUnit;
23
13
 
24
14
  import utils.Colors;
25
15
 
26
-
27
-
28
- /** Manages local AuthFS login state, device identity, and account display. */
29
-
16
+ /** Manages VaultFS login state, device identity, and account display.
17
+ * Authentication is delegated to a remote auth server; the CLI polls for results. */
30
18
  public class AuthManager {
31
19
 
32
20
  private static final String TOKEN_DIR = System.getProperty("user.home") + "/.authfs";
33
-
34
21
  private static final String TOKEN_FILE = TOKEN_DIR + "/token";
35
-
36
22
  private static final String EMAIL_FILE = TOKEN_DIR + "/email";
37
-
38
23
  private static final String NAME_FILE = TOKEN_DIR + "/name";
39
-
40
24
  private static final String CONFIG_FILE = TOKEN_DIR + "/config";
41
25
 
42
-
43
-
44
26
  /** Returns whether a non-empty token file exists. */
45
-
46
27
  public static boolean isLoggedIn() {
47
-
48
28
  File token = new File(TOKEN_FILE);
49
-
50
29
  return token.exists() && token.length() > 0;
51
-
52
30
  }
53
31
 
54
-
55
-
56
32
  /** Returns the saved user email or Unknown when not available. */
57
-
58
33
  public static String getUserEmail() {
59
-
60
34
  String email = readFile(EMAIL_FILE);
61
-
62
35
  if (email == null || email.isEmpty()) {
63
-
64
36
  return "Unknown";
65
-
66
37
  }
67
-
68
38
  return email;
69
-
70
39
  }
71
40
 
72
-
73
-
74
41
  /** Returns the saved user name or Unknown when not available. */
75
-
76
42
  public static String getUserName() {
77
-
78
43
  String name = readFile(NAME_FILE);
79
-
80
44
  if (name == null || name.isEmpty()) {
81
-
82
45
  return "Unknown";
83
-
84
46
  }
85
-
86
47
  return name;
87
-
88
48
  }
89
49
 
90
-
91
-
92
50
  /** Returns the device ID from config or generates and persists a new UUID. */
93
-
94
51
  public static String getDeviceId() {
95
-
96
52
  String deviceId = readFile(CONFIG_FILE);
97
-
98
53
  if (deviceId != null && !deviceId.isEmpty()) {
99
-
100
54
  return deviceId;
101
-
102
55
  }
103
-
104
-
105
-
106
56
  new File(TOKEN_DIR).mkdirs();
107
-
108
57
  String generated = UUID.randomUUID().toString();
109
-
110
58
  writeFile(CONFIG_FILE, generated);
111
-
112
59
  return generated;
113
-
114
60
  }
115
61
 
116
-
117
-
118
- /** Starts browser-based login, waits for callback, and stores token and email. */
119
-
62
+ /** Starts the remote auth-server login flow with browser + polling. */
120
63
  public static void startLoginFlow() {
121
-
122
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
+ }
123
72
 
124
- final String sessionToken = UUID.randomUUID().toString();
73
+ // Step 2 Build login URL
74
+ String loginUrl = AuthConfig.AUTH_SERVER_URL + "/auth/login?sessionId=" + sessionId;
125
75
 
126
- String authURL = "http://localhost:9000/login?session=" + sessionToken;
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();
127
89
 
90
+ // Step 4 — Open browser
91
+ openBrowser(loginUrl);
128
92
 
93
+ // Step 5 — Poll auth server for result
94
+ pollForLogin(sessionId);
129
95
 
130
- final CountDownLatch loginLatch = new CountDownLatch(1);
131
- final HttpServer server = HttpServer.create(new InetSocketAddress(9000), 0);
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
+ }
132
101
 
133
-
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
+ }
134
116
 
135
- // Serve the React build index.html at /login
136
- server.createContext("/login", new HttpHandler() {
137
- @Override
138
- public void handle(HttpExchange exchange) throws IOException {
139
- File file = new File(System.getProperty("user.dir") + "/frontend/dist/index.html");
140
- String response = "";
141
- if (file.exists()) {
142
- response = readFile(file.getAbsolutePath());
143
- } else {
144
- response = "<html><body><h1>Error: frontend build not found. Run npm run build in frontend/</h1></body></html>";
145
- }
146
- exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
147
- exchange.sendResponseHeaders(200, response.getBytes("UTF-8").length);
148
- OutputStream os = exchange.getResponseBody();
149
- os.write(response.getBytes("UTF-8"));
150
- os.close();
151
- }
152
- });
153
-
154
- // Serve static assets (JS, CSS) from frontend/dist/assets/
155
- server.createContext("/assets", new HttpHandler() {
156
- @Override
157
- public void handle(HttpExchange exchange) throws IOException {
158
- String path = exchange.getRequestURI().getPath();
159
- File file = new File(System.getProperty("user.dir") + "/frontend/dist" + path);
160
- if (file.exists() && file.isFile()) {
161
- String contentType = "application/octet-stream";
162
- if (path.endsWith(".js")) contentType = "application/javascript; charset=UTF-8";
163
- else if (path.endsWith(".css")) contentType = "text/css; charset=UTF-8";
164
- else if (path.endsWith(".svg")) contentType = "image/svg+xml";
165
- byte[] bytes = Files.readAllBytes(file.toPath());
166
- exchange.getResponseHeaders().set("Content-Type", contentType);
167
- exchange.sendResponseHeaders(200, bytes.length);
168
- OutputStream os = exchange.getResponseBody();
169
- os.write(bytes);
170
- os.close();
171
- } else {
172
- String msg = "Not found";
173
- exchange.sendResponseHeaders(404, msg.length());
174
- OutputStream os = exchange.getResponseBody();
175
- os.write(msg.getBytes());
176
- os.close();
177
- }
178
- }
179
- });
180
-
181
- // Redirect to Google OAuth
182
- server.createContext("/auth/google", new HttpHandler() {
183
- @Override
184
- public void handle(HttpExchange exchange) throws IOException {
185
- if (!OAuthConfig.isGoogleConfigured()) {
186
- 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>";
187
- exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
188
- exchange.sendResponseHeaders(200, msg.getBytes("UTF-8").length);
189
- OutputStream os = exchange.getResponseBody();
190
- os.write(msg.getBytes("UTF-8"));
191
- os.close();
192
- return;
193
- }
194
- String url = OAuthHandler.getGoogleAuthUrl(sessionToken);
195
- exchange.getResponseHeaders().set("Location", url);
196
- exchange.sendResponseHeaders(302, -1);
197
- exchange.close();
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;
198
152
  }
199
- });
200
-
201
- // Redirect to GitHub OAuth
202
- server.createContext("/auth/github", new HttpHandler() {
203
- @Override
204
- public void handle(HttpExchange exchange) throws IOException {
205
- if (!OAuthConfig.isGitHubConfigured()) {
206
- 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>";
207
- exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
208
- exchange.sendResponseHeaders(200, msg.getBytes("UTF-8").length);
209
- OutputStream os = exchange.getResponseBody();
210
- os.write(msg.getBytes("UTF-8"));
211
- os.close();
212
- return;
213
- }
214
- String url = OAuthHandler.getGitHubAuthUrl(sessionToken);
215
- exchange.getResponseHeaders().set("Location", url);
216
- exchange.sendResponseHeaders(302, -1);
217
- exchange.close();
218
- }
219
- });
220
-
221
- // Google OAuth callback — exchange code for user info
222
- server.createContext("/callback/google", new HttpHandler() {
223
- @Override
224
- public void handle(HttpExchange exchange) throws IOException {
225
- String state = extractQueryParam(exchange.getRequestURI().getQuery(), "state");
226
- if (state == null || !state.equals(sessionToken)) {
227
- serveError(exchange, "Invalid session. Please restart login.");
228
- exchange.close();
229
- return;
230
- }
231
-
232
- String code = extractQueryParam(exchange.getRequestURI().getQuery(), "code");
233
- String error = extractQueryParam(exchange.getRequestURI().getQuery(), "error");
234
-
235
- if (error != null || code == null) {
236
- serveError(exchange, "Google authentication was cancelled or failed.");
237
- return;
238
- }
239
-
240
- String[] result = OAuthHandler.handleGoogleCallback(code);
241
- if (result == null) {
242
- serveError(exchange, "Failed to verify Google credentials. Please try again.");
243
- return;
244
- }
245
-
246
- persistLogin(result[2], result[1], result[0]);
247
- serveSuccess(exchange, result[0]);
248
- System.out.println(Colors.c(Colors.GREEN, "✓") + " Logged in via Google as "
249
- + Colors.c(Colors.YELLOW, result[0]) + " (" + result[1] + ")");
250
- loginLatch.countDown();
251
- server.stop(0);
252
- }
253
- });
254
-
255
- // GitHub OAuth callback — exchange code for user info
256
- server.createContext("/callback/github", new HttpHandler() {
257
- @Override
258
- public void handle(HttpExchange exchange) throws IOException {
259
- String state = extractQueryParam(exchange.getRequestURI().getQuery(), "state");
260
- if (state == null || !state.equals(sessionToken)) {
261
- serveError(exchange, "Invalid session. Please restart login.");
262
- exchange.close();
263
- return;
264
- }
265
-
266
- String code = extractQueryParam(exchange.getRequestURI().getQuery(), "code");
267
- String error = extractQueryParam(exchange.getRequestURI().getQuery(), "error");
268
-
269
- if (error != null || code == null) {
270
- serveError(exchange, "GitHub authentication was cancelled or failed.");
271
- return;
272
- }
273
-
274
- String[] result = OAuthHandler.handleGitHubCallback(code);
275
- if (result == null) {
276
- serveError(exchange, "Failed to verify GitHub credentials. Please try again.");
277
- return;
278
- }
279
-
280
- persistLogin(result[2], result[1], result[0]);
281
- serveSuccess(exchange, result[0]);
282
- System.out.println(Colors.c(Colors.GREEN, "✓") + " Logged in via GitHub as "
283
- + Colors.c(Colors.YELLOW, result[0]) + " (" + result[1] + ")");
284
- loginLatch.countDown();
285
- server.stop(0);
286
- }
287
- });
288
-
289
- // Guest login callback
290
- server.createContext("/callback", new HttpHandler() {
291
- @Override
292
- public void handle(HttpExchange exchange) throws IOException {
293
- persistLogin(UUID.randomUUID().toString(), "guest@local", "Guest");
294
- serveSuccess(exchange, "Guest");
295
- System.out.println(Colors.c(Colors.GREEN, "✓") + " Logged in as " + Colors.c(Colors.YELLOW, "Guest"));
296
- loginLatch.countDown();
297
- server.stop(0);
298
- }
299
- });
300
-
301
- server.setExecutor(null);
302
- server.start();
303
-
304
- // Print styled login URL box
305
- System.out.println();
306
- System.out.println(Colors.c(Colors.CYAN, " ╔══════════════════════════════════════════╗"));
307
- System.out.println(Colors.c(Colors.CYAN, " ║ VaultFS — Login Required ║"));
308
- System.out.println(Colors.c(Colors.CYAN, " ║ ║"));
309
- System.out.println(Colors.c(Colors.CYAN, " ║ Opening browser at: ║"));
310
- System.out.println(Colors.c(Colors.CYAN, " ║ http://localhost:9000 ║"));
311
- System.out.println(Colors.c(Colors.CYAN, " ║ ║"));
312
- System.out.println(Colors.c(Colors.CYAN, " ║ If browser doesn't open, visit the ║"));
313
- System.out.println(Colors.c(Colors.CYAN, " ║ URL above manually. ║"));
314
- System.out.println(Colors.c(Colors.CYAN, " ╚══════════════════════════════════════════╝"));
315
- System.out.println();
316
-
317
- // Open browser (cross-platform)
318
- openBrowser(authURL);
319
-
320
- // Wait up to 120 seconds with periodic progress
321
- System.out.println(Colors.c(Colors.GRAY, "\uD83D\uDD10 Waiting for login... (120s timeout)"));
322
- System.out.println(Colors.c(Colors.GRAY, "\uD83D\uDC49 If browser didn't open, visit: http://localhost:9000"));
323
- System.out.println();
324
153
 
325
- boolean completed = false;
326
- for (int waited = 0; waited < 120; waited++) {
327
- if (loginLatch.await(1, TimeUnit.SECONDS)) {
328
- completed = true;
154
+ if (response.contains("\"status\":\"expired\"")) {
329
155
  break;
330
156
  }
331
- if ((waited + 1) % 10 == 0) {
332
- System.out.println(Colors.c(Colors.GRAY, " Still waiting... (" + (120 - waited - 1) + "s remaining)"));
333
- }
334
- }
335
-
336
- if (!completed && !isLoggedIn()) {
337
- System.out.println(Colors.c(Colors.YELLOW, "\u26A0\uFE0F Login timed out. Continuing as Guest."));
338
- persistLogin(UUID.randomUUID().toString(), "guest@local", "Guest");
339
- server.stop(0);
340
- }
341
157
 
342
- } catch (Exception e) {
343
-
344
- System.out.println(Colors.c(Colors.RED, "Login failed: " + e.getMessage()));
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
+ }
345
162
 
163
+ } catch (Exception ignored) {}
346
164
  }
347
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");
348
168
  }
349
169
 
350
170
  /** Opens a URL in the default browser using platform-specific commands. */
@@ -357,7 +177,6 @@ public class AuthManager {
357
177
  } else if (os.contains("mac")) {
358
178
  rt.exec(new String[]{"open", url});
359
179
  } else {
360
- // Linux/WSL
361
180
  String[] browsers = {"xdg-open", "firefox", "google-chrome", "chromium-browser"};
362
181
  boolean opened = false;
363
182
  for (String browser : browsers) {
@@ -377,24 +196,14 @@ public class AuthManager {
377
196
  }
378
197
  }
379
198
 
380
-
381
-
382
199
  /** Clears local auth files and logs the user out. */
383
-
384
200
  public static void logout() {
385
-
386
201
  new File(TOKEN_FILE).delete();
387
-
388
202
  new File(EMAIL_FILE).delete();
389
-
390
203
  new File(NAME_FILE).delete();
391
-
392
- System.out.println(Colors.c(Colors.GREEN, "✓") + " Logged out successfully");
393
-
204
+ System.out.println(Colors.c(Colors.GREEN, "\u2713") + " Logged out successfully");
394
205
  }
395
206
 
396
-
397
-
398
207
  /** Prints formatted account details when logged in. */
399
208
  public static void whoami() {
400
209
  if (!isLoggedIn()) {
@@ -404,30 +213,19 @@ public class AuthManager {
404
213
  System.out.println(Colors.c(Colors.GRAY, "==== Account Details ===="));
405
214
  System.out.println("Email : " + Colors.c(Colors.YELLOW, getUserEmail()));
406
215
  System.out.println("Device ID: " + Colors.c(Colors.CYAN, getDeviceId()));
407
- System.out.println("Status : " + Colors.c(Colors.GREEN, " Online"));
216
+ System.out.println("Status : " + Colors.c(Colors.GREEN, "\u25CF Online"));
408
217
  System.out.println(Colors.c(Colors.GRAY, "========================="));
409
218
  }
410
219
 
411
-
412
-
413
220
  /** Writes file content to the given path and reports failures. */
414
-
415
221
  private static void writeFile(String path, String content) {
416
-
417
222
  try (FileWriter writer = new FileWriter(path)) {
418
-
419
223
  writer.write(content == null ? "" : content);
420
-
421
224
  } catch (IOException e) {
422
-
423
225
  System.out.println(Colors.c(Colors.RED, "Failed to write file: " + path));
424
-
425
226
  }
426
-
427
227
  }
428
228
 
429
-
430
-
431
229
  /** Reads and trims file content or returns null on failure. */
432
230
  private static String readFile(String path) {
433
231
  try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
@@ -442,20 +240,21 @@ public class AuthManager {
442
240
  }
443
241
  }
444
242
 
445
- /** Extracts a query parameter value by key from a URL query string. */
446
- private static String extractQueryParam(String query, String key) {
447
- if (query == null || query.isEmpty()) return null;
448
- for (String part : query.split("&")) {
449
- String[] kv = part.split("=", 2);
450
- if (kv.length == 2 && key.equals(kv[0])) {
451
- try {
452
- return URLDecoder.decode(kv[1], "UTF-8");
453
- } catch (Exception e) {
454
- return kv[1];
455
- }
456
- }
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";
457
257
  }
458
- return null;
459
258
  }
460
259
 
461
260
  /** Persists login credentials to local auth files. */
@@ -465,47 +264,4 @@ public class AuthManager {
465
264
  writeFile(EMAIL_FILE, email);
466
265
  writeFile(NAME_FILE, name);
467
266
  }
468
-
469
- /** Serves the success HTML page after authentication. */
470
- private static void serveSuccess(HttpExchange exchange, String name) throws IOException {
471
- File successPage = new File(System.getProperty("user.dir") + "/frontend/success.html");
472
- String response;
473
- if (successPage.exists()) {
474
- response = readFile(successPage.getAbsolutePath());
475
- } else {
476
- response = "<html><head><style>*{margin:0;padding:0;box-sizing:border-box}"
477
- + "body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#000;"
478
- + "color:#fff;height:100vh;display:flex;align-items:center;justify-content:center;"
479
- + "text-align:center}h1{font-size:28px;font-weight:600;margin-bottom:12px}"
480
- + "p{color:#86868b;font-size:17px}</style></head><body><div>"
481
- + "<h1>You're all set.</h1>"
482
- + "<p>Welcome, " + name + ". You can close this tab.</p>"
483
- + "</div></body></html>";
484
- }
485
- exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
486
- exchange.sendResponseHeaders(200, response.getBytes("UTF-8").length);
487
- OutputStream os = exchange.getResponseBody();
488
- os.write(response.getBytes("UTF-8"));
489
- os.close();
490
- }
491
-
492
- /** Serves a styled error page when authentication fails. */
493
- private static void serveError(HttpExchange exchange, String message) throws IOException {
494
- String response = "<html><head><style>*{margin:0;padding:0;box-sizing:border-box}"
495
- + "body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#000;"
496
- + "color:#fff;height:100vh;display:flex;align-items:center;justify-content:center;"
497
- + "text-align:center}h1{font-size:24px;font-weight:600;margin-bottom:12px}"
498
- + "p{color:#86868b;font-size:17px;max-width:360px}"
499
- + "a{color:#fff;margin-top:24px;display:inline-block;font-size:15px}</style></head>"
500
- + "<body><div><h1>Authentication Failed</h1>"
501
- + "<p>" + message + "</p>"
502
- + "<a href='/login'>Try again</a>"
503
- + "</div></body></html>";
504
- exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
505
- exchange.sendResponseHeaders(200, response.getBytes("UTF-8").length);
506
- OutputStream os = exchange.getResponseBody();
507
- os.write(response.getBytes("UTF-8"));
508
- os.close();
509
- }
510
267
  }
511
-
@@ -39,6 +39,9 @@ public class DiskService {
39
39
 
40
40
  if (existingStartBlockId >= 0) {
41
41
  metadata.startBlockId = existingStartBlockId;
42
+ } else if (existingStartBlockId == -2) {
43
+ // Already tracked but disk was full — don't retry allocation
44
+ metadata.startBlockId = -1;
42
45
  } else if (metadata.startBlockId == -1) {
43
46
  metadata.startBlockId = diskSimulator.allocateFile(metadata.sizeBytes);
44
47
  }
@@ -762,10 +762,13 @@ public class FileSystem {
762
762
  } else if (entry.isFile()) {
763
763
  String filePath = diskService.normalizePath(entry.getAbsolutePath());
764
764
  Integer existingBlockId = fileBlockIndex.get(filePath);
765
+ // If file is already tracked (even with -1 = disk full), skip re-allocation
766
+ int blockHint = existingBlockId != null ? existingBlockId : -1;
767
+ boolean alreadyTracked = fileBlockIndex.containsKey(filePath);
765
768
  models.FileMetadata metadata = diskService.metadataFromDiskFile(
766
769
  entry,
767
770
  diskSimulator,
768
- existingBlockId != null ? existingBlockId : -1
771
+ alreadyTracked ? Math.max(blockHint, 0) == blockHint ? blockHint : -2 : -1
769
772
  );
770
773
  diskFiles.add(metadata);
771
774
  diskFilePaths.add(filePath);
@@ -858,7 +861,9 @@ public class FileSystem {
858
861
  }
859
862
 
860
863
  /** Allows plain names only so commands operate within current directory scope.
861
- * Rejects path separators, traversal patterns (..), null bytes, and empty/blank names. */
864
+ * Rejects path separators, traversal patterns (..), null bytes, and empty/blank names.
865
+ * Note: any name containing ".." is rejected, including "foo..bar", as a
866
+ * defense-in-depth measure against path traversal after separator stripping. */
862
867
  private boolean isSimpleName(String name) {
863
868
  if (name == null || name.trim().isEmpty()) {
864
869
  return false;
@@ -869,8 +874,6 @@ public class FileSystem {
869
874
  if (".".equals(name) || "..".equals(name)) {
870
875
  return false;
871
876
  }
872
- // Reject names that contain ".." as a substring (e.g. "foo..bar" is fine,
873
- // but this catches edge cases with path separators stripped earlier)
874
877
  if (name.contains("..")) {
875
878
  return false;
876
879
  }
@@ -955,7 +958,6 @@ public class FileSystem {
955
958
  if (AuthManager.isLoggedIn() && syncPending.compareAndSet(false, true)) {
956
959
  syncExecutor.submit(() -> {
957
960
  try {
958
- syncPending.set(false);
959
961
  String stateContent = new String(
960
962
  java.nio.file.Files.readAllBytes(
961
963
  java.nio.file.Paths.get(
@@ -969,8 +971,9 @@ public class FileSystem {
969
971
  stateContent
970
972
  );
971
973
  } catch (Exception e) {
972
- syncPending.set(false);
973
974
  Logger.warn("[sync] Cloud sync failed: " + e.getClass().getSimpleName());
975
+ } finally {
976
+ syncPending.set(false);
974
977
  }
975
978
  });
976
979
  }