@ycniuqton/devlens 0.1.15 → 0.1.16
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/dist/routes/auth.d.ts +1 -0
- package/dist/routes/auth.js +48 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/server.js +28 -0
- package/dist/server.js.map +1 -1
- package/dist/services/auth.d.ts +6 -0
- package/dist/services/auth.js +45 -0
- package/dist/services/auth.js.map +1 -0
- package/package.json +7 -1
- package/public/css/style.css +46 -0
- package/public/index.html +31 -0
- package/public/js/app.js +52 -0
- package/public/login.html +181 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const authRouter: import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authRouter = void 0;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
exports.authRouter = (0, express_1.Router)();
|
|
6
|
+
exports.authRouter.get('/status', (req, res) => {
|
|
7
|
+
res.json({ authenticated: !!req.session.authenticated });
|
|
8
|
+
});
|
|
9
|
+
exports.authRouter.post('/login', async (req, res) => {
|
|
10
|
+
const { username, password } = req.body;
|
|
11
|
+
const authService = req.app.locals.authService;
|
|
12
|
+
try {
|
|
13
|
+
const valid = await authService.validateCredentials(username, password);
|
|
14
|
+
if (valid) {
|
|
15
|
+
req.session.authenticated = true;
|
|
16
|
+
res.json({ ok: true });
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
res.status(401).json({ error: 'Invalid username or password' });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
res.status(500).json({ error: err.message });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
exports.authRouter.post('/logout', (req, res) => {
|
|
27
|
+
req.session.destroy(() => { });
|
|
28
|
+
res.json({ ok: true });
|
|
29
|
+
});
|
|
30
|
+
exports.authRouter.post('/change-password', async (req, res) => {
|
|
31
|
+
if (!req.session.authenticated)
|
|
32
|
+
return res.status(401).json({ error: 'Not authenticated' });
|
|
33
|
+
const { currentPassword, newPassword } = req.body;
|
|
34
|
+
if (!newPassword || newPassword.length < 4)
|
|
35
|
+
return res.status(400).json({ error: 'Password must be at least 4 characters' });
|
|
36
|
+
const authService = req.app.locals.authService;
|
|
37
|
+
try {
|
|
38
|
+
const valid = await authService.validateCredentials(authService.getUsername(), currentPassword);
|
|
39
|
+
if (!valid)
|
|
40
|
+
return res.status(401).json({ error: 'Current password is incorrect' });
|
|
41
|
+
await authService.changePassword(newPassword);
|
|
42
|
+
res.json({ ok: true });
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
res.status(500).json({ error: err.message });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.createServer = createServer;
|
|
|
7
7
|
const express_1 = __importDefault(require("express"));
|
|
8
8
|
const http_1 = __importDefault(require("http"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const express_session_1 = __importDefault(require("express-session"));
|
|
10
11
|
const ws_1 = require("ws");
|
|
11
12
|
const chokidar_1 = __importDefault(require("chokidar"));
|
|
12
13
|
const git_1 = require("./services/git");
|
|
@@ -14,23 +15,32 @@ const watcher_1 = require("./services/watcher");
|
|
|
14
15
|
const taskStore_1 = require("./services/taskStore");
|
|
15
16
|
const rules_1 = require("./services/rules");
|
|
16
17
|
const settings_1 = require("./services/settings");
|
|
18
|
+
const auth_1 = require("./services/auth");
|
|
17
19
|
const diff_1 = require("./routes/diff");
|
|
18
20
|
const tasks_1 = require("./routes/tasks");
|
|
19
21
|
const integrations_1 = require("./routes/integrations");
|
|
20
22
|
const rules_2 = require("./routes/rules");
|
|
21
23
|
const browser_1 = require("./routes/browser");
|
|
22
24
|
const settings_2 = require("./routes/settings");
|
|
25
|
+
const auth_2 = require("./routes/auth");
|
|
23
26
|
function createServer(options) {
|
|
24
27
|
const app = (0, express_1.default)();
|
|
25
28
|
const httpServer = http_1.default.createServer(app);
|
|
26
29
|
const wss = new ws_1.WebSocketServer({ server: httpServer, path: '/ws' });
|
|
27
30
|
// Middleware
|
|
28
31
|
app.use(express_1.default.json());
|
|
32
|
+
app.use((0, express_session_1.default)({
|
|
33
|
+
secret: require('crypto').randomBytes(32).toString('hex'),
|
|
34
|
+
resave: false,
|
|
35
|
+
saveUninitialized: false,
|
|
36
|
+
cookie: { httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000 },
|
|
37
|
+
}));
|
|
29
38
|
// Services
|
|
30
39
|
const gitService = (0, git_1.createGitService)(options.projectDir);
|
|
31
40
|
const taskStore = (0, taskStore_1.createTaskStore)(options.projectDir);
|
|
32
41
|
const rulesService = (0, rules_1.createRulesService)(options.projectDir);
|
|
33
42
|
const settingsService = (0, settings_1.createSettingsService)(options.projectDir);
|
|
43
|
+
const authService = (0, auth_1.createAuthService)(options.projectDir);
|
|
34
44
|
rulesService.ensureDefault();
|
|
35
45
|
// Always re-enable direct IP on startup so users are never permanently locked out
|
|
36
46
|
settingsService.updateSettings({ directIpAccess: true });
|
|
@@ -39,6 +49,7 @@ function createServer(options) {
|
|
|
39
49
|
app.locals.taskStore = taskStore;
|
|
40
50
|
app.locals.rulesService = rulesService;
|
|
41
51
|
app.locals.settingsService = settingsService;
|
|
52
|
+
app.locals.authService = authService;
|
|
42
53
|
app.locals.projectDir = options.projectDir;
|
|
43
54
|
app.locals.port = options.port;
|
|
44
55
|
// Block non-localhost requests when directIpAccess is disabled
|
|
@@ -50,6 +61,19 @@ function createServer(options) {
|
|
|
50
61
|
}
|
|
51
62
|
next();
|
|
52
63
|
});
|
|
64
|
+
// Auth routes (public — no auth required)
|
|
65
|
+
app.use('/api/auth', auth_2.authRouter);
|
|
66
|
+
// Auth guard — protect all other API routes and the SPA
|
|
67
|
+
app.use((req, res, next) => {
|
|
68
|
+
const isAuthenticated = !!req.session.authenticated;
|
|
69
|
+
const isPublic = req.path === '/login' || req.path.startsWith('/css/') || req.path.startsWith('/js/') || req.path.startsWith('/fonts/');
|
|
70
|
+
if (!isAuthenticated && !isPublic) {
|
|
71
|
+
if (req.path.startsWith('/api/'))
|
|
72
|
+
return res.status(401).json({ error: 'Not authenticated' });
|
|
73
|
+
return res.redirect('/login');
|
|
74
|
+
}
|
|
75
|
+
next();
|
|
76
|
+
});
|
|
53
77
|
// API routes
|
|
54
78
|
app.get('/api/info', (_req, res) => {
|
|
55
79
|
const os = require('os');
|
|
@@ -77,6 +101,10 @@ function createServer(options) {
|
|
|
77
101
|
// Static files
|
|
78
102
|
const publicDir = path_1.default.resolve(__dirname, '../public');
|
|
79
103
|
app.use(express_1.default.static(publicDir));
|
|
104
|
+
// Login page (public)
|
|
105
|
+
app.get('/login', (_req, res) => {
|
|
106
|
+
res.sendFile(path_1.default.join(publicDir, 'login.html'));
|
|
107
|
+
});
|
|
80
108
|
// SPA fallback
|
|
81
109
|
app.get('*', (_req, res) => {
|
|
82
110
|
res.sendFile(path_1.default.join(publicDir, 'index.html'));
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";;;;;AAqBA,oCAwLC;AA7MD,sDAA8B;AAC9B,gDAAwB;AACxB,gDAAwB;AACxB,sEAAsC;AACtC,2BAAgD;AAEhD,wDAAgC;AAChC,wCAAkD;AAClD,gDAAmD;AACnD,oDAAuD;AACvD,4CAAsD;AACtD,kDAA4D;AAC5D,0CAAoD;AACpD,wCAA2C;AAC3C,0CAAmE;AACnE,wDAA2D;AAC3D,0CAA6C;AAC7C,8CAAiD;AACjD,gDAAmD;AACnD,wCAA2C;AAE3C,SAAgB,YAAY,CAAC,OAAsB;IACjD,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;IACtB,MAAM,UAAU,GAAG,cAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,GAAG,GAAG,IAAI,oBAAe,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAErE,aAAa;IACb,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACxB,GAAG,CAAC,GAAG,CAAC,IAAA,yBAAO,EAAC;QACd,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QACzD,MAAM,EAAE,KAAK;QACb,iBAAiB,EAAE,KAAK;QACxB,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE;KAC5D,CAAC,CAAC,CAAC;IAEJ,WAAW;IACX,MAAM,UAAU,GAAG,IAAA,sBAAgB,EAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,MAAM,SAAS,GAAG,IAAA,2BAAe,EAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACtD,MAAM,YAAY,GAAG,IAAA,0BAAkB,EAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5D,MAAM,eAAe,GAAG,IAAA,gCAAqB,EAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAClE,MAAM,WAAW,GAAG,IAAA,wBAAiB,EAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1D,YAAY,CAAC,aAAa,EAAE,CAAC;IAC7B,kFAAkF;IAClF,eAAe,CAAC,cAAc,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC;IAEzD,wCAAwC;IACxC,GAAG,CAAC,MAAM,CAAC,UAAU,GAAG,UAAU,CAAC;IACnC,GAAG,CAAC,MAAM,CAAC,SAAS,GAAG,SAAS,CAAC;IACjC,GAAG,CAAC,MAAM,CAAC,YAAY,GAAG,YAAY,CAAC;IACvC,GAAG,CAAC,MAAM,CAAC,eAAe,GAAG,eAAe,CAAC;IAC7C,GAAG,CAAC,MAAM,CAAC,WAAW,GAAG,WAAW,CAAC;IACrC,GAAG,CAAC,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAC3C,GAAG,CAAC,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAE/B,+DAA+D;IAC/D,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,EAAE,CAAC;QAC1C,MAAM,OAAO,GAAG,EAAE,KAAK,WAAW,IAAI,EAAE,KAAK,KAAK,IAAI,EAAE,KAAK,kBAAkB,CAAC;QAChF,IAAI,CAAC,OAAO,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,CAAC,cAAc,EAAE,CAAC;YAC9D,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,mDAAmD,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,EAAE,CAAC;IACT,CAAC,CAAC,CAAC;IAEH,0CAA0C;IAC1C,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,iBAAU,CAAC,CAAC;IAEjC,wDAAwD;IACxD,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,MAAM,eAAe,GAAG,CAAC,CAAE,GAAG,CAAC,OAAe,CAAC,aAAa,CAAC;QAC7D,MAAM,QAAQ,GAAG,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAExI,IAAI,CAAC,eAAe,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClC,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;gBAAE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAC9F,OAAO,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAChC,CAAC;QACD,IAAI,EAAE,CAAC;IACT,CAAC,CAAC,CAAC;IAEH,aAAa;IACb,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACjC,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACzB,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,iBAAiB,EAAS,CAAC,EAAE,CAAC;YAClE,KAAK,MAAM,KAAK,IAAI,MAAe,EAAE,CAAC;gBACpC,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ;oBAAE,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACjF,CAAC;QACH,CAAC;QACD,GAAG,CAAC,IAAI,CAAC;YACP,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,WAAW,EAAE,cAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC;YAC9C,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,QAAQ,EAAE,oBAAoB,OAAO,CAAC,IAAI,EAAE;YAC5C,WAAW,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,UAAU,EAAE,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;SAClE,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAU,CAAC,CAAC;IAC5B,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,mBAAW,CAAC,CAAC;IACnC,GAAG,CAAC,GAAG,CAAC,mBAAmB,EAAE,iCAAkB,CAAC,CAAC;IACjD,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,mBAAW,CAAC,CAAC;IACnC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,uBAAa,CAAC,CAAC;IACvC,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,yBAAc,CAAC,CAAC;IAEzC,eAAe;IACf,MAAM,SAAS,GAAG,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IACvD,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IAEnC,sBAAsB;IACtB,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QAC9B,GAAG,CAAC,QAAQ,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,eAAe;IACf,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACzB,GAAG,CAAC,QAAQ,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,6BAA6B;IAC7B,SAAS,SAAS,CAAC,OAAkB;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACrC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE;YAC7B,IAAI,MAAM,CAAC,UAAU,KAAK,cAAS,CAAC,IAAI,EAAE,CAAC;gBACzC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,yEAAyE;IACzE,IAAI,OAAO,GAA4C,IAAI,CAAC;IAE5D,MAAM,eAAe,GAAG,KAAK,IAAI,EAAE;QACjC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,CAAC;YACxC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,CAAC;YAC5C,SAAS,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;YACtD,SAAS,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QAC5D,CAAC;QAAC,MAAM,CAAC;YACP,yCAAyC;QAC3C,CAAC;IACH,CAAC,CAAC;IAEF,SAAS,YAAY;QACnB,OAAO,GAAG,IAAA,uBAAa,EACrB,OAAO,CAAC,UAAU,EAClB,eAAe,EACf,eAAe,CAAC,sBAAsB,EAAE,CACzC,CAAC;IACJ,CAAC;IAED,YAAY,EAAE,CAAC;IAEf,SAAS,aAAa;QACpB,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAClC,CAAC;QACD,YAAY,EAAE,CAAC;IACjB,CAAC;IAED,GAAG,CAAC,MAAM,CAAC,aAAa,GAAG,aAAa,CAAC;IAEzC,sCAAsC;IACtC,MAAM,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;IACxE,MAAM,YAAY,GAAG,kBAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACxE,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QAC7B,SAAS,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,YAAY,CAAC,QAAQ,EAAE,EAAE,EAAS,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,8BAA8B;IAC9B,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IAC7D,MAAM,eAAe,GAAG,kBAAQ,CAAC,KAAK,CAAC;QACrC,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,mBAAmB,CAAC;QAC1C,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,oBAAoB,CAAC;KAC5C,EAAE,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;IAE7B,SAAS,sBAAsB;QAC7B,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACzB,MAAM,WAAW,GAAG,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,mBAAmB,CAAC,CAAC;QAC/D,MAAM,YAAY,GAAG,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,oBAAoB,CAAC,CAAC;QACjE,IAAI,OAAO,GAAkB,IAAI,CAAC;QAClC,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,UAAU,GAAkB,IAAI,CAAC;QACrC,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC;YAAE,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QACvF,IAAI,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAChC,QAAQ,GAAG,IAAI,CAAC;YAChB,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC5E,IAAI,CAAC;gBAAE,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAClC,CAAC;QACD,SAAS,CAAC,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAS,CAAC,CAAC;IACnG,CAAC;IAED,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,CAAC,CAAC;IAClD,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,CAAC,CAAC;IACrD,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,CAAC,CAAC;IAErD,0CAA0C;IAC1C,MAAM,gBAAgB,GAAG,WAAW,CAAC,GAAG,EAAE;QACxC,IAAA,4BAAoB,EAAC,SAAS,CAAC,CAAC;IAClC,CAAC,EAAE,KAAK,CAAC,CAAC;IAEV,2CAA2C;IAC3C,GAAG,CAAC,MAAM,CAAC,SAAS,GAAG,SAAS,CAAC;IACjC,GAAG,CAAC,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;IAC7B,GAAG,CAAC,MAAM,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;IAE/C,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createAuthService = createAuthService;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
|
10
|
+
const DEFAULT_PASSWORD = 'devlens';
|
|
11
|
+
const DEFAULT_USERNAME = 'admin';
|
|
12
|
+
function createAuthService(projectDir) {
|
|
13
|
+
const authFile = path_1.default.join(projectDir, '.devlens', 'auth.json');
|
|
14
|
+
function ensureAuthFile() {
|
|
15
|
+
const devlensDir = path_1.default.join(projectDir, '.devlens');
|
|
16
|
+
if (!fs_1.default.existsSync(devlensDir))
|
|
17
|
+
fs_1.default.mkdirSync(devlensDir, { recursive: true });
|
|
18
|
+
if (!fs_1.default.existsSync(authFile)) {
|
|
19
|
+
const hash = bcryptjs_1.default.hashSync(DEFAULT_PASSWORD, 10);
|
|
20
|
+
const data = { username: DEFAULT_USERNAME, passwordHash: hash };
|
|
21
|
+
fs_1.default.writeFileSync(authFile, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function load() {
|
|
25
|
+
ensureAuthFile();
|
|
26
|
+
return JSON.parse(fs_1.default.readFileSync(authFile, 'utf-8'));
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
async validateCredentials(username, password) {
|
|
30
|
+
const data = load();
|
|
31
|
+
if (username !== data.username)
|
|
32
|
+
return false;
|
|
33
|
+
return bcryptjs_1.default.compare(password, data.passwordHash);
|
|
34
|
+
},
|
|
35
|
+
async changePassword(newPassword) {
|
|
36
|
+
const data = load();
|
|
37
|
+
data.passwordHash = bcryptjs_1.default.hashSync(newPassword, 10);
|
|
38
|
+
fs_1.default.writeFileSync(authFile, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
39
|
+
},
|
|
40
|
+
getUsername() {
|
|
41
|
+
return load().username;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/services/auth.ts"],"names":[],"mappings":";;;;;AAkBA,8CAmCC;AArDD,4CAAoB;AACpB,gDAAwB;AACxB,wDAA8B;AAE9B,MAAM,gBAAgB,GAAG,SAAS,CAAC;AACnC,MAAM,gBAAgB,GAAG,OAAO,CAAC;AAajC,SAAgB,iBAAiB,CAAC,UAAkB;IAClD,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;IAEhE,SAAS,cAAc;QACrB,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QACrD,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,YAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9E,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,GAAG,kBAAM,CAAC,QAAQ,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;YACnD,MAAM,IAAI,GAAa,EAAE,QAAQ,EAAE,gBAAgB,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;YAC1E,YAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;IAED,SAAS,IAAI;QACX,cAAc,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC,KAAK,CAAC,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,OAAO;QACL,KAAK,CAAC,mBAAmB,CAAC,QAAgB,EAAE,QAAgB;YAC1D,MAAM,IAAI,GAAG,IAAI,EAAE,CAAC;YACpB,IAAI,QAAQ,KAAK,IAAI,CAAC,QAAQ;gBAAE,OAAO,KAAK,CAAC;YAC7C,OAAO,kBAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QACrD,CAAC;QAED,KAAK,CAAC,cAAc,CAAC,WAAmB;YACtC,MAAM,IAAI,GAAG,IAAI,EAAE,CAAC;YACpB,IAAI,CAAC,YAAY,GAAG,kBAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;YACrD,YAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7E,CAAC;QAED,WAAW;YACT,OAAO,IAAI,EAAE,CAAC,QAAQ,CAAC;QACzB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ycniuqton/devlens",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -30,20 +30,26 @@
|
|
|
30
30
|
"homepage": "https://github.com/ycniuqton/Devlens#readme",
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@ycniuqton/devlens": "^0.1.13",
|
|
33
|
+
"bcryptjs": "^3.0.3",
|
|
33
34
|
"better-sqlite3": "^12.8.0",
|
|
34
35
|
"chokidar": "^3.6.0",
|
|
35
36
|
"cloudflared": "^0.7.1",
|
|
36
37
|
"commander": "^12.0.0",
|
|
38
|
+
"cookie-parser": "^1.4.7",
|
|
37
39
|
"diff2html": "^3.4.47",
|
|
38
40
|
"express": "^4.18.2",
|
|
41
|
+
"express-session": "^1.19.0",
|
|
39
42
|
"ngrok": "^4.3.3",
|
|
40
43
|
"open": "^10.1.0",
|
|
41
44
|
"simple-git": "~3.33.0",
|
|
42
45
|
"ws": "^8.16.0"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
48
|
+
"@types/bcryptjs": "^2.4.6",
|
|
45
49
|
"@types/better-sqlite3": "^7.6.13",
|
|
50
|
+
"@types/cookie-parser": "^1.4.10",
|
|
46
51
|
"@types/express": "^4.17.21",
|
|
52
|
+
"@types/express-session": "^1.19.0",
|
|
47
53
|
"@types/node": "^20.11.0",
|
|
48
54
|
"@types/ws": "^8.5.10",
|
|
49
55
|
"typescript": "^5.3.3"
|
package/public/css/style.css
CHANGED
|
@@ -236,6 +236,52 @@ body {
|
|
|
236
236
|
border-top: 1px solid var(--color-border);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
.auth-footer {
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: var(--sp-1);
|
|
243
|
+
margin-top: var(--sp-2);
|
|
244
|
+
}
|
|
245
|
+
.btn-auth {
|
|
246
|
+
display: flex; align-items: center; gap: 6px;
|
|
247
|
+
background: none; border: none; cursor: pointer;
|
|
248
|
+
color: var(--color-text-muted); font-size: var(--text-xs);
|
|
249
|
+
font-family: inherit; padding: var(--sp-1) var(--sp-2);
|
|
250
|
+
border-radius: var(--radius-sm);
|
|
251
|
+
transition: color 0.15s, background 0.15s;
|
|
252
|
+
flex: 1;
|
|
253
|
+
}
|
|
254
|
+
.btn-auth:hover { color: var(--color-text); background: var(--color-surface-2); }
|
|
255
|
+
.btn-logout { flex: 0; }
|
|
256
|
+
|
|
257
|
+
.modal-overlay {
|
|
258
|
+
position: fixed; inset: 0; z-index: 1000;
|
|
259
|
+
background: rgba(0,0,0,0.6);
|
|
260
|
+
display: flex; align-items: center; justify-content: center;
|
|
261
|
+
}
|
|
262
|
+
.modal-box {
|
|
263
|
+
background: var(--color-surface);
|
|
264
|
+
border: 1px solid var(--color-border);
|
|
265
|
+
border-radius: var(--radius-md);
|
|
266
|
+
width: 100%; max-width: 380px;
|
|
267
|
+
padding: 24px;
|
|
268
|
+
}
|
|
269
|
+
.modal-header {
|
|
270
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
271
|
+
margin-bottom: 20px;
|
|
272
|
+
}
|
|
273
|
+
.modal-header h3 { font-size: var(--text-base); font-weight: 600; }
|
|
274
|
+
.modal-close {
|
|
275
|
+
background: none; border: none; cursor: pointer;
|
|
276
|
+
color: var(--color-text-muted); font-size: 16px; padding: 4px;
|
|
277
|
+
}
|
|
278
|
+
.modal-close:hover { color: var(--color-text); }
|
|
279
|
+
.modal-body label {
|
|
280
|
+
display: block; font-size: var(--text-xs);
|
|
281
|
+
color: var(--color-text-secondary); margin-bottom: 4px;
|
|
282
|
+
font-weight: 500;
|
|
283
|
+
}
|
|
284
|
+
|
|
239
285
|
.connection-status {
|
|
240
286
|
display: flex;
|
|
241
287
|
align-items: center;
|
package/public/index.html
CHANGED
|
@@ -76,9 +76,40 @@
|
|
|
76
76
|
<span class="status-indicator"></span>
|
|
77
77
|
<span class="status-label">Disconnected</span>
|
|
78
78
|
</div>
|
|
79
|
+
<div class="auth-footer">
|
|
80
|
+
<button class="btn-auth" onclick="showChangePassword()" title="Change password">
|
|
81
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M6 20v-2a6 6 0 0 1 12 0v2"/></svg>
|
|
82
|
+
<span id="auth-username">admin</span>
|
|
83
|
+
</button>
|
|
84
|
+
<button class="btn-auth btn-logout" onclick="logoutAction()" title="Sign out">
|
|
85
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
79
88
|
</div>
|
|
80
89
|
</aside>
|
|
81
90
|
|
|
91
|
+
<!-- Change Password Modal -->
|
|
92
|
+
<div id="change-password-modal" class="modal-overlay" style="display:none">
|
|
93
|
+
<div class="modal-box">
|
|
94
|
+
<div class="modal-header">
|
|
95
|
+
<h3>Change Password</h3>
|
|
96
|
+
<button class="modal-close" onclick="hideChangePassword()">✕</button>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="modal-body">
|
|
99
|
+
<label>Current Password</label>
|
|
100
|
+
<input type="password" id="cp-current" placeholder="Current password" style="width:100%;margin-bottom:12px;padding:8px 12px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);color:var(--color-text);font-size:var(--text-sm)">
|
|
101
|
+
<label>New Password</label>
|
|
102
|
+
<input type="password" id="cp-new" placeholder="New password (min 4 chars)" style="width:100%;margin-bottom:12px;padding:8px 12px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);color:var(--color-text);font-size:var(--text-sm)">
|
|
103
|
+
<label>Confirm New Password</label>
|
|
104
|
+
<input type="password" id="cp-confirm" placeholder="Confirm new password" style="width:100%;margin-bottom:16px;padding:8px 12px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);color:var(--color-text);font-size:var(--text-sm)">
|
|
105
|
+
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
106
|
+
<button class="btn btn-ghost btn-sm" onclick="hideChangePassword()">Cancel</button>
|
|
107
|
+
<button class="btn btn-primary btn-sm" onclick="submitChangePassword()">Update Password</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
82
113
|
<!-- Main Content -->
|
|
83
114
|
<main id="main-content">
|
|
84
115
|
|
package/public/js/app.js
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
+
// Auth
|
|
2
|
+
fetch('/api/auth/status').then(r => r.json()).then(data => {
|
|
3
|
+
if (!data.authenticated) location.href = '/login';
|
|
4
|
+
}).catch(() => {});
|
|
5
|
+
|
|
6
|
+
async function logoutAction() {
|
|
7
|
+
await fetch('/api/auth/logout', { method: 'POST' });
|
|
8
|
+
location.href = '/login';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function showChangePassword() {
|
|
12
|
+
document.getElementById('cp-current').value = '';
|
|
13
|
+
document.getElementById('cp-new').value = '';
|
|
14
|
+
document.getElementById('cp-confirm').value = '';
|
|
15
|
+
document.getElementById('change-password-modal').style.display = 'flex';
|
|
16
|
+
document.getElementById('cp-current').focus();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hideChangePassword() {
|
|
20
|
+
document.getElementById('change-password-modal').style.display = 'none';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function submitChangePassword() {
|
|
24
|
+
const current = document.getElementById('cp-current').value;
|
|
25
|
+
const newPw = document.getElementById('cp-new').value;
|
|
26
|
+
const confirm = document.getElementById('cp-confirm').value;
|
|
27
|
+
if (!current || !newPw) { showToast('Please fill in all fields', 'error'); return; }
|
|
28
|
+
if (newPw !== confirm) { showToast('Passwords do not match', 'error'); return; }
|
|
29
|
+
if (newPw.length < 4) { showToast('Password must be at least 4 characters', 'error'); return; }
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch('/api/auth/change-password', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ currentPassword: current, newPassword: newPw }),
|
|
35
|
+
});
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
if (data.ok) {
|
|
38
|
+
hideChangePassword();
|
|
39
|
+
showToast('Password updated', 'success');
|
|
40
|
+
} else {
|
|
41
|
+
showToast(data.error || 'Failed to update password', 'error');
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
showToast('Connection error', 'error');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Close modal on backdrop click
|
|
49
|
+
document.getElementById('change-password-modal')?.addEventListener('click', (e) => {
|
|
50
|
+
if (e.target === e.currentTarget) hideChangePassword();
|
|
51
|
+
});
|
|
52
|
+
|
|
1
53
|
// Load project info into sidebar + page title
|
|
2
54
|
fetch('/api/info').then(r => r.json()).then(info => {
|
|
3
55
|
if (info.projectName) {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Devlens — Login</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
9
|
+
<style>
|
|
10
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
11
|
+
:root {
|
|
12
|
+
--color-bg: #0d0d0f;
|
|
13
|
+
--color-surface: #16181c;
|
|
14
|
+
--color-surface-2: #1e2026;
|
|
15
|
+
--color-border: #2a2d35;
|
|
16
|
+
--color-text: #e8eaed;
|
|
17
|
+
--color-text-secondary: #9aa0ad;
|
|
18
|
+
--color-primary: #6366f1;
|
|
19
|
+
--color-primary-hover: #818cf8;
|
|
20
|
+
--color-danger: #f87171;
|
|
21
|
+
--radius-md: 8px;
|
|
22
|
+
}
|
|
23
|
+
body {
|
|
24
|
+
font-family: 'Inter', sans-serif;
|
|
25
|
+
background: var(--color-bg);
|
|
26
|
+
color: var(--color-text);
|
|
27
|
+
min-height: 100vh;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
}
|
|
32
|
+
.login-card {
|
|
33
|
+
background: var(--color-surface);
|
|
34
|
+
border: 1px solid var(--color-border);
|
|
35
|
+
border-radius: 12px;
|
|
36
|
+
padding: 40px;
|
|
37
|
+
width: 100%;
|
|
38
|
+
max-width: 380px;
|
|
39
|
+
}
|
|
40
|
+
.brand {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 12px;
|
|
44
|
+
margin-bottom: 32px;
|
|
45
|
+
}
|
|
46
|
+
.brand-icon {
|
|
47
|
+
width: 40px; height: 40px;
|
|
48
|
+
background: linear-gradient(135deg, #6366f1, #818cf8);
|
|
49
|
+
border-radius: 10px;
|
|
50
|
+
display: flex; align-items: center; justify-content: center;
|
|
51
|
+
}
|
|
52
|
+
.brand-name { font-size: 20px; font-weight: 700; letter-spacing: -0.3px; }
|
|
53
|
+
.brand-sub { font-size: 12px; color: var(--color-text-secondary); }
|
|
54
|
+
label {
|
|
55
|
+
display: block;
|
|
56
|
+
font-size: 13px;
|
|
57
|
+
font-weight: 500;
|
|
58
|
+
color: var(--color-text-secondary);
|
|
59
|
+
margin-bottom: 6px;
|
|
60
|
+
}
|
|
61
|
+
input {
|
|
62
|
+
width: 100%;
|
|
63
|
+
padding: 10px 14px;
|
|
64
|
+
background: var(--color-surface-2);
|
|
65
|
+
border: 1px solid var(--color-border);
|
|
66
|
+
border-radius: var(--radius-md);
|
|
67
|
+
color: var(--color-text);
|
|
68
|
+
font-size: 14px;
|
|
69
|
+
font-family: inherit;
|
|
70
|
+
outline: none;
|
|
71
|
+
transition: border-color 0.15s;
|
|
72
|
+
margin-bottom: 16px;
|
|
73
|
+
}
|
|
74
|
+
input:focus { border-color: var(--color-primary); }
|
|
75
|
+
button {
|
|
76
|
+
width: 100%;
|
|
77
|
+
padding: 11px;
|
|
78
|
+
background: var(--color-primary);
|
|
79
|
+
color: white;
|
|
80
|
+
border: none;
|
|
81
|
+
border-radius: var(--radius-md);
|
|
82
|
+
font-size: 14px;
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
font-family: inherit;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
transition: background 0.15s;
|
|
87
|
+
margin-top: 4px;
|
|
88
|
+
}
|
|
89
|
+
button:hover { background: var(--color-primary-hover); }
|
|
90
|
+
button:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
91
|
+
.error {
|
|
92
|
+
color: var(--color-danger);
|
|
93
|
+
font-size: 13px;
|
|
94
|
+
margin-top: 12px;
|
|
95
|
+
text-align: center;
|
|
96
|
+
min-height: 20px;
|
|
97
|
+
}
|
|
98
|
+
.hint {
|
|
99
|
+
font-size: 12px;
|
|
100
|
+
color: var(--color-text-secondary);
|
|
101
|
+
text-align: center;
|
|
102
|
+
margin-top: 20px;
|
|
103
|
+
}
|
|
104
|
+
code {
|
|
105
|
+
font-family: 'JetBrains Mono', monospace;
|
|
106
|
+
background: var(--color-surface-2);
|
|
107
|
+
padding: 2px 6px;
|
|
108
|
+
border-radius: 4px;
|
|
109
|
+
font-size: 11px;
|
|
110
|
+
}
|
|
111
|
+
</style>
|
|
112
|
+
</head>
|
|
113
|
+
<body>
|
|
114
|
+
<div class="login-card">
|
|
115
|
+
<div class="brand">
|
|
116
|
+
<div class="brand-icon">
|
|
117
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
118
|
+
<circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/>
|
|
119
|
+
</svg>
|
|
120
|
+
</div>
|
|
121
|
+
<div>
|
|
122
|
+
<div class="brand-name">Devlens</div>
|
|
123
|
+
<div class="brand-sub">Claude Code Dashboard</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<form id="login-form">
|
|
128
|
+
<label for="username">Username</label>
|
|
129
|
+
<input type="text" id="username" name="username" value="admin" autocomplete="username" required>
|
|
130
|
+
|
|
131
|
+
<label for="password">Password</label>
|
|
132
|
+
<input type="password" id="password" name="password" placeholder="Enter password" autocomplete="current-password" required>
|
|
133
|
+
|
|
134
|
+
<button type="submit" id="login-btn">Sign in</button>
|
|
135
|
+
<div class="error" id="error-msg"></div>
|
|
136
|
+
</form>
|
|
137
|
+
|
|
138
|
+
<p class="hint">Default: <code>admin</code> / <code>devlens</code></p>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<script>
|
|
142
|
+
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
const btn = document.getElementById('login-btn');
|
|
145
|
+
const errorEl = document.getElementById('error-msg');
|
|
146
|
+
btn.disabled = true;
|
|
147
|
+
btn.textContent = 'Signing in...';
|
|
148
|
+
errorEl.textContent = '';
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch('/api/auth/login', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
username: document.getElementById('username').value,
|
|
156
|
+
password: document.getElementById('password').value,
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
const data = await res.json();
|
|
160
|
+
if (data.ok) {
|
|
161
|
+
const redirect = new URLSearchParams(location.search).get('redirect') || '/';
|
|
162
|
+
location.href = redirect;
|
|
163
|
+
} else {
|
|
164
|
+
errorEl.textContent = data.error || 'Login failed';
|
|
165
|
+
btn.disabled = false;
|
|
166
|
+
btn.textContent = 'Sign in';
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
errorEl.textContent = 'Connection error';
|
|
170
|
+
btn.disabled = false;
|
|
171
|
+
btn.textContent = 'Sign in';
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Focus password if username is already filled
|
|
176
|
+
if (document.getElementById('username').value) {
|
|
177
|
+
document.getElementById('password').focus();
|
|
178
|
+
}
|
|
179
|
+
</script>
|
|
180
|
+
</body>
|
|
181
|
+
</html>
|