TOTP + Pushover
This commit is contained in:
Generated
+92
@@ -10,10 +10,88 @@
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"otplib": "^13.2.1",
|
||||
"undici": "^6.18.1",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
|
||||
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@otplib/core": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-13.2.1.tgz",
|
||||
"integrity": "sha512-IyfHvYNCyipDxhmJdcUUvUeT3Hz84/GgM6G2G6BTEmnAKPzNA7U0kYGkxKZWY9h23W94RJk4qiClJRJN5zKGvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@otplib/hotp": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/hotp/-/hotp-13.2.1.tgz",
|
||||
"integrity": "sha512-iRKqvj0TnemtXXtEswzBX50Z0yMNa0lH9PSdr5N4CJc1mDEuUmFFZQqnu3PfA3fPd3WeAU+mHgmK/xq18+K1QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@otplib/core": "13.2.1",
|
||||
"@otplib/uri": "13.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@otplib/plugin-base32-scure": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/plugin-base32-scure/-/plugin-base32-scure-13.2.1.tgz",
|
||||
"integrity": "sha512-vnA2qqgJ/FbFbDNGOLAS8dKfCsJFXwFsZKYklE8yl2INkCOUR0vbVdJ2TVmufzC8R1RRZHW+cDR20ACgc9XFYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@otplib/core": "13.2.1",
|
||||
"@scure/base": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@otplib/plugin-crypto-noble": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/plugin-crypto-noble/-/plugin-crypto-noble-13.2.1.tgz",
|
||||
"integrity": "sha512-Dxjmt4L+5eDWJf5EvbcMp+fxcliyKoB9N9sNQq/vuVAUvq+KiqpiiCQZ/wHyrN0ArB0NdevtK1KByyAq080ldg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^2.0.1",
|
||||
"@otplib/core": "13.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@otplib/totp": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/totp/-/totp-13.2.1.tgz",
|
||||
"integrity": "sha512-LzDzAAK3w8rspF3urBnWjOlxso1SCGxX9Pnu/iy+HkC0y0HgiLsW7jhkr2hJ3u4cyBdL/tOKUhhELwsjyvunwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@otplib/core": "13.2.1",
|
||||
"@otplib/hotp": "13.2.1",
|
||||
"@otplib/uri": "13.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@otplib/uri": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@otplib/uri/-/uri-13.2.1.tgz",
|
||||
"integrity": "sha512-ssYnfiUrFTs/rPRUW8h59m0MVLYOC+UKk7tVGYgtG15lLaLBrNBQjM2YFanuzn9Jm4iv9JxiNG7TRkwcnyR09A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@otplib/core": "13.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz",
|
||||
"integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -578,6 +656,20 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/otplib": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/otplib/-/otplib-13.2.1.tgz",
|
||||
"integrity": "sha512-Cft9h/m34LtvnoB2TjP1E1E6v0biwcUntl6U4e+HgWrTa0bpwmb+u/D9gLFA+U6/ztlvrult0811Bu30nUVUuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@otplib/core": "13.2.1",
|
||||
"@otplib/hotp": "13.2.1",
|
||||
"@otplib/plugin-base32-scure": "13.2.1",
|
||||
"@otplib/plugin-crypto-noble": "13.2.1",
|
||||
"@otplib/totp": "13.2.1",
|
||||
"@otplib/uri": "13.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"otplib": "^13.2.1",
|
||||
"undici": "^6.18.1",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
|
||||
+230
-4
@@ -7,6 +7,17 @@ import { addServer, applyServerLayout, deleteServer, readServers, setPoolsCollap
|
||||
import { detectServerType } from './detect.js';
|
||||
import { readAlertsConfig, writeAlertsConfig } from './store/alerts.js';
|
||||
import { evaluateAndSendAlerts, sendTestAlert } from './alerts/evaluate.js';
|
||||
import { createInstanceId } from './store/instance.js';
|
||||
import {
|
||||
clearPendingTotp,
|
||||
clearSessions,
|
||||
createSession,
|
||||
readTotpConfig,
|
||||
setPendingTotp,
|
||||
validateSession,
|
||||
writeTotpConfig
|
||||
} from './store/totp.js';
|
||||
import { generateSecret, generateURI, verifySync } from 'otplib';
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 5001;
|
||||
@@ -14,10 +25,43 @@ const port = process.env.PORT || 5001;
|
||||
const STORAGE_CACHE_TTL_MS = 15000;
|
||||
let storageCache = null;
|
||||
let storageRefresh = null;
|
||||
let instanceMeta = { instanceId: 'unknown', startedAt: null };
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
const TOTP_OPEN_PATHS = new Set([
|
||||
'/api/health',
|
||||
'/api/totp',
|
||||
'/api/totp/setup',
|
||||
'/api/totp/activate',
|
||||
'/api/totp/verify',
|
||||
'/api/totp/disable',
|
||||
'/api/totp/regenerate'
|
||||
]);
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
if (!req.path.startsWith('/api')) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const totp = await readTotpConfig();
|
||||
if (!totp.enabled || TOTP_OPEN_PATHS.has(req.path)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const token = String(req.headers['x-vault-session'] || '').trim();
|
||||
const { valid } = await validateSession(token);
|
||||
if (valid) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(401).json({ error: 'TOTP required.', code: 'TOTP_REQUIRED' });
|
||||
});
|
||||
|
||||
const normalizeType = (type) => {
|
||||
if (!type) {
|
||||
return null;
|
||||
@@ -53,7 +97,14 @@ const resolveStorageForServer = async (server) => {
|
||||
};
|
||||
|
||||
app.get('/api/health', (_req, res) => {
|
||||
res.json({ ok: true, service: 'vault', uptime: process.uptime(), timestamp: new Date().toISOString() });
|
||||
res.json({
|
||||
ok: true,
|
||||
service: 'vault',
|
||||
uptime: process.uptime(),
|
||||
timestamp: new Date().toISOString(),
|
||||
instanceId: instanceMeta.instanceId,
|
||||
startedAt: instanceMeta.startedAt
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/servers', async (_req, res) => {
|
||||
@@ -283,12 +334,178 @@ app.post('/api/alerts/test', async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/totp', async (_req, res) => {
|
||||
const config = await readTotpConfig();
|
||||
res.json({
|
||||
totp: {
|
||||
enabled: Boolean(config.enabled),
|
||||
issuer: config.issuer,
|
||||
label: config.label,
|
||||
createdAt: config.createdAt,
|
||||
updatedAt: config.updatedAt,
|
||||
hasSecret: Boolean(config.secret),
|
||||
pending: Boolean(config.pendingSecret),
|
||||
pendingCreatedAt: config.pendingCreatedAt,
|
||||
pendingOtpauthUrl: config.pendingSecret ? config.pendingOtpauthUrl || '' : ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/totp/setup', async (_req, res) => {
|
||||
const current = await readTotpConfig();
|
||||
const issuer = current.issuer || 'Vault';
|
||||
const label = current.label || 'vault';
|
||||
const secret = generateSecret();
|
||||
const otpauthUrl = generateURI({ secret, issuer, label });
|
||||
const updated = await setPendingTotp({ secret, otpauthUrl });
|
||||
res.json({
|
||||
totp: {
|
||||
enabled: Boolean(updated.enabled),
|
||||
issuer: updated.issuer,
|
||||
label: updated.label,
|
||||
pending: true,
|
||||
pendingCreatedAt: updated.pendingCreatedAt,
|
||||
pendingOtpauthUrl: updated.pendingOtpauthUrl || '',
|
||||
createdAt: updated.createdAt,
|
||||
updatedAt: updated.updatedAt
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/totp/regenerate', async (_req, res) => {
|
||||
const current = await readTotpConfig();
|
||||
const issuer = current.issuer || 'Vault';
|
||||
const label = current.label || 'vault';
|
||||
const secret = generateSecret();
|
||||
const otpauthUrl = generateURI({ secret, issuer, label });
|
||||
const updated = await setPendingTotp({ secret, otpauthUrl });
|
||||
res.json({
|
||||
totp: {
|
||||
enabled: Boolean(updated.enabled),
|
||||
issuer: updated.issuer,
|
||||
label: updated.label,
|
||||
pending: true,
|
||||
pendingCreatedAt: updated.pendingCreatedAt,
|
||||
pendingOtpauthUrl: updated.pendingOtpauthUrl || '',
|
||||
createdAt: updated.createdAt,
|
||||
updatedAt: updated.updatedAt
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/totp/disable', async (req, res) => {
|
||||
const { code } = req.body ?? {};
|
||||
const current = await readTotpConfig();
|
||||
if (current.enabled && current.secret) {
|
||||
const token = String(code || '').replace(/\s+/g, '');
|
||||
const valid = token ? verifySync({ token, secret: current.secret }) : false;
|
||||
if (!valid) {
|
||||
res.status(400).json({ error: 'Valid TOTP code required to disable.' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
await clearSessions();
|
||||
await clearPendingTotp();
|
||||
const updated = await writeTotpConfig({
|
||||
enabled: false,
|
||||
secret: '',
|
||||
otpauthUrl: ''
|
||||
});
|
||||
res.json({
|
||||
totp: {
|
||||
enabled: false,
|
||||
issuer: updated.issuer,
|
||||
label: updated.label,
|
||||
pending: false,
|
||||
createdAt: updated.createdAt,
|
||||
updatedAt: updated.updatedAt
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/totp/activate', async (req, res) => {
|
||||
const { code, rememberDays } = req.body ?? {};
|
||||
const current = await readTotpConfig();
|
||||
if (!current.pendingSecret) {
|
||||
res.status(400).json({ error: 'No pending TOTP setup found.' });
|
||||
return;
|
||||
}
|
||||
const token = String(code || '').replace(/\s+/g, '');
|
||||
if (!token) {
|
||||
res.status(400).json({ error: 'code is required.' });
|
||||
return;
|
||||
}
|
||||
const valid = verifySync({ token, secret: current.pendingSecret });
|
||||
if (!valid) {
|
||||
res.status(400).json({ error: 'Invalid TOTP code.' });
|
||||
return;
|
||||
}
|
||||
|
||||
await clearSessions();
|
||||
const updated = await writeTotpConfig({
|
||||
enabled: true,
|
||||
secret: current.pendingSecret,
|
||||
otpauthUrl: current.pendingOtpauthUrl || ''
|
||||
});
|
||||
await clearPendingTotp();
|
||||
const ttlDays = Math.max(0, Math.min(365, Number(rememberDays) || 0));
|
||||
const userAgent = String(req.headers['user-agent'] || 'device').slice(0, 80);
|
||||
const session = await createSession({ ttlDays: ttlDays || 1, label: userAgent });
|
||||
res.json({
|
||||
ok: true,
|
||||
sessionToken: session.token,
|
||||
expiresAt: session.expiresAt,
|
||||
totp: {
|
||||
enabled: true,
|
||||
issuer: updated.issuer,
|
||||
label: updated.label,
|
||||
pending: false,
|
||||
createdAt: updated.createdAt,
|
||||
updatedAt: updated.updatedAt
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/totp/verify', async (req, res) => {
|
||||
const { code, rememberDays } = req.body ?? {};
|
||||
const totp = await readTotpConfig();
|
||||
if (!totp.enabled || !totp.secret) {
|
||||
res.status(400).json({ error: 'TOTP is not configured.' });
|
||||
return;
|
||||
}
|
||||
const token = String(code || '').replace(/\s+/g, '');
|
||||
if (!token) {
|
||||
res.status(400).json({ error: 'code is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const valid = verifySync({ token, secret: totp.secret });
|
||||
if (!valid) {
|
||||
res.status(400).json({ error: 'Invalid TOTP code.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const ttlDays = Math.max(0, Math.min(365, Number(rememberDays) || 0));
|
||||
const userAgent = String(req.headers['user-agent'] || 'device').slice(0, 80);
|
||||
const session = await createSession({ ttlDays: ttlDays || 1, label: userAgent });
|
||||
res.json({
|
||||
ok: true,
|
||||
sessionToken: session.token,
|
||||
expiresAt: session.expiresAt
|
||||
});
|
||||
});
|
||||
|
||||
const buildStoragePayload = async () => {
|
||||
const stored = await readServers();
|
||||
const configured = stored.filter((server) => server.state === 'configured');
|
||||
|
||||
if (configured.length === 0) {
|
||||
return { source: 'empty', generatedAt: new Date().toISOString(), servers: [] };
|
||||
return {
|
||||
source: 'empty',
|
||||
generatedAt: new Date().toISOString(),
|
||||
instanceId: instanceMeta.instanceId,
|
||||
servers: []
|
||||
};
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(configured.map((server) => resolveStorageForServer(server)));
|
||||
@@ -324,6 +541,7 @@ const buildStoragePayload = async () => {
|
||||
return {
|
||||
source: errors.length ? 'error' : 'live',
|
||||
generatedAt: new Date().toISOString(),
|
||||
instanceId: instanceMeta.instanceId,
|
||||
servers,
|
||||
errors
|
||||
};
|
||||
@@ -385,6 +603,14 @@ app.get('/api/storage', async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Vault backend listening on port ${port}`);
|
||||
const start = async () => {
|
||||
instanceMeta = await createInstanceId();
|
||||
app.listen(port, () => {
|
||||
console.log(`Vault backend listening on port ${port} (instance ${instanceMeta.instanceId})`);
|
||||
});
|
||||
};
|
||||
|
||||
start().catch((error) => {
|
||||
console.error('Failed to start backend:', error.message || error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const dataDir = path.resolve(process.cwd(), 'data');
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const backendDir = path.resolve(__dirname, '..', '..');
|
||||
const dataDir = path.join(backendDir, 'data');
|
||||
const dataFile = path.join(dataDir, 'alerts.json');
|
||||
|
||||
const DEFAULT_ALERTS = {
|
||||
@@ -63,4 +67,3 @@ export async function writeAlertsConfig(patch) {
|
||||
}
|
||||
|
||||
export { DEFAULT_ALERTS };
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const backendDir = path.resolve(__dirname, '..', '..');
|
||||
const dataDir = path.join(backendDir, 'data');
|
||||
const dataFile = path.join(dataDir, 'instance.json');
|
||||
|
||||
async function ensureDir() {
|
||||
await fs.mkdir(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
export async function createInstanceId() {
|
||||
await ensureDir();
|
||||
const instanceId = randomUUID();
|
||||
const payload = {
|
||||
instanceId,
|
||||
startedAt: new Date().toISOString()
|
||||
};
|
||||
await fs.writeFile(dataFile, JSON.stringify(payload, null, 2), 'utf-8');
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function readInstanceId() {
|
||||
await ensureDir();
|
||||
try {
|
||||
const raw = await fs.readFile(dataFile, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed?.instanceId) {
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
// ignore and recreate below
|
||||
}
|
||||
return createInstanceId();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const dataDir = path.resolve(process.cwd(), 'data');
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const backendDir = path.resolve(__dirname, '..', '..');
|
||||
const dataDir = path.join(backendDir, 'data');
|
||||
const dataFile = path.join(dataDir, 'servers.json');
|
||||
|
||||
async function ensureStore() {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { randomUUID, randomBytes } from 'crypto';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const backendDir = path.resolve(__dirname, '..', '..');
|
||||
const dataDir = path.join(backendDir, 'data');
|
||||
const dataFile = path.join(dataDir, 'totp.json');
|
||||
|
||||
const DEFAULT_TOTP = {
|
||||
enabled: false,
|
||||
secret: '',
|
||||
otpauthUrl: '',
|
||||
issuer: 'Vault',
|
||||
label: 'vault',
|
||||
pendingSecret: '',
|
||||
pendingOtpauthUrl: '',
|
||||
pendingCreatedAt: null,
|
||||
sessions: [],
|
||||
createdAt: null,
|
||||
updatedAt: null
|
||||
};
|
||||
|
||||
async function ensureStore() {
|
||||
await fs.mkdir(dataDir, { recursive: true });
|
||||
try {
|
||||
await fs.access(dataFile);
|
||||
} catch {
|
||||
await fs.writeFile(dataFile, JSON.stringify(DEFAULT_TOTP, null, 2), 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
export async function readTotpConfig() {
|
||||
await ensureStore();
|
||||
try {
|
||||
const raw = await fs.readFile(dataFile, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
...DEFAULT_TOTP,
|
||||
...parsed
|
||||
};
|
||||
} catch {
|
||||
return { ...DEFAULT_TOTP };
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeTotpConfig(patch) {
|
||||
const current = await readTotpConfig();
|
||||
const timestamp = new Date().toISOString();
|
||||
const next = {
|
||||
...current,
|
||||
...patch,
|
||||
updatedAt: timestamp
|
||||
};
|
||||
if (!current.createdAt) {
|
||||
next.createdAt = timestamp;
|
||||
}
|
||||
await fs.writeFile(dataFile, JSON.stringify(next, null, 2), 'utf-8');
|
||||
return next;
|
||||
}
|
||||
|
||||
const normalizeSessions = (sessions) => {
|
||||
if (!Array.isArray(sessions)) {
|
||||
return [];
|
||||
}
|
||||
const now = Date.now();
|
||||
return sessions.filter((session) => {
|
||||
const expiresAt = new Date(session.expiresAt || 0).getTime();
|
||||
return Number.isFinite(expiresAt) && expiresAt > now;
|
||||
});
|
||||
};
|
||||
|
||||
const withPrunedSessions = async () => {
|
||||
const config = await readTotpConfig();
|
||||
const pruned = normalizeSessions(config.sessions);
|
||||
if (pruned.length === (config.sessions || []).length) {
|
||||
return { config, sessions: pruned };
|
||||
}
|
||||
const updated = await writeTotpConfig({ sessions: pruned });
|
||||
return { config: updated, sessions: pruned };
|
||||
};
|
||||
|
||||
const createToken = () => {
|
||||
const raw = randomBytes(24).toString('base64url');
|
||||
return `vault_${raw}`;
|
||||
};
|
||||
|
||||
export async function createSession({ ttlDays = 30, label = 'device' } = {}) {
|
||||
const { sessions } = await withPrunedSessions();
|
||||
const now = Date.now();
|
||||
const ttlMs = Math.max(1, Number(ttlDays) || 30) * 24 * 60 * 60 * 1000;
|
||||
const expiresAt = new Date(now + ttlMs).toISOString();
|
||||
const session = {
|
||||
id: randomUUID(),
|
||||
token: createToken(),
|
||||
label,
|
||||
createdAt: new Date(now).toISOString(),
|
||||
expiresAt
|
||||
};
|
||||
const nextSessions = [...sessions, session];
|
||||
await writeTotpConfig({ sessions: nextSessions });
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function validateSession(token) {
|
||||
if (!token) {
|
||||
return { valid: false, session: null };
|
||||
}
|
||||
const { sessions } = await withPrunedSessions();
|
||||
const match = sessions.find((session) => session.token === token) || null;
|
||||
return { valid: Boolean(match), session: match };
|
||||
}
|
||||
|
||||
export async function clearSessions() {
|
||||
const updated = await writeTotpConfig({ sessions: [] });
|
||||
return updated.sessions || [];
|
||||
}
|
||||
|
||||
export async function setPendingTotp({ secret, otpauthUrl }) {
|
||||
const updated = await writeTotpConfig({
|
||||
pendingSecret: secret,
|
||||
pendingOtpauthUrl: otpauthUrl,
|
||||
pendingCreatedAt: new Date().toISOString()
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function clearPendingTotp() {
|
||||
const updated = await writeTotpConfig({
|
||||
pendingSecret: '',
|
||||
pendingOtpauthUrl: '',
|
||||
pendingCreatedAt: null
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
export { DEFAULT_TOTP };
|
||||
-1444
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vault Overview</title>
|
||||
<script type="module" crossorigin src="/assets/index-BK-UjMSe.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DjBL3oZq.css">
|
||||
<script type="module" crossorigin src="/assets/index-DztdzZJA.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Ca0EvzTC.css">
|
||||
<link rel="manifest" href="/manifest.webmanifest"></head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,o)=>{const r=e||("document"in self?document.currentScript.src:"")||location.href;if(s[r])return;let t={};const c=e=>i(e,r),l={module:{uri:r},exports:t,require:c};s[r]=Promise.all(n.map(e=>l[e]||c(e))).then(e=>(o(...e),t))}}define(["./workbox-8c29f6e4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"cac9af624d426ce3ce0abbd7ece62cd5"},{url:"assets/workbox-window.prod.es5-vqzQaGvo.js",revision:null},{url:"assets/index-DjBL3oZq.css",revision:null},{url:"assets/index-BK-UjMSe.js",revision:null},{url:"icons/icon-192.svg",revision:"148d70820b3f8540f998696ffce4521a"},{url:"icons/icon-512.svg",revision:"d95bff06bd458724ce54678616112431"},{url:"manifest.webmanifest",revision:"ed28637cdae5a42ef9abf5d45823d526"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
|
||||
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,o)=>{const r=e||("document"in self?document.currentScript.src:"")||location.href;if(s[r])return;let t={};const l=e=>i(e,r),d={module:{uri:r},exports:t,require:l};s[r]=Promise.all(n.map(e=>d[e]||l(e))).then(e=>(o(...e),t))}}define(["./workbox-8c29f6e4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"015603a7fc7218d363350c68a1362db4"},{url:"assets/workbox-window.prod.es5-vqzQaGvo.js",revision:null},{url:"assets/index-DztdzZJA.js",revision:null},{url:"assets/index-Ca0EvzTC.css",revision:null},{url:"icons/icon-192.svg",revision:"148d70820b3f8540f998696ffce4521a"},{url:"icons/icon-512.svg",revision:"d95bff06bd458724ce54678616112431"},{url:"manifest.webmanifest",revision:"ed28637cdae5a42ef9abf5d45823d526"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
|
||||
|
||||
@@ -24,6 +24,11 @@ server {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:5001;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
Generated
+321
-4
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.6.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
@@ -2997,6 +2998,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001766",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
||||
@@ -3018,11 +3028,91 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@@ -3035,7 +3125,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
@@ -3179,6 +3268,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
@@ -3225,6 +3323,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
@@ -3583,6 +3687,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
@@ -3708,6 +3825,15 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -4117,7 +4243,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -4521,6 +4646,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
@@ -4738,6 +4875,42 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -4745,6 +4918,15 @@
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -4809,6 +4991,15 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
@@ -4911,6 +5102,23 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -5103,6 +5311,15 @@
|
||||
"regjsparser": "bin/parser"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@@ -5113,6 +5330,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
@@ -5151,7 +5374,6 @@
|
||||
"integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -5320,6 +5542,12 @@
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
@@ -5819,6 +6047,7 @@
|
||||
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
@@ -6268,6 +6497,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/which-typed-array": {
|
||||
"version": "1.1.20",
|
||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
|
||||
@@ -6695,12 +6930,94 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,15 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"primereact": "^10.6.0",
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.6.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"vite-plugin-pwa": "^0.19.7",
|
||||
"vite": "^5.0.0"
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-pwa": "^0.19.7"
|
||||
}
|
||||
}
|
||||
|
||||
+271
-7
@@ -159,21 +159,27 @@ body {
|
||||
}
|
||||
|
||||
.sheet-backdrop {
|
||||
--settings-tabbar-height: calc(10px + 12px + env(safe-area-inset-bottom) + 32px);
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
display: grid;
|
||||
align-items: end;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.sheet-card {
|
||||
background: var(--card-strong);
|
||||
border-radius: 20px 20px 0 0;
|
||||
padding: 20px 18px 28px;
|
||||
padding: 20px 18px 12px;
|
||||
border: 1px solid rgba(30, 41, 59, 0.16);
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
border-bottom: none;
|
||||
max-height: calc(80vh - var(--settings-tabbar-height, 68px));
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sheet-header {
|
||||
@@ -190,8 +196,93 @@ body {
|
||||
}
|
||||
|
||||
.drawer-system {
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sheet-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-bottom: 0.5rem;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.sheet-body::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bottom-nav.settings-bottom-nav {
|
||||
--settings-tabbar-height: calc(10px + 12px + env(safe-area-inset-bottom) + 32px);
|
||||
position: static;
|
||||
left: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
z-index: auto;
|
||||
display: grid !important;
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
padding: 10px 12px calc(12px + env(safe-area-inset-bottom));
|
||||
background: rgba(248, 250, 252, 0.95);
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.35);
|
||||
}
|
||||
|
||||
.settings-tab-action {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--ink-soft);
|
||||
font-size: 11px;
|
||||
padding: 0.35rem 0.25rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.settings-tab-action > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bottom-nav.settings-bottom-nav .settings-tab-action {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--ink-soft);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.bottom-nav.settings-bottom-nav .settings-tab-action > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.settings-tab-action.is-active {
|
||||
color: var(--ink);
|
||||
background: transparent;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-tab-action:focus-visible {
|
||||
outline: 2px solid rgba(15, 118, 110, 0.45);
|
||||
outline-offset: 2px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.drawer-section {
|
||||
@@ -210,8 +301,50 @@ body {
|
||||
}
|
||||
|
||||
.drawer-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-actions .p-button {
|
||||
flex: 0 0 180px;
|
||||
width: 180px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.drawer-actions .p-button-text {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.form-actions.drawer-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-actions .p-button .p-button-label,
|
||||
.drawer-actions .p-button .p-button-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drawer-actions .p-button .p-button-label {
|
||||
flex: 0 0 auto;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.drawer-actions .p-button .p-button-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.drawer-hint {
|
||||
@@ -242,6 +375,113 @@ body {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.totp-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.totp-meta {
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.totp-secret-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem;
|
||||
background: rgba(248, 250, 252, 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.totp-secret-title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.totp-secret-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.95rem;
|
||||
padding: 0.4rem 0.55rem;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
border: 1px dashed rgba(148, 163, 184, 0.6);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.totp-secret-hint {
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.totp-secret-url {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--ink-soft);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.totp-qr {
|
||||
width: 200px;
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: white;
|
||||
padding: 0.35rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.auth-gate {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 2rem clamp(1.25rem, 4vw, 3rem);
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: min(640px, 96vw);
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.auth-card .p-card-body {
|
||||
padding: 1.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.auth-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
justify-content: flex-start;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.auth-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.auth-meta-field {
|
||||
min-width: 220px;
|
||||
flex: 1 1 240px;
|
||||
}
|
||||
|
||||
.auth-meta-field .p-inputtext {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-secret {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
@@ -307,6 +547,21 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
color: var(--ink-soft);
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-state .p-progressspinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.error-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -885,4 +1140,13 @@ body {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.drawer-actions {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.drawer-actions .p-button {
|
||||
flex: 1 1 50%;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
+796
-152
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,12 @@ import './App.css';
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
registerSW({ immediate: true });
|
||||
const updateSW = registerSW({
|
||||
immediate: true,
|
||||
onNeedRefresh() {
|
||||
updateSW(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
|
||||
@@ -15,6 +15,11 @@ export default defineConfig(({ mode }) => {
|
||||
devOptions: {
|
||||
enabled: false
|
||||
},
|
||||
workbox: {
|
||||
cleanupOutdatedCaches: true,
|
||||
clientsClaim: true,
|
||||
skipWaiting: true
|
||||
},
|
||||
manifest: {
|
||||
name: 'Vault Storage',
|
||||
short_name: 'Vault',
|
||||
|
||||
Reference in New Issue
Block a user