Merge pull request #2 from tiquis0290/test-backup
Resolving conflicts with dev
This commit is contained in:
@@ -379,12 +379,9 @@ router.post('/rotate-jwt-secret', (req: Request, res: Response) => {
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
const authReq = req as AuthRequest;
|
||||
writeAudit({
|
||||
user_id: authReq.user?.id ?? null,
|
||||
username: authReq.user?.username ?? 'unknown',
|
||||
userId: authReq.user?.id ?? null,
|
||||
action: 'admin.rotate_jwt_secret',
|
||||
target_type: 'system',
|
||||
target_id: null,
|
||||
details: null,
|
||||
resource: 'system',
|
||||
ip: getClientIp(req),
|
||||
});
|
||||
res.json({ success: true });
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
const router = express.Router();
|
||||
|
||||
// ── Dual auth middleware (JWT or ephemeral token for <img> src) ─────────────
|
||||
|
||||
function authFromQuery(req: Request, res: Response, next: NextFunction) {
|
||||
const queryToken = req.query.token as string | undefined;
|
||||
if (queryToken) {
|
||||
@@ -178,7 +177,6 @@ router.get('/albums', authenticate, async (req: Request, res: Response) => {
|
||||
res.status(502).json({ error: 'Could not reach Immich' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/trips/:tripId/album-links', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
|
||||
@@ -465,17 +465,92 @@ export function deleteTemplateItem(itemId: string) {
|
||||
|
||||
export function listAddons() {
|
||||
const addons = db.prepare('SELECT * FROM addons ORDER BY sort_order, id').all() as Addon[];
|
||||
return addons.map(a => ({ ...a, enabled: !!a.enabled, config: JSON.parse(a.config || '{}') }));
|
||||
const providers = db.prepare(`
|
||||
SELECT id, name, description, icon, enabled, config, sort_order
|
||||
FROM photo_providers
|
||||
ORDER BY sort_order, id
|
||||
`).all() as Array<{ id: string; name: string; description?: string | null; icon: string; enabled: number; config: string; sort_order: number }>;
|
||||
const fields = db.prepare(`
|
||||
SELECT provider_id, field_key, label, input_type, placeholder, required, secret, settings_key, payload_key, sort_order
|
||||
FROM photo_provider_fields
|
||||
ORDER BY sort_order, id
|
||||
`).all() as Array<{
|
||||
provider_id: string;
|
||||
field_key: string;
|
||||
label: string;
|
||||
input_type: string;
|
||||
placeholder?: string | null;
|
||||
required: number;
|
||||
secret: number;
|
||||
settings_key?: string | null;
|
||||
payload_key?: string | null;
|
||||
sort_order: number;
|
||||
}>;
|
||||
const fieldsByProvider = new Map<string, typeof fields>();
|
||||
for (const field of fields) {
|
||||
const arr = fieldsByProvider.get(field.provider_id) || [];
|
||||
arr.push(field);
|
||||
fieldsByProvider.set(field.provider_id, arr);
|
||||
}
|
||||
|
||||
return [
|
||||
...addons.map(a => ({ ...a, enabled: !!a.enabled, config: JSON.parse(a.config || '{}') })),
|
||||
...providers.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
type: 'photo_provider',
|
||||
icon: p.icon,
|
||||
enabled: !!p.enabled,
|
||||
config: JSON.parse(p.config || '{}'),
|
||||
fields: (fieldsByProvider.get(p.id) || []).map(f => ({
|
||||
key: f.field_key,
|
||||
label: f.label,
|
||||
input_type: f.input_type,
|
||||
placeholder: f.placeholder || '',
|
||||
required: !!f.required,
|
||||
secret: !!f.secret,
|
||||
settings_key: f.settings_key || null,
|
||||
payload_key: f.payload_key || null,
|
||||
sort_order: f.sort_order,
|
||||
})),
|
||||
sort_order: p.sort_order,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
export function updateAddon(id: string, data: { enabled?: boolean; config?: Record<string, unknown> }) {
|
||||
const addon = db.prepare('SELECT * FROM addons WHERE id = ?').get(id);
|
||||
if (!addon) return { error: 'Addon not found', status: 404 };
|
||||
if (data.enabled !== undefined) db.prepare('UPDATE addons SET enabled = ? WHERE id = ?').run(data.enabled ? 1 : 0, id);
|
||||
if (data.config !== undefined) db.prepare('UPDATE addons SET config = ? WHERE id = ?').run(JSON.stringify(data.config), id);
|
||||
const updated = db.prepare('SELECT * FROM addons WHERE id = ?').get(id) as Addon;
|
||||
const addon = db.prepare('SELECT * FROM addons WHERE id = ?').get(id) as Addon | undefined;
|
||||
const provider = db.prepare('SELECT * FROM photo_providers WHERE id = ?').get(id) as { id: string; name: string; description?: string | null; icon: string; enabled: number; config: string; sort_order: number } | undefined;
|
||||
if (!addon && !provider) return { error: 'Addon not found', status: 404 };
|
||||
|
||||
if (addon) {
|
||||
if (data.enabled !== undefined) db.prepare('UPDATE addons SET enabled = ? WHERE id = ?').run(data.enabled ? 1 : 0, id);
|
||||
if (data.config !== undefined) db.prepare('UPDATE addons SET config = ? WHERE id = ?').run(JSON.stringify(data.config), id);
|
||||
} else {
|
||||
if (data.enabled !== undefined) db.prepare('UPDATE photo_providers SET enabled = ? WHERE id = ?').run(data.enabled ? 1 : 0, id);
|
||||
if (data.config !== undefined) db.prepare('UPDATE photo_providers SET config = ? WHERE id = ?').run(JSON.stringify(data.config), id);
|
||||
}
|
||||
|
||||
const updatedAddon = db.prepare('SELECT * FROM addons WHERE id = ?').get(id) as Addon | undefined;
|
||||
const updatedProvider = db.prepare('SELECT * FROM photo_providers WHERE id = ?').get(id) as { id: string; name: string; description?: string | null; icon: string; enabled: number; config: string; sort_order: number } | undefined;
|
||||
const updated = updatedAddon
|
||||
? { ...updatedAddon, enabled: !!updatedAddon.enabled, config: JSON.parse(updatedAddon.config || '{}') }
|
||||
: updatedProvider
|
||||
? {
|
||||
id: updatedProvider.id,
|
||||
name: updatedProvider.name,
|
||||
description: updatedProvider.description,
|
||||
type: 'photo_provider',
|
||||
icon: updatedProvider.icon,
|
||||
enabled: !!updatedProvider.enabled,
|
||||
config: JSON.parse(updatedProvider.config || '{}'),
|
||||
sort_order: updatedProvider.sort_order,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
addon: { ...updated, enabled: !!updated.enabled, config: JSON.parse(updated.config || '{}') },
|
||||
addon: updated,
|
||||
auditDetails: { enabled: data.enabled !== undefined ? !!data.enabled : undefined, config_changed: data.config !== undefined },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -981,7 +981,7 @@ export function createWsToken(userId: number): { error?: string; status?: number
|
||||
}
|
||||
|
||||
export function createResourceToken(userId: number, purpose?: string): { error?: string; status?: number; token?: string } {
|
||||
if (purpose !== 'download' && purpose !== 'immich') {
|
||||
if (purpose !== 'download' && purpose !== 'immich' && purpose !== 'synologyphotos') {
|
||||
return { error: 'Invalid purpose', status: 400 };
|
||||
}
|
||||
const token = createEphemeralToken(userId, purpose);
|
||||
|
||||
@@ -175,11 +175,12 @@ export async function searchPhotos(
|
||||
|
||||
export function listTripPhotos(tripId: string, userId: number) {
|
||||
return db.prepare(`
|
||||
SELECT tp.immich_asset_id, tp.user_id, tp.shared, tp.added_at,
|
||||
SELECT tp.asset_id AS immich_asset_id, tp.user_id, tp.shared, tp.added_at,
|
||||
u.username, u.avatar, u.immich_url
|
||||
FROM trip_photos tp
|
||||
JOIN users u ON tp.user_id = u.id
|
||||
WHERE tp.trip_id = ?
|
||||
AND tp.provider = 'immich'
|
||||
AND (tp.user_id = ? OR tp.shared = 1)
|
||||
ORDER BY tp.added_at ASC
|
||||
`).all(tripId, userId);
|
||||
@@ -191,25 +192,23 @@ export function addTripPhotos(
|
||||
assetIds: string[],
|
||||
shared: boolean
|
||||
): number {
|
||||
const insert = db.prepare(
|
||||
'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, immich_asset_id, shared) VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
const insert = db.prepare('INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared) VALUES (?, ?, ?, ?, ?)');
|
||||
let added = 0;
|
||||
for (const assetId of assetIds) {
|
||||
const result = insert.run(tripId, userId, assetId, shared ? 1 : 0);
|
||||
const result = insert.run(tripId, userId, assetId, 'immich', shared ? 1 : 0);
|
||||
if (result.changes > 0) added++;
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
export function removeTripPhoto(tripId: string, userId: number, assetId: string) {
|
||||
db.prepare('DELETE FROM trip_photos WHERE trip_id = ? AND user_id = ? AND immich_asset_id = ?')
|
||||
.run(tripId, userId, assetId);
|
||||
db.prepare('DELETE FROM trip_photos WHERE trip_id = ? AND user_id = ? AND asset_id = ? AND provider = ?')
|
||||
.run(tripId, userId, assetId, 'immich');
|
||||
}
|
||||
|
||||
export function togglePhotoSharing(tripId: string, userId: number, assetId: string, shared: boolean) {
|
||||
db.prepare('UPDATE trip_photos SET shared = ? WHERE trip_id = ? AND user_id = ? AND immich_asset_id = ?')
|
||||
.run(shared ? 1 : 0, tripId, userId, assetId);
|
||||
db.prepare('UPDATE trip_photos SET shared = ? WHERE trip_id = ? AND user_id = ? AND asset_id = ? AND provider = ?')
|
||||
.run(shared ? 1 : 0, tripId, userId, assetId, 'immich');
|
||||
}
|
||||
|
||||
// ── Asset Info / Proxy ─────────────────────────────────────────────────────
|
||||
@@ -329,7 +328,7 @@ export function listAlbumLinks(tripId: string) {
|
||||
SELECT tal.*, u.username
|
||||
FROM trip_album_links tal
|
||||
JOIN users u ON tal.user_id = u.id
|
||||
WHERE tal.trip_id = ?
|
||||
WHERE tal.trip_id = ? AND tal.provider = 'immich'
|
||||
ORDER BY tal.created_at ASC
|
||||
`).all(tripId);
|
||||
}
|
||||
@@ -342,8 +341,8 @@ export function createAlbumLink(
|
||||
): { success: boolean; error?: string } {
|
||||
try {
|
||||
db.prepare(
|
||||
'INSERT OR IGNORE INTO trip_album_links (trip_id, user_id, immich_album_id, album_name) VALUES (?, ?, ?, ?)'
|
||||
).run(tripId, userId, albumId, albumName || '');
|
||||
'INSERT OR IGNORE INTO trip_album_links (trip_id, user_id, provider, album_id, album_name) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(tripId, userId, 'immich', albumId, albumName || '');
|
||||
return { success: true };
|
||||
} catch {
|
||||
return { success: false, error: 'Album already linked' };
|
||||
@@ -360,15 +359,15 @@ export async function syncAlbumAssets(
|
||||
linkId: string,
|
||||
userId: number
|
||||
): Promise<{ success?: boolean; added?: number; total?: number; error?: string; status?: number }> {
|
||||
const link = db.prepare('SELECT * FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?')
|
||||
.get(linkId, tripId, userId) as any;
|
||||
const link = db.prepare('SELECT * FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ? AND provider = ?')
|
||||
.get(linkId, tripId, userId, 'immich') as any;
|
||||
if (!link) return { error: 'Album link not found', status: 404 };
|
||||
|
||||
const creds = getImmichCredentials(userId);
|
||||
if (!creds) return { error: 'Immich not configured', status: 400 };
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${creds.immich_url}/api/albums/${link.immich_album_id}`, {
|
||||
const resp = await fetch(`${creds.immich_url}/api/albums/${link.album_id}`, {
|
||||
headers: { 'x-api-key': creds.immich_api_key, 'Accept': 'application/json' },
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
@@ -376,9 +375,7 @@ export async function syncAlbumAssets(
|
||||
const albumData = await resp.json() as { assets?: any[] };
|
||||
const assets = (albumData.assets || []).filter((a: any) => a.type === 'IMAGE');
|
||||
|
||||
const insert = db.prepare(
|
||||
'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, immich_asset_id, shared) VALUES (?, ?, ?, 1)'
|
||||
);
|
||||
const insert = db.prepare("INSERT OR IGNORE INTO trip_photos (trip_id, user_id, asset_id, provider, shared) VALUES (?, ?, ?, 'immich', 1)");
|
||||
let added = 0;
|
||||
for (const asset of assets) {
|
||||
const r = insert.run(tripId, userId, asset.id);
|
||||
|
||||
@@ -44,6 +44,7 @@ function getWebhookUrl(): string | null {
|
||||
}
|
||||
|
||||
function getAppUrl(): string {
|
||||
if (process.env.APP_URL) return process.env.APP_URL;
|
||||
const origins = process.env.ALLOWED_ORIGINS;
|
||||
if (origins) {
|
||||
const first = origins.split(',')[0]?.trim();
|
||||
|
||||
@@ -175,7 +175,7 @@ export function getSharedTripData(token: string): Record<string, any> | null {
|
||||
|
||||
// Collab messages (only if owner chose to share)
|
||||
const collabMessages = permissions.share_collab
|
||||
? db.prepare('SELECT m.*, u.username, u.avatar FROM collab_messages m JOIN users u ON m.user_id = u.id WHERE m.trip_id = ? ORDER BY m.created_at ASC').all(tripId)
|
||||
? db.prepare('SELECT m.*, u.username, u.avatar FROM collab_messages m JOIN users u ON m.user_id = u.id WHERE m.trip_id = ? AND m.deleted = 0 ORDER BY m.created_at').all(tripId)
|
||||
: [];
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user