mirror of
https://github.com/LogicLabs-OU/OpenArchiver.git
synced 2026-04-06 00:31:57 +02:00
Compare commits
6 Commits
docs
...
security-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a56df62099 | ||
|
|
26a760b232 | ||
|
|
6be0774bc4 | ||
|
|
4a23f8f29f | ||
|
|
074256ed59 | ||
|
|
7d178d786b |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: [wayneshn]
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "open-archiver",
|
||||
"version": "0.3.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -- pnpm --filter \"./packages/*\" --parallel dev",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"yauzl": "^3.2.0",
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
@@ -74,7 +74,6 @@
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^24.0.12",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"bull-board": "^2.1.3",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
@@ -4,54 +4,63 @@ import { z } from 'zod';
|
||||
import { config } from '../../config';
|
||||
|
||||
const generateApiKeySchema = z.object({
|
||||
name: z.string().min(1, 'API kay name must be more than 1 characters').max(255, 'API kay name must not be more than 255 characters'),
|
||||
expiresInDays: z.number().int().positive('Only positive number is allowed').max(730, "The API key must expire within 2 years / 730 days."),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, 'API kay name must be more than 1 characters')
|
||||
.max(255, 'API kay name must not be more than 255 characters'),
|
||||
expiresInDays: z
|
||||
.number()
|
||||
.int()
|
||||
.positive('Only positive number is allowed')
|
||||
.max(730, 'The API key must expire within 2 years / 730 days.'),
|
||||
});
|
||||
|
||||
export class ApiKeyController {
|
||||
public async generateApiKey(req: Request, res: Response) {
|
||||
if (config.app.isDemo) {
|
||||
return res.status(403).json({ message: req.t('errors.demoMode') });
|
||||
}
|
||||
try {
|
||||
const { name, expiresInDays } = generateApiKeySchema.parse(req.body);
|
||||
if (!req.user || !req.user.sub) {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
const userId = req.user.sub;
|
||||
public async generateApiKey(req: Request, res: Response) {
|
||||
if (config.app.isDemo) {
|
||||
return res.status(403).json({ message: req.t('errors.demoMode') });
|
||||
}
|
||||
try {
|
||||
const { name, expiresInDays } = generateApiKeySchema.parse(req.body);
|
||||
if (!req.user || !req.user.sub) {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
const userId = req.user.sub;
|
||||
|
||||
const key = await ApiKeyService.generate(userId, name, expiresInDays);
|
||||
const key = await ApiKeyService.generate(userId, name, expiresInDays);
|
||||
|
||||
res.status(201).json({ key });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: req.t('api.requestBodyInvalid'), errors: error.message });
|
||||
}
|
||||
res.status(500).json({ message: req.t('errors.internalServerError') });
|
||||
}
|
||||
}
|
||||
res.status(201).json({ key });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: req.t('api.requestBodyInvalid'), errors: error.message });
|
||||
}
|
||||
res.status(500).json({ message: req.t('errors.internalServerError') });
|
||||
}
|
||||
}
|
||||
|
||||
public async getApiKeys(req: Request, res: Response) {
|
||||
if (!req.user || !req.user.sub) {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
const userId = req.user.sub;
|
||||
const keys = await ApiKeyService.getKeys(userId);
|
||||
public async getApiKeys(req: Request, res: Response) {
|
||||
if (!req.user || !req.user.sub) {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
const userId = req.user.sub;
|
||||
const keys = await ApiKeyService.getKeys(userId);
|
||||
|
||||
res.status(200).json(keys);
|
||||
}
|
||||
res.status(200).json(keys);
|
||||
}
|
||||
|
||||
public async deleteApiKey(req: Request, res: Response) {
|
||||
if (config.app.isDemo) {
|
||||
return res.status(403).json({ message: req.t('errors.demoMode') });
|
||||
}
|
||||
const { id } = req.params;
|
||||
if (!req.user || !req.user.sub) {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
const userId = req.user.sub;
|
||||
await ApiKeyService.deleteKey(id, userId);
|
||||
public async deleteApiKey(req: Request, res: Response) {
|
||||
if (config.app.isDemo) {
|
||||
return res.status(403).json({ message: req.t('errors.demoMode') });
|
||||
}
|
||||
const { id } = req.params;
|
||||
if (!req.user || !req.user.sub) {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
const userId = req.user.sub;
|
||||
await ApiKeyService.deleteKey(id, userId);
|
||||
|
||||
res.status(204).send({ message: req.t('apiKeys.deleteSuccess') });
|
||||
}
|
||||
res.status(204).send({ message: req.t('apiKeys.deleteSuccess') });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ export const rateLimiter = rateLimit({
|
||||
max: config.api.rateLimit.max,
|
||||
message: {
|
||||
status: 429,
|
||||
message: `Too many requests from this IP, please try again after ${windowInMinutes} minutes`
|
||||
message: `Too many requests from this IP, please try again after ${windowInMinutes} minutes`,
|
||||
},
|
||||
statusCode: 429,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ export const requireAuth = (authService: AuthService) => {
|
||||
req.user = {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
roles: user.role ? [user.role.name] : []
|
||||
roles: user.role ? [user.role.name] : [],
|
||||
};
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import { requireAuth } from '../middleware/requireAuth';
|
||||
import { AuthService } from '../../services/AuthService';
|
||||
|
||||
export const apiKeyRoutes = (authService: AuthService) => {
|
||||
const router = Router();
|
||||
const controller = new ApiKeyController();
|
||||
const router = Router();
|
||||
const controller = new ApiKeyController();
|
||||
|
||||
router.post('/', requireAuth(authService), controller.generateApiKey);
|
||||
router.get('/', requireAuth(authService), controller.getApiKeys);
|
||||
router.delete('/:id', requireAuth(authService), controller.deleteApiKey);
|
||||
router.post('/', requireAuth(authService), controller.generateApiKey);
|
||||
router.get('/', requireAuth(authService), controller.getApiKeys);
|
||||
router.delete('/:id', requireAuth(authService), controller.deleteApiKey);
|
||||
|
||||
return router;
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'dotenv/config';
|
||||
|
||||
export const apiConfig = {
|
||||
rateLimit: {
|
||||
windowMs: process.env.RATE_LIMIT_WINDOW_MS ? parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) : 1 * 60 * 1000, // 1 minutes
|
||||
max: process.env.RATE_LIMIT_MAX_REQUESTS ? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) : 100, // limit each IP to 100 requests per windowMs
|
||||
}
|
||||
rateLimit: {
|
||||
windowMs: process.env.RATE_LIMIT_WINDOW_MS
|
||||
? parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10)
|
||||
: 1 * 60 * 1000, // 1 minutes
|
||||
max: process.env.RATE_LIMIT_MAX_REQUESTS
|
||||
? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10)
|
||||
: 100, // limit each IP to 100 requests per windowMs
|
||||
},
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,146 +1,146 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1752225352591,
|
||||
"tag": "0000_amusing_namora",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1752326803882,
|
||||
"tag": "0001_odd_night_thrasher",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1752332648392,
|
||||
"tag": "0002_lethal_quentin_quire",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1752332967084,
|
||||
"tag": "0003_petite_wrecker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1752606108876,
|
||||
"tag": "0004_sleepy_paper_doll",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "7",
|
||||
"when": 1752606327253,
|
||||
"tag": "0005_chunky_sue_storm",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1753112018514,
|
||||
"tag": "0006_majestic_caretaker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "7",
|
||||
"when": 1753190159356,
|
||||
"tag": "0007_handy_archangel",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "7",
|
||||
"when": 1753370737317,
|
||||
"tag": "0008_eminent_the_spike",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "7",
|
||||
"when": 1754337938241,
|
||||
"tag": "0009_late_lenny_balinger",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "7",
|
||||
"when": 1754420780849,
|
||||
"tag": "0010_perpetual_lightspeed",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "7",
|
||||
"when": 1754422064158,
|
||||
"tag": "0011_tan_blackheart",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "7",
|
||||
"when": 1754476962901,
|
||||
"tag": "0012_warm_the_stranger",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "7",
|
||||
"when": 1754659373517,
|
||||
"tag": "0013_classy_talkback",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "7",
|
||||
"when": 1754831765718,
|
||||
"tag": "0014_foamy_vapor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "7",
|
||||
"when": 1755443936046,
|
||||
"tag": "0015_wakeful_norman_osborn",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 16,
|
||||
"version": "7",
|
||||
"when": 1755780572342,
|
||||
"tag": "0016_lonely_mariko_yashida",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "7",
|
||||
"when": 1755961566627,
|
||||
"tag": "0017_tranquil_shooting_star",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "7",
|
||||
"when": 1756911118035,
|
||||
"tag": "0018_flawless_owl",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 19,
|
||||
"version": "7",
|
||||
"when": 1756937533843,
|
||||
"tag": "0019_confused_scream",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1752225352591,
|
||||
"tag": "0000_amusing_namora",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1752326803882,
|
||||
"tag": "0001_odd_night_thrasher",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1752332648392,
|
||||
"tag": "0002_lethal_quentin_quire",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1752332967084,
|
||||
"tag": "0003_petite_wrecker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1752606108876,
|
||||
"tag": "0004_sleepy_paper_doll",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "7",
|
||||
"when": 1752606327253,
|
||||
"tag": "0005_chunky_sue_storm",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1753112018514,
|
||||
"tag": "0006_majestic_caretaker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "7",
|
||||
"when": 1753190159356,
|
||||
"tag": "0007_handy_archangel",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "7",
|
||||
"when": 1753370737317,
|
||||
"tag": "0008_eminent_the_spike",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "7",
|
||||
"when": 1754337938241,
|
||||
"tag": "0009_late_lenny_balinger",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "7",
|
||||
"when": 1754420780849,
|
||||
"tag": "0010_perpetual_lightspeed",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "7",
|
||||
"when": 1754422064158,
|
||||
"tag": "0011_tan_blackheart",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "7",
|
||||
"when": 1754476962901,
|
||||
"tag": "0012_warm_the_stranger",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "7",
|
||||
"when": 1754659373517,
|
||||
"tag": "0013_classy_talkback",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "7",
|
||||
"when": 1754831765718,
|
||||
"tag": "0014_foamy_vapor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "7",
|
||||
"when": 1755443936046,
|
||||
"tag": "0015_wakeful_norman_osborn",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 16,
|
||||
"version": "7",
|
||||
"when": 1755780572342,
|
||||
"tag": "0016_lonely_mariko_yashida",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "7",
|
||||
"when": 1755961566627,
|
||||
"tag": "0017_tranquil_shooting_star",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "7",
|
||||
"when": 1756911118035,
|
||||
"tag": "0018_flawless_owl",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 19,
|
||||
"version": "7",
|
||||
"when": 1756937533843,
|
||||
"tag": "0019_confused_scream",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||
import { users } from './users';
|
||||
|
||||
export const apiKeys = pgTable('api_keys', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
userId: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
key: text('key').notNull(), // Encrypted API key
|
||||
keyHash: text('key_hash').notNull(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'date' }).notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
userId: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
key: text('key').notNull(), // Encrypted API key
|
||||
keyHash: text('key_hash').notNull(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'date' }).notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
@@ -95,10 +95,7 @@ app.use('/v1/upload', uploadRouter);
|
||||
// Middleware for all other routes
|
||||
app.use((req, res, next) => {
|
||||
// exclude certain API endpoints from the rate limiter, for example status, system settings
|
||||
const excludedPatterns = [
|
||||
/^\/v\d+\/auth\/status$/,
|
||||
/^\/v\d+\/settings\/system$/
|
||||
];
|
||||
const excludedPatterns = [/^\/v\d+\/auth\/status$/, /^\/v\d+\/settings\/system$/];
|
||||
for (const pattern of excludedPatterns) {
|
||||
if (pattern.test(req.path)) {
|
||||
return next();
|
||||
|
||||
@@ -6,67 +6,67 @@ import { and, eq } from 'drizzle-orm';
|
||||
import { ApiKey } from '@open-archiver/types';
|
||||
|
||||
export class ApiKeyService {
|
||||
public static async generate(
|
||||
userId: string,
|
||||
name: string,
|
||||
expiresInDays: number
|
||||
): Promise<string> {
|
||||
const key = randomBytes(32).toString('hex');
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInDays);
|
||||
const keyHash = createHash('sha256').update(key).digest('hex');
|
||||
public static async generate(
|
||||
userId: string,
|
||||
name: string,
|
||||
expiresInDays: number
|
||||
): Promise<string> {
|
||||
const key = randomBytes(32).toString('hex');
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + expiresInDays);
|
||||
const keyHash = createHash('sha256').update(key).digest('hex');
|
||||
|
||||
await db.insert(apiKeys).values({
|
||||
userId,
|
||||
name,
|
||||
key: CryptoService.encrypt(key),
|
||||
keyHash,
|
||||
expiresAt
|
||||
});
|
||||
await db.insert(apiKeys).values({
|
||||
userId,
|
||||
name,
|
||||
key: CryptoService.encrypt(key),
|
||||
keyHash,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
return key;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
public static async getKeys(userId: string): Promise<ApiKey[]> {
|
||||
const keys = await db.select().from(apiKeys).where(eq(apiKeys.userId, userId));
|
||||
public static async getKeys(userId: string): Promise<ApiKey[]> {
|
||||
const keys = await db.select().from(apiKeys).where(eq(apiKeys.userId, userId));
|
||||
|
||||
return keys
|
||||
.map((apiKey) => {
|
||||
const decryptedKey = CryptoService.decrypt(apiKey.key);
|
||||
if (!decryptedKey) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...apiKey,
|
||||
key: decryptedKey.slice(0, 5) + "*****",
|
||||
expiresAt: apiKey.expiresAt.toISOString(),
|
||||
createdAt: apiKey.createdAt.toISOString()
|
||||
};
|
||||
})
|
||||
.filter((k): k is NonNullable<typeof k> => k !== null);
|
||||
}
|
||||
return keys
|
||||
.map((apiKey) => {
|
||||
const decryptedKey = CryptoService.decrypt(apiKey.key);
|
||||
if (!decryptedKey) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...apiKey,
|
||||
key: decryptedKey.slice(0, 5) + '*****',
|
||||
expiresAt: apiKey.expiresAt.toISOString(),
|
||||
createdAt: apiKey.createdAt.toISOString(),
|
||||
};
|
||||
})
|
||||
.filter((k): k is NonNullable<typeof k> => k !== null);
|
||||
}
|
||||
|
||||
public static async deleteKey(id: string, userId: string) {
|
||||
await db.delete(apiKeys).where(and(eq(apiKeys.id, id), eq(apiKeys.userId, userId)));
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param key API key
|
||||
* @returns The owner user ID or null. null means the API key is not found.
|
||||
*/
|
||||
public static async validateKey(key: string): Promise<string | null> {
|
||||
const keyHash = createHash('sha256').update(key).digest('hex');
|
||||
const [apiKey] = await db.select().from(apiKeys).where(eq(apiKeys.keyHash, keyHash));
|
||||
if (!apiKey || apiKey.expiresAt < new Date()) {
|
||||
return null;
|
||||
}
|
||||
public static async deleteKey(id: string, userId: string) {
|
||||
await db.delete(apiKeys).where(and(eq(apiKeys.id, id), eq(apiKeys.userId, userId)));
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param key API key
|
||||
* @returns The owner user ID or null. null means the API key is not found.
|
||||
*/
|
||||
public static async validateKey(key: string): Promise<string | null> {
|
||||
const keyHash = createHash('sha256').update(key).digest('hex');
|
||||
const [apiKey] = await db.select().from(apiKeys).where(eq(apiKeys.keyHash, keyHash));
|
||||
if (!apiKey || apiKey.expiresAt < new Date()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decryptedKey = CryptoService.decrypt(apiKey.key);
|
||||
if (decryptedKey !== key) {
|
||||
// This should not happen if the hash matches, but as a security measure, we double-check.
|
||||
return null;
|
||||
}
|
||||
const decryptedKey = CryptoService.decrypt(apiKey.key);
|
||||
if (decryptedKey !== key) {
|
||||
// This should not happen if the hash matches, but as a security measure, we double-check.
|
||||
return null;
|
||||
}
|
||||
|
||||
return apiKey.userId;
|
||||
}
|
||||
return apiKey.userId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
"dependencies": {
|
||||
"@iconify/svelte": "^5.0.1",
|
||||
"@open-archiver/types": "workspace:*",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/kit": "^2.38.1",
|
||||
"bits-ui": "^2.8.10",
|
||||
"clsx": "^2.1.1",
|
||||
"d3-shape": "^3.2.0",
|
||||
"jose": "^6.0.1",
|
||||
"lucide-svelte": "^0.525.0",
|
||||
"postal-mime": "^2.4.4",
|
||||
"semver": "^7.7.2",
|
||||
"svelte-persisted-store": "^0.12.0",
|
||||
"sveltekit-i18n": "^2.4.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
@@ -35,6 +36,7 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/semver": "^7.7.1",
|
||||
"dotenv": "^17.2.0",
|
||||
"layerchart": "2.0.0-next.27",
|
||||
"mode-watcher": "^1.1.0",
|
||||
|
||||
@@ -52,16 +52,16 @@
|
||||
|
||||
<div class="mt-2 rounded-md border bg-white p-4">
|
||||
{#if isLoading}
|
||||
<p>{$t('components.email_preview.loading')}</p>
|
||||
<p>{$t('app.components.email_preview.loading')}</p>
|
||||
{:else if emailHtml}
|
||||
<iframe
|
||||
title={$t('archive.email_preview')}
|
||||
title={$t('app.archive.email_preview')}
|
||||
srcdoc={emailHtml()}
|
||||
class="h-[600px] w-full border-none"
|
||||
></iframe>
|
||||
{:else if raw}
|
||||
<p>{$t('components.email_preview.render_error')}</p>
|
||||
<p>{$t('app.components.email_preview.render_error')}</p>
|
||||
{:else}
|
||||
<p class="text-gray-500">{$t('components.email_preview.not_available')}</p>
|
||||
<p class="text-gray-500">{$t('app.components.email_preview.not_available')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/translations';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { Info } from 'lucide-svelte';
|
||||
|
||||
export let currentVersion: string;
|
||||
export let newVersionInfo: { version: string; description: string; url: string } | null = null;
|
||||
</script>
|
||||
|
||||
<footer class="bg-muted py-6 md:py-0">
|
||||
<div
|
||||
class="container mx-auto flex flex-col items-center justify-center gap-4 md:h-24 md:flex-row"
|
||||
>
|
||||
<div class="container mx-auto flex flex-col items-center justify-center gap-4 py-8 md:flex-row">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
{#if newVersionInfo}
|
||||
<Alert.Root>
|
||||
<Alert.Title class="flex items-center gap-2">
|
||||
<Info class="h-4 w-4" />
|
||||
{$t('app.components.footer.new_version_available')}
|
||||
<a
|
||||
href={newVersionInfo.url}
|
||||
target="_blank"
|
||||
class=" text-muted-foreground underline"
|
||||
>
|
||||
{newVersionInfo.description}
|
||||
</a>
|
||||
</Alert.Title>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
<p class="text-balance text-center text-xs font-medium leading-loose">
|
||||
© {new Date().getFullYear()}
|
||||
<a href="https://openarchiver.com/" target="_blank">Open Archiver</a>. {$t(
|
||||
'app.components.footer.all_rights_reserved'
|
||||
)}
|
||||
</p>
|
||||
<p class="text-balance text-center text-xs font-medium leading-loose">
|
||||
Version: {currentVersion}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -163,7 +163,8 @@
|
||||
"not_available": "Raw .eml file not available for this email."
|
||||
},
|
||||
"footer": {
|
||||
"all_rights_reserved": "All rights reserved."
|
||||
"all_rights_reserved": "All rights reserved.",
|
||||
"new_version_available": "New version available"
|
||||
},
|
||||
"ingestion_source_form": {
|
||||
"provider_generic_imap": "Generic IMAP",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"app": {
|
||||
"auth": {
|
||||
"login": "Accesso",
|
||||
"login": "Accedi",
|
||||
"login_tip": "Inserisci la tua email qui sotto per accedere al tuo account.",
|
||||
"email": "Email",
|
||||
"password": "Password"
|
||||
},
|
||||
"common": {
|
||||
"working": "In lavorazione"
|
||||
"working": "In corso"
|
||||
},
|
||||
"archive": {
|
||||
"title": "Archivio",
|
||||
"no_subject": "Nessun oggetto",
|
||||
"no_subject": "Nessun Oggetto",
|
||||
"from": "Da",
|
||||
"sent": "Inviato",
|
||||
"recipients": "Destinatari",
|
||||
@@ -20,27 +20,27 @@
|
||||
"folder": "Cartella",
|
||||
"tags": "Tag",
|
||||
"size": "Dimensione",
|
||||
"email_preview": "Anteprima email",
|
||||
"email_preview": "Anteprima Email",
|
||||
"attachments": "Allegati",
|
||||
"download": "Scarica",
|
||||
"actions": "Azioni",
|
||||
"download_eml": "Scarica email (.eml)",
|
||||
"delete_email": "Elimina email",
|
||||
"email_thread": "Thread email",
|
||||
"download_eml": "Scarica Email (.eml)",
|
||||
"delete_email": "Elimina Email",
|
||||
"email_thread": "Thread Email",
|
||||
"delete_confirmation_title": "Sei sicuro di voler eliminare questa email?",
|
||||
"delete_confirmation_description": "Questa azione non può essere annullata ed eliminerà permanentemente l'email e i suoi allegati.",
|
||||
"delete_confirmation_description": "Questa azione non può essere annullata e rimuoverà permanentemente l'email e i suoi allegati.",
|
||||
"deleting": "Eliminazione in corso",
|
||||
"confirm": "Conferma",
|
||||
"cancel": "Annulla",
|
||||
"not_found": "Email non trovata."
|
||||
},
|
||||
"ingestions": {
|
||||
"title": "Fonti di ingestione",
|
||||
"ingestion_sources": "Fonti di ingestione",
|
||||
"bulk_actions": "Azioni di massa",
|
||||
"force_sync": "Forza sincronizzazione",
|
||||
"title": "Sorgenti di Ingestione",
|
||||
"ingestion_sources": "Sorgenti di Ingestione",
|
||||
"bulk_actions": "Azioni di Massa",
|
||||
"force_sync": "Forza Sincronizzazione",
|
||||
"delete": "Elimina",
|
||||
"create_new": "Crea nuovo",
|
||||
"create_new": "Crea Nuovo",
|
||||
"name": "Nome",
|
||||
"provider": "Provider",
|
||||
"status": "Stato",
|
||||
@@ -52,28 +52,28 @@
|
||||
"open_menu": "Apri menu",
|
||||
"edit": "Modifica",
|
||||
"create": "Crea",
|
||||
"ingestion_source": "Fonte di ingestione",
|
||||
"edit_description": "Apporta modifiche alla tua fonte di ingestione qui.",
|
||||
"create_description": "Aggiungi una nuova fonte di ingestione per iniziare ad archiviare le email.",
|
||||
"ingestion_source": "Sorgente di Ingestione",
|
||||
"edit_description": "Apporta modifiche alla tua sorgente di ingestione qui.",
|
||||
"create_description": "Aggiungi una nuova sorgente di ingestione per iniziare ad archiviare le email.",
|
||||
"read": "Leggi",
|
||||
"docs_here": "documenti qui",
|
||||
"delete_confirmation_title": "Sei sicuro di voler eliminare questa ingestione?",
|
||||
"delete_confirmation_description": "Questo eliminerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a questa ingestione. Se desideri solo interrompere la sincronizzazione di nuove email, puoi invece mettere in pausa l'ingestione.",
|
||||
"delete_confirmation_description": "Questo cancellerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a questa ingestione. Se vuoi solo interrompere la sincronizzazione di nuove email, puoi mettere in pausa l'ingestione.",
|
||||
"deleting": "Eliminazione in corso",
|
||||
"confirm": "Conferma",
|
||||
"cancel": "Annulla",
|
||||
"bulk_delete_confirmation_title": "Sei sicuro di voler eliminare {{count}} ingestioni selezionate?",
|
||||
"bulk_delete_confirmation_description": "Questo eliminerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a queste ingestioni. Se desideri solo interrompere la sincronizzazione di nuove email, puoi invece mettere in pausa le ingestioni."
|
||||
"bulk_delete_confirmation_description": "Questo cancellerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a queste ingestioni. Se vuoi solo interrompere la sincronizzazione di nuove email, puoi mettere in pausa le ingestioni."
|
||||
},
|
||||
"search": {
|
||||
"title": "Cerca",
|
||||
"description": "Cerca email archiviate.",
|
||||
"email_search": "Ricerca email",
|
||||
"title": "Ricerca",
|
||||
"description": "Ricerca email archiviate.",
|
||||
"email_search": "Ricerca Email",
|
||||
"placeholder": "Cerca per parola chiave, mittente, destinatario...",
|
||||
"search_button": "Cerca",
|
||||
"search_options": "Opzioni di ricerca",
|
||||
"strategy_fuzzy": "Fuzzy",
|
||||
"strategy_verbatim": "Verbatim",
|
||||
"strategy_fuzzy": "Approssimativa",
|
||||
"strategy_verbatim": "Esatta",
|
||||
"strategy_frequency": "Frequenza",
|
||||
"select_strategy": "Seleziona una strategia",
|
||||
"error": "Errore",
|
||||
@@ -87,18 +87,18 @@
|
||||
"next": "Succ"
|
||||
},
|
||||
"roles": {
|
||||
"title": "Gestione ruoli",
|
||||
"role_management": "Gestione ruoli",
|
||||
"create_new": "Crea nuovo",
|
||||
"title": "Gestione Ruoli",
|
||||
"role_management": "Gestione Ruoli",
|
||||
"create_new": "Crea Nuovo",
|
||||
"name": "Nome",
|
||||
"created_at": "Creato il",
|
||||
"actions": "Azioni",
|
||||
"open_menu": "Apri menu",
|
||||
"view_policy": "Visualizza policy",
|
||||
"view_policy": "Visualizza Policy",
|
||||
"edit": "Modifica",
|
||||
"delete": "Elimina",
|
||||
"no_roles_found": "Nessun ruolo trovato.",
|
||||
"role_policy": "Policy ruolo",
|
||||
"role_policy": "Policy Ruolo",
|
||||
"viewing_policy_for_role": "Visualizzazione policy per il ruolo: {{name}}",
|
||||
"create": "Crea",
|
||||
"role": "Ruolo",
|
||||
@@ -111,22 +111,22 @@
|
||||
"cancel": "Annulla"
|
||||
},
|
||||
"system_settings": {
|
||||
"title": "Impostazioni di sistema",
|
||||
"system_settings": "Impostazioni di sistema",
|
||||
"title": "Impostazioni di Sistema",
|
||||
"system_settings": "Impostazioni di Sistema",
|
||||
"description": "Gestisci le impostazioni globali dell'applicazione.",
|
||||
"language": "Lingua",
|
||||
"default_theme": "Tema predefinito",
|
||||
"light": "Chiaro",
|
||||
"dark": "Scuro",
|
||||
"system": "Sistema",
|
||||
"support_email": "Email di supporto",
|
||||
"saving": "Salvataggio",
|
||||
"save_changes": "Salva modifiche"
|
||||
"support_email": "Email di Supporto",
|
||||
"saving": "Salvataggio in corso",
|
||||
"save_changes": "Salva Modifiche"
|
||||
},
|
||||
"users": {
|
||||
"title": "Gestione utenti",
|
||||
"user_management": "Gestione utenti",
|
||||
"create_new": "Crea nuovo",
|
||||
"title": "Gestione Utenti",
|
||||
"user_management": "Gestione Utenti",
|
||||
"create_new": "Crea Nuovo",
|
||||
"name": "Nome",
|
||||
"email": "Email",
|
||||
"role": "Ruolo",
|
||||
@@ -146,33 +146,10 @@
|
||||
"confirm": "Conferma",
|
||||
"cancel": "Annulla"
|
||||
},
|
||||
"setup": {
|
||||
"title": "Configurazione",
|
||||
"description": "Configura l'account amministratore iniziale per Open Archiver.",
|
||||
"welcome": "Benvenuto",
|
||||
"create_admin_account": "Crea il primo account amministratore per iniziare.",
|
||||
"first_name": "Nome",
|
||||
"last_name": "Cognome",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"creating_account": "Creazione account",
|
||||
"create_account": "Crea account"
|
||||
},
|
||||
"layout": {
|
||||
"dashboard": "Dashboard",
|
||||
"ingestions": "Ingestioni",
|
||||
"archived_emails": "Email archiviate",
|
||||
"search": "Cerca",
|
||||
"settings": "Impostazioni",
|
||||
"system": "Sistema",
|
||||
"users": "Utenti",
|
||||
"roles": "Ruoli",
|
||||
"logout": "Esci"
|
||||
},
|
||||
"components": {
|
||||
"charts": {
|
||||
"emails_ingested": "Email ingerite",
|
||||
"storage_used": "Spazio di archiviazione utilizzato",
|
||||
"emails_ingested": "Email Acquisite",
|
||||
"storage_used": "Spazio di Archiviazione Utilizzato",
|
||||
"emails": "Email"
|
||||
},
|
||||
"common": {
|
||||
@@ -182,35 +159,36 @@
|
||||
},
|
||||
"email_preview": {
|
||||
"loading": "Caricamento anteprima email...",
|
||||
"render_error": "Impossibile visualizzare l'anteprima dell'email.",
|
||||
"not_available": "File .eml non disponibile per questa email."
|
||||
"render_error": "Impossibile renderizzare l'anteprima dell'email.",
|
||||
"not_available": "File .eml grezzo non disponibile per questa email."
|
||||
},
|
||||
"footer": {
|
||||
"all_rights_reserved": "Tutti i diritti riservati."
|
||||
},
|
||||
"ingestion_source_form": {
|
||||
"provider_generic_imap": "IMAP generico",
|
||||
"provider_generic_imap": "IMAP Generico",
|
||||
"provider_google_workspace": "Google Workspace",
|
||||
"provider_microsoft_365": "Microsoft 365",
|
||||
"provider_pst_import": "Importazione PST",
|
||||
"provider_eml_import": "Importazione EML",
|
||||
"select_provider": "Seleziona un provider",
|
||||
"service_account_key": "Chiave account di servizio (JSON)",
|
||||
"service_account_key_placeholder": "Incolla il contenuto JSON della tua chiave account di servizio",
|
||||
"impersonated_admin_email": "Email amministratore impersonata",
|
||||
"client_id": "ID applicazione (client)",
|
||||
"client_secret": "Valore segreto client",
|
||||
"client_secret_placeholder": "Inserisci il valore segreto, non l'ID segreto",
|
||||
"tenant_id": "ID directory (tenant)",
|
||||
"service_account_key": "Chiave Account di Servizio (JSON)",
|
||||
"service_account_key_placeholder": "Incolla il contenuto JSON della chiave del tuo account di servizio",
|
||||
"impersonated_admin_email": "Email dell'Amministratore Impersonato",
|
||||
"client_id": "ID Applicazione (Client)",
|
||||
"client_secret": "Valore Segreto Client",
|
||||
"client_secret_placeholder": "Inserisci il Valore segreto, non l'ID Segreto",
|
||||
"tenant_id": "ID Directory (Tenant)",
|
||||
"host": "Host",
|
||||
"port": "Porta",
|
||||
"username": "Nome utente",
|
||||
"username": "Nome Utente",
|
||||
"use_tls": "Usa TLS",
|
||||
"allow_insecure_cert": "Consenti certificato non sicuro",
|
||||
"pst_file": "File PST",
|
||||
"eml_file": "File EML",
|
||||
"heads_up": "Attenzione!",
|
||||
"org_wide_warning": "Si prega di notare che questa è un'operazione a livello di organizzazione. Questo tipo di ingestione importerà e indicizzerà <b>tutte</b> le caselle di posta elettronica della tua organizzazione. Se desideri importare solo caselle di posta elettronica specifiche, utilizza il connettore IMAP.",
|
||||
"upload_failed": "Caricamento non riuscito, riprova"
|
||||
"org_wide_warning": "Si prega di notare che questa è un'operazione a livello di organizzazione. Questo tipo di ingestione importerà e indicizzerà <b>tutte</b> le caselle di posta elettronica nella tua organizzazione. Se vuoi importare solo caselle di posta elettronica specifiche, usa il connettore IMAP.",
|
||||
"upload_failed": "Caricamento Fallito, riprova"
|
||||
},
|
||||
"role_form": {
|
||||
"policies_json": "Policy (JSON)",
|
||||
@@ -223,28 +201,61 @@
|
||||
"select_role": "Seleziona un ruolo"
|
||||
}
|
||||
},
|
||||
"dashboard_page": {
|
||||
"title": "Dashboard",
|
||||
"meta_description": "Panoramica del tuo archivio email.",
|
||||
"header": "Dashboard",
|
||||
"create_ingestion": "Crea un'ingestione",
|
||||
"no_ingestion_header": "Non hai alcuna fonte di ingestione configurata.",
|
||||
"no_ingestion_text": "Aggiungi una fonte di ingestione per iniziare ad archiviare le tue caselle di posta.",
|
||||
"total_emails_archived": "Email totali archiviate",
|
||||
"total_storage_used": "Spazio di archiviazione totale utilizzato",
|
||||
"failed_ingestions": "Ingestioni non riuscite (ultimi 7 giorni)",
|
||||
"ingestion_history": "Cronologia ingestioni",
|
||||
"no_ingestion_history": "Nessuna cronologia di ingestione disponibile.",
|
||||
"storage_by_source": "Archiviazione per fonte di ingestione",
|
||||
"no_ingestion_sources": "Nessuna fonte di ingestione disponibile.",
|
||||
"indexed_insights": "Approfondimenti indicizzati",
|
||||
"top_10_senders": "Top 10 mittenti",
|
||||
"no_indexed_insights": "Nessun approfondimento indicizzato disponibile."
|
||||
"setup": {
|
||||
"title": "Configurazione",
|
||||
"description": "Configura l'account amministratore iniziale per Open Archiver.",
|
||||
"welcome": "Benvenuto",
|
||||
"create_admin_account": "Crea il primo account amministratore per iniziare.",
|
||||
"first_name": "Nome",
|
||||
"last_name": "Cognome",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"creating_account": "Creazione Account",
|
||||
"create_account": "Crea Account"
|
||||
},
|
||||
"layout": {
|
||||
"dashboard": "Dashboard",
|
||||
"ingestions": "Ingestioni",
|
||||
"archived_emails": "Email archiviate",
|
||||
"search": "Ricerca",
|
||||
"settings": "Impostazioni",
|
||||
"system": "Sistema",
|
||||
"users": "Utenti",
|
||||
"roles": "Ruoli",
|
||||
"api_keys": "Chiavi API",
|
||||
"logout": "Esci"
|
||||
},
|
||||
"api_keys_page": {
|
||||
"title": "Chiavi API",
|
||||
"header": "Chiavi API",
|
||||
"generate_new_key": "Genera Nuova Chiave",
|
||||
"name": "Nome",
|
||||
"key": "Chiave",
|
||||
"expires_at": "Scade il",
|
||||
"created_at": "Creato il",
|
||||
"actions": "Azioni",
|
||||
"delete": "Elimina",
|
||||
"no_keys_found": "Nessuna chiave API trovata.",
|
||||
"generate_modal_title": "Genera Nuova Chiave API",
|
||||
"generate_modal_description": "Fornisci un nome e una scadenza per la tua nuova chiave API.",
|
||||
"expires_in": "Scade Tra",
|
||||
"select_expiration": "Seleziona una scadenza",
|
||||
"30_days": "30 Giorni",
|
||||
"60_days": "60 Giorni",
|
||||
"6_months": "6 Mesi",
|
||||
"12_months": "12 Mesi",
|
||||
"24_months": "24 Mesi",
|
||||
"generate": "Genera",
|
||||
"new_api_key": "Nuova Chiave API",
|
||||
"failed_to_delete": "Impossibile eliminare la chiave API",
|
||||
"api_key_deleted": "Chiave API eliminata",
|
||||
"generated_title": "Chiave API Generata",
|
||||
"generated_message": "La tua chiave API è stata generata, per favore copiala e salvala in un luogo sicuro. Questa chiave verrà mostrata solo una volta."
|
||||
},
|
||||
"archived_emails_page": {
|
||||
"title": "Email archiviate",
|
||||
"header": "Email archiviate",
|
||||
"select_ingestion_source": "Seleziona una fonte di ingestione",
|
||||
"header": "Email Archiviate",
|
||||
"select_ingestion_source": "Seleziona una sorgente di ingestione",
|
||||
"date": "Data",
|
||||
"subject": "Oggetto",
|
||||
"sender": "Mittente",
|
||||
@@ -255,6 +266,24 @@
|
||||
"no_emails_found": "Nessuna email archiviata trovata.",
|
||||
"prev": "Prec",
|
||||
"next": "Succ"
|
||||
},
|
||||
"dashboard_page": {
|
||||
"title": "Dashboard",
|
||||
"meta_description": "Panoramica del tuo archivio email.",
|
||||
"header": "Dashboard",
|
||||
"create_ingestion": "Crea un'ingestione",
|
||||
"no_ingestion_header": "Non hai impostato nessuna sorgente di ingestione.",
|
||||
"no_ingestion_text": "Aggiungi una sorgente di ingestione per iniziare ad archiviare le tue caselle di posta.",
|
||||
"total_emails_archived": "Totale Email Archiviate",
|
||||
"total_storage_used": "Spazio di Archiviazione Totale Utilizzato",
|
||||
"failed_ingestions": "Ingestioni Fallite (Ultimi 7 Giorni)",
|
||||
"ingestion_history": "Cronologia Ingestioni",
|
||||
"no_ingestion_history": "Nessuna cronologia delle ingestioni disponibile.",
|
||||
"storage_by_source": "Spazio di Archiviazione per Sorgente di Ingestione",
|
||||
"no_ingestion_sources": "Nessuna sorgente di ingestione disponibile.",
|
||||
"indexed_insights": "Approfondimenti indicizzati",
|
||||
"top_10_senders": "I 10 Mittenti Principali",
|
||||
"no_indexed_insights": "Nessun approfondimento indicizzato disponibile."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ import type { LayoutServerLoad } from './$types';
|
||||
import 'dotenv/config';
|
||||
import { api } from '$lib/server/api';
|
||||
import type { SystemSettings } from '@open-archiver/types';
|
||||
import { version } from '../../../../package.json';
|
||||
import semver from 'semver';
|
||||
|
||||
let newVersionInfo: { version: string; description: string; url: string } | null = null;
|
||||
let lastChecked: Date | null = null;
|
||||
|
||||
export const load: LayoutServerLoad = async (event) => {
|
||||
const { locals, url } = event;
|
||||
@@ -32,10 +37,35 @@ export const load: LayoutServerLoad = async (event) => {
|
||||
? await systemSettingsResponse.json()
|
||||
: null;
|
||||
|
||||
const now = new Date();
|
||||
if (!lastChecked || now.getTime() - lastChecked.getTime() > 1000 * 60 * 60) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
'https://api.github.com/repos/LogicLabs-OU/OpenArchiver/releases/latest'
|
||||
);
|
||||
if (res.ok) {
|
||||
const latestRelease = await res.json();
|
||||
const latestVersion = latestRelease.tag_name.replace('v', '');
|
||||
if (semver.gt(latestVersion, version)) {
|
||||
newVersionInfo = {
|
||||
version: latestVersion,
|
||||
description: latestRelease.name,
|
||||
url: latestRelease.html_url,
|
||||
};
|
||||
}
|
||||
}
|
||||
lastChecked = now;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch latest version from GitHub:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user: locals.user,
|
||||
accessToken: locals.accessToken,
|
||||
isDemo: process.env.IS_DEMO === 'true',
|
||||
systemSettings,
|
||||
currentVersion: version,
|
||||
newVersionInfo: newVersionInfo,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
<main class="flex-1">
|
||||
{@render children()}
|
||||
</main>
|
||||
<Footer />
|
||||
<Footer currentVersion={data.currentVersion} newVersionInfo={data.newVersionInfo} />
|
||||
</div>
|
||||
|
||||
@@ -2,49 +2,48 @@ import { api } from '$lib/server/api';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const response = await api('/api-keys', event);
|
||||
const apiKeys = await response.json();
|
||||
const response = await api('/api-keys', event);
|
||||
const apiKeys = await response.json();
|
||||
|
||||
return {
|
||||
apiKeys,
|
||||
};
|
||||
return {
|
||||
apiKeys,
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
generate: async (event) => {
|
||||
const data = await event.request.formData();
|
||||
const name = data.get('name') as string;
|
||||
const expiresInDays = Number(data.get('expiresInDays'));
|
||||
generate: async (event) => {
|
||||
const data = await event.request.formData();
|
||||
const name = data.get('name') as string;
|
||||
const expiresInDays = Number(data.get('expiresInDays'));
|
||||
|
||||
const response = await api('/api-keys', event, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, expiresInDays }),
|
||||
});
|
||||
const response = await api('/api-keys', event, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, expiresInDays }),
|
||||
});
|
||||
|
||||
const responseBody = await response.json();
|
||||
const responseBody = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
message: responseBody.message || '',
|
||||
errors: responseBody.errors
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
return {
|
||||
message: responseBody.message || '',
|
||||
errors: responseBody.errors,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
newApiKey: responseBody.key,
|
||||
};
|
||||
},
|
||||
delete: async (event) => {
|
||||
const data = await event.request.formData();
|
||||
const id = data.get('id') as string;
|
||||
|
||||
return {
|
||||
newApiKey: responseBody.key,
|
||||
};
|
||||
},
|
||||
delete: async (event) => {
|
||||
const data = await event.request.formData();
|
||||
const id = data.get('id') as string;
|
||||
await api(`/api-keys/${id}`, event, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
await api(`/api-keys/${id}`, event, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
470
pnpm-lock.yaml
generated
470
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user