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.
- package/README.md +139 -232
- package/bin/postinstall.js +138 -129
- package/bin/vaultfs-npm.js +148 -147
- package/install.bat +261 -0
- package/install.sh +1 -1
- package/package.json +34 -34
- package/src/auth/AuthConfig.java +9 -0
- package/src/auth/AuthManager.java +111 -355
- package/src/filesystem/DiskService.java +3 -0
- package/src/filesystem/FileSystem.java +9 -6
- package/src/sync/FirestoreSync.java +10 -2
- package/src/utils/EnvParser.java +25 -4
- package/version.txt +1 -1
|
@@ -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.
|
|
17
|
-
import java.net.
|
|
18
|
-
import java.net.
|
|
19
|
-
import java.
|
|
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
|
-
|
|
73
|
+
// Step 2 — Build login URL
|
|
74
|
+
String loginUrl = AuthConfig.AUTH_SERVER_URL + "/auth/login?sessionId=" + sessionId;
|
|
125
75
|
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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, "
|
|
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
|
-
/**
|
|
446
|
-
private static String
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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
|
}
|