Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,39 @@ import { companyLinks, userCompanyLinks, users } from '$lib/server/db/schema.js'
|
||||
import { requireCompanyRoleAny } from '$lib/server/authorization.js';
|
||||
import { logCompanyEvent } from '$lib/server/audit.js';
|
||||
import { validateLinkUrl, InvalidLinkUrlError } from '$lib/server/links/validate.js';
|
||||
import { fetchFavicon } from '$lib/server/favicons/index.js';
|
||||
import { and, asc, eq, isNull, sql } from 'drizzle-orm';
|
||||
|
||||
function scheduleCompanyFavicon(linkId: string, url: string): void {
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await fetchFavicon(url);
|
||||
if (!result) return;
|
||||
await db
|
||||
.update(companyLinks)
|
||||
.set({ faviconUrl: result.dataUrl, faviconFetchedAt: result.fetchedAt })
|
||||
.where(eq(companyLinks.id, linkId));
|
||||
} catch (err) {
|
||||
console.error('[favicon] company link fetch failed', err);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function schedulePersonalFavicon(linkId: string, url: string): void {
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await fetchFavicon(url);
|
||||
if (!result) return;
|
||||
await db
|
||||
.update(userCompanyLinks)
|
||||
.set({ faviconUrl: result.dataUrl, faviconFetchedAt: result.fetchedAt })
|
||||
.where(eq(userCompanyLinks.id, linkId));
|
||||
} catch (err) {
|
||||
console.error('[favicon] personal link fetch failed', err);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
const CATEGORIES = [
|
||||
'internal_tool',
|
||||
'communication',
|
||||
@@ -186,6 +217,8 @@ export const actions: Actions = {
|
||||
{ linkId: inserted.id, category, url: validUrl }
|
||||
);
|
||||
|
||||
scheduleCompanyFavicon(inserted.id, validUrl);
|
||||
|
||||
return { success: true, action: 'addCompanyLink' };
|
||||
},
|
||||
|
||||
@@ -244,6 +277,8 @@ export const actions: Actions = {
|
||||
{ linkId: id, category }
|
||||
);
|
||||
|
||||
if (urlChanged) scheduleCompanyFavicon(id, validUrl);
|
||||
|
||||
return { success: true, action: 'updateCompanyLink' };
|
||||
},
|
||||
|
||||
@@ -336,7 +371,9 @@ export const actions: Actions = {
|
||||
category
|
||||
});
|
||||
|
||||
await db.insert(userCompanyLinks).values({
|
||||
const [inserted] = await db
|
||||
.insert(userCompanyLinks)
|
||||
.values({
|
||||
userId: user.id,
|
||||
companyId: params.companyId,
|
||||
category,
|
||||
@@ -345,7 +382,10 @@ export const actions: Actions = {
|
||||
url: validUrl,
|
||||
description,
|
||||
sortOrder
|
||||
});
|
||||
})
|
||||
.returning({ id: userCompanyLinks.id });
|
||||
|
||||
schedulePersonalFavicon(inserted.id, validUrl);
|
||||
|
||||
return { success: true, action: 'addPersonalLink' };
|
||||
},
|
||||
@@ -404,6 +444,8 @@ export const actions: Actions = {
|
||||
})
|
||||
.where(eq(userCompanyLinks.id, id));
|
||||
|
||||
if (urlChanged) schedulePersonalFavicon(id, validUrl);
|
||||
|
||||
return { success: true, action: 'updatePersonalLink' };
|
||||
},
|
||||
|
||||
@@ -465,5 +507,82 @@ export const actions: Actions = {
|
||||
});
|
||||
|
||||
return { success: true, action: 'reorderPersonalLinks' };
|
||||
},
|
||||
|
||||
refreshCompanyFavicon: async ({ request, locals, params }) => {
|
||||
await requireCompanyRoleAny(locals, params.companyId, ['admin', 'manager']);
|
||||
const fd = await request.formData();
|
||||
const id = trimOrNull(fd.get('id'));
|
||||
if (!id) return fail(400, { action: 'refreshCompanyFavicon', error: 'Link id is required' });
|
||||
|
||||
const [existing] = await db
|
||||
.select({ id: companyLinks.id, url: companyLinks.url })
|
||||
.from(companyLinks)
|
||||
.where(
|
||||
and(
|
||||
eq(companyLinks.id, id),
|
||||
eq(companyLinks.companyId, params.companyId),
|
||||
isNull(companyLinks.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
if (!existing) error(404, 'Link not found');
|
||||
|
||||
const result = await fetchFavicon(existing.url);
|
||||
await db
|
||||
.update(companyLinks)
|
||||
.set({
|
||||
faviconUrl: result?.dataUrl ?? null,
|
||||
faviconFetchedAt: result?.fetchedAt ?? new Date()
|
||||
})
|
||||
.where(eq(companyLinks.id, id));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'refreshCompanyFavicon',
|
||||
faviconFound: Boolean(result)
|
||||
};
|
||||
},
|
||||
|
||||
refreshPersonalFavicon: async ({ request, locals, params }) => {
|
||||
const { user } = await requireCompanyRoleAny(locals, params.companyId, [
|
||||
'admin',
|
||||
'manager',
|
||||
'user',
|
||||
'viewer',
|
||||
'hr',
|
||||
'accountant'
|
||||
]);
|
||||
const fd = await request.formData();
|
||||
const id = trimOrNull(fd.get('id'));
|
||||
if (!id) return fail(400, { action: 'refreshPersonalFavicon', error: 'Link id is required' });
|
||||
|
||||
const [existing] = await db
|
||||
.select({ id: userCompanyLinks.id, url: userCompanyLinks.url })
|
||||
.from(userCompanyLinks)
|
||||
.where(
|
||||
and(
|
||||
eq(userCompanyLinks.id, id),
|
||||
eq(userCompanyLinks.userId, user.id),
|
||||
eq(userCompanyLinks.companyId, params.companyId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
if (!existing) error(404, 'Link not found');
|
||||
|
||||
const result = await fetchFavicon(existing.url);
|
||||
await db
|
||||
.update(userCompanyLinks)
|
||||
.set({
|
||||
faviconUrl: result?.dataUrl ?? null,
|
||||
faviconFetchedAt: result?.fetchedAt ?? new Date()
|
||||
})
|
||||
.where(eq(userCompanyLinks.id, id));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'refreshPersonalFavicon',
|
||||
faviconFound: Boolean(result)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
canEdit: boolean;
|
||||
editAction: string;
|
||||
deleteAction: string;
|
||||
refreshAction: string;
|
||||
emptyMessage: string;
|
||||
};
|
||||
|
||||
@@ -98,6 +99,7 @@
|
||||
canEdit: data.canManage,
|
||||
editAction: '?/updateCompanyLink',
|
||||
deleteAction: '?/deleteCompanyLink',
|
||||
refreshAction: '?/refreshCompanyFavicon',
|
||||
emptyMessage: data.canManage
|
||||
? 'No company links yet. Click "+ New Link" to add the first one.'
|
||||
: 'No company links have been added yet.'
|
||||
@@ -106,6 +108,7 @@
|
||||
canEdit: true,
|
||||
editAction: '?/updatePersonalLink',
|
||||
deleteAction: '?/deletePersonalLink',
|
||||
refreshAction: '?/refreshPersonalFavicon',
|
||||
emptyMessage:
|
||||
'No personal bookmarks yet. Bookmarks added here are private to you and scoped to this company.'
|
||||
};
|
||||
@@ -349,6 +352,22 @@
|
||||
{/if}
|
||||
|
||||
{#if editingId === link.id}
|
||||
<form
|
||||
method="POST"
|
||||
action={opts.refreshAction}
|
||||
use:enhance={() => async ({ update }) => {
|
||||
await update({ reset: false });
|
||||
}}
|
||||
class="mt-2"
|
||||
>
|
||||
<input type="hidden" name="id" value={link.id} />
|
||||
<button
|
||||
type="submit"
|
||||
class="text-xs font-medium text-gray-600 underline hover:text-gray-800 dark:text-gray-300 dark:hover:text-white"
|
||||
>
|
||||
Refresh favicon
|
||||
</button>
|
||||
</form>
|
||||
<form
|
||||
method="POST"
|
||||
action={opts.editAction}
|
||||
|
||||
Reference in New Issue
Block a user