Skip to content

Commit 7e5b9b9

Browse files
committed
add alt link moduleas per latest version of maxijabase/sm-whois
1 parent 5b887b0 commit 7e5b9b9

File tree

4 files changed

+262
-2
lines changed

4 files changed

+262
-2
lines changed

src/lib/components/Sidebar.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@
6060
const whoisItem: NavItem = {
6161
name: 'Whois',
6262
icon: SearchOutline,
63-
href: '/whois'
63+
href: '#',
64+
children: [
65+
{ name: 'Search', href: '/whois', icon: SearchOutline },
66+
{ name: 'Alt Link', href: '/whois/alt', icon: UsersGroupOutline }
67+
]
6468
};
6569
6670
const adminItem: NavItem = {

src/routes/whois/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@
143143
</script>
144144

145145
<div class="p-4">
146-
<div class="flex items-end justify-between gap-2">
146+
<div class="flex items-end justify-between gap-2 mb-4">
147147
<div>
148148
<Title>Whois</Title>
149149
</div>
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import type { Actions, PageServerLoad } from './$types';
2+
import { redirect, error, fail } from '@sveltejs/kit';
3+
import prismaArg from '$lib/prisma/prismaArg';
4+
import { toSteam64FromAny, allIdVariantsForSteam64 } from '$lib/whois/utils';
5+
6+
type AltLink = {
7+
steam_id: string;
8+
main_steam_id: string | null;
9+
linked_at: Date;
10+
linked_by: string | null;
11+
};
12+
13+
function isValidSteam64(id: string | null | undefined): id is string {
14+
return !!id && /^\d{17}$/.test(id);
15+
}
16+
17+
export const load: PageServerLoad = async (event) => {
18+
const user = event.locals.user as { steamid: string; role?: string } | null;
19+
20+
if (!user) throw redirect(302, '/');
21+
if (user.role !== 'owner') throw error(403, 'Forbidden');
22+
23+
// Fetch all rows and normalize to 64-bit IDs for grouping/display
24+
const rows = (await prismaArg.whois_alt_links.findMany()) as unknown as AltLink[];
25+
26+
const normalized = rows
27+
.map((r) => {
28+
const main64 = r.main_steam_id ? toSteam64FromAny(r.main_steam_id) : null;
29+
const alt64 = toSteam64FromAny(r.steam_id);
30+
return {
31+
steam_id_64: alt64,
32+
main_steam_id_64: main64,
33+
linked_at: r.linked_at,
34+
linked_by: r.linked_by || null
35+
};
36+
})
37+
.filter((r) => !!r.steam_id_64) as Array<{
38+
steam_id_64: string;
39+
main_steam_id_64: string | null;
40+
linked_at: Date;
41+
linked_by: string | null;
42+
}>;
43+
44+
const mainsSet = new Set<string>();
45+
for (const r of normalized) {
46+
if (r.main_steam_id_64 && isValidSteam64(r.main_steam_id_64)) mainsSet.add(r.main_steam_id_64);
47+
}
48+
const altIds = new Set(normalized.filter((r) => r.main_steam_id_64).map((r) => r.steam_id_64));
49+
for (const r of normalized) {
50+
if (!altIds.has(r.steam_id_64) && isValidSteam64(r.steam_id_64)) mainsSet.add(r.steam_id_64);
51+
}
52+
53+
const mains = Array.from(mainsSet);
54+
const groups = mains.map((main) => ({
55+
main,
56+
alts: normalized.filter((r) => r.main_steam_id_64 === main).map((r) => r.steam_id_64)
57+
}));
58+
59+
// also return ungrouped: mains with no explicit alts
60+
return { user, groups };
61+
};
62+
63+
export const actions: Actions = {
64+
add_main: async ({ request, locals }) => {
65+
const user = locals.user as { steamid: string; role?: string } | null;
66+
if (!user || user.role !== 'owner') return fail(403, { message: 'Forbidden' });
67+
const form = await request.formData();
68+
const mainIn = String(form.get('main') || '').trim();
69+
const main = toSteam64FromAny(mainIn);
70+
if (!main) return fail(400, { message: 'Invalid Steam ID' });
71+
72+
// Ensure a row exists for the main itself (self-main is represented by absence in table; no-op)
73+
// We do nothing here except ensure the main is present as a standalone group when loading.
74+
return { ok: true };
75+
},
76+
77+
add_alt: async ({ request, locals }) => {
78+
const user = locals.user as { steamid: string; role?: string } | null;
79+
if (!user || user.role !== 'owner') return fail(403, { message: 'Forbidden' });
80+
const form = await request.formData();
81+
const mainIn = String(form.get('main') || '').trim();
82+
const altIn = String(form.get('alt') || '').trim();
83+
const main = toSteam64FromAny(mainIn);
84+
const alt = toSteam64FromAny(altIn);
85+
if (!main || !alt) return fail(400, { message: 'Invalid Steam ID' });
86+
if (main === alt) return fail(400, { message: 'Alt cannot equal main' });
87+
88+
await prismaArg.whois_alt_links.upsert({
89+
where: { steam_id: alt },
90+
update: { main_steam_id: main, linked_by: user.steamid },
91+
create: { steam_id: alt, main_steam_id: main, linked_by: user.steamid }
92+
});
93+
return { ok: true };
94+
},
95+
96+
edit_alt: async ({ request, locals }) => {
97+
const user = locals.user as { steamid: string; role?: string } | null;
98+
if (!user || user.role !== 'owner') return fail(403, { message: 'Forbidden' });
99+
const form = await request.formData();
100+
const altIn = String(form.get('alt') || '').trim();
101+
const mainIn = String(form.get('main') || '').trim();
102+
const alt = toSteam64FromAny(altIn);
103+
const main = toSteam64FromAny(mainIn);
104+
if (!alt || !main) return fail(400, { message: 'Invalid Steam ID' });
105+
await prismaArg.whois_alt_links.update({
106+
where: { steam_id: alt },
107+
data: { main_steam_id: main, linked_by: user.steamid }
108+
});
109+
return { ok: true };
110+
},
111+
112+
delete_alt: async ({ request, locals }) => {
113+
const user = locals.user as { steamid: string; role?: string } | null;
114+
if (!user || user.role !== 'owner') return fail(403, { message: 'Forbidden' });
115+
const form = await request.formData();
116+
const altIn = String(form.get('alt') || '').trim();
117+
const alt64 = toSteam64FromAny(altIn);
118+
if (!alt64) return fail(400, { message: 'Invalid Steam ID' });
119+
const variants = allIdVariantsForSteam64(alt64);
120+
await prismaArg.whois_alt_links.deleteMany({ where: { steam_id: { in: variants } } });
121+
return { ok: true };
122+
}
123+
};
124+
125+

src/routes/whois/alt/+page.svelte

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<script lang="ts">
2+
import Title from '$lib/components/Title.svelte';
3+
import type { PageData } from './$types';
4+
import { enhance } from '$app/forms';
5+
6+
let { data } = $props<{ data: PageData }>();
7+
8+
let profiles: Record<string, { avatar: string; avatarmedium: string; avatarfull: string; personaname?: string }> = $state({});
9+
let loadingProfiles = $state(false);
10+
let errorMsg: string | null = $state(null);
11+
12+
async function loadProfiles() {
13+
try {
14+
errorMsg = null;
15+
const ids = new Set<string>();
16+
for (const g of data.groups || []) {
17+
ids.add(g.main);
18+
for (const a of g.alts) ids.add(a);
19+
}
20+
if (ids.size === 0) {
21+
profiles = {};
22+
return;
23+
}
24+
loadingProfiles = true;
25+
const resp = await fetch(`/api/steam/profile?steamids=${encodeURIComponent(Array.from(ids).join(','))}`);
26+
if (!resp.ok) {
27+
loadingProfiles = false;
28+
return;
29+
}
30+
profiles = await resp.json();
31+
loadingProfiles = false;
32+
} catch (e: any) {
33+
loadingProfiles = false;
34+
errorMsg = e?.message || 'Failed to load Steam profiles';
35+
}
36+
}
37+
38+
function pf(id: string) {
39+
return profiles[id];
40+
}
41+
42+
function profileUrl(id: string) {
43+
return `https://steamcommunity.com/profiles/${id}`;
44+
}
45+
46+
$effect(() => {
47+
loadProfiles();
48+
});
49+
</script>
50+
51+
<div class="p-4">
52+
<div class="flex items-end justify-between gap-2 mb-4">
53+
<div>
54+
<Title>Alt Link</Title>
55+
</div>
56+
</div>
57+
<div class="container mx-auto max-w-6xl space-y-6 px-3">
58+
<section class="rounded border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
59+
<h2 class="text-lg font-semibold mb-3">Add Main</h2>
60+
<form method="POST" use:enhance class="flex gap-2">
61+
<input name="main" type="text" placeholder="SteamID64" class="w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm text-gray-700 placeholder:text-gray-400 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200" />
62+
<button name="/add_main" formaction="?/add_main" class="rounded bg-blue-600 px-4 py-2 text-white">Add</button>
63+
</form>
64+
</section>
65+
66+
{#if errorMsg}
67+
<div class="text-red-600 dark:text-red-400">{errorMsg}</div>
68+
{/if}
69+
70+
<section class="space-y-4">
71+
{#each data.groups as g}
72+
<div class="rounded border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
73+
<div class="mb-3 flex items-center gap-3">
74+
{#if loadingProfiles}
75+
<div class="h-10 w-10 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
76+
{:else if pf(g.main)?.avatarfull}
77+
<img class="h-10 w-10 rounded" src={pf(g.main).avatarfull} alt="avatar" />
78+
{:else}
79+
<div class="h-10 w-10 rounded bg-gray-200 dark:bg-gray-700"></div>
80+
{/if}
81+
<div class="min-w-0">
82+
<a href={profileUrl(g.main)} target="_blank" rel="noreferrer" class="font-semibold text-blue-600 hover:underline dark:text-blue-400">{pf(g.main)?.personaname || g.main}</a>
83+
<div class="font-mono truncate text-xs text-gray-500">{g.main}</div>
84+
</div>
85+
</div>
86+
87+
<div class="space-y-2">
88+
{#if (g.alts || []).length === 0}
89+
<div class="text-sm text-gray-500">No alts linked.</div>
90+
{:else}
91+
{#each g.alts as alt}
92+
<div class="flex items-center justify-between gap-3 rounded border border-gray-200 bg-gray-50 p-2 dark:border-gray-700 dark:bg-gray-900/40">
93+
<div class="flex items-center gap-3">
94+
{#if loadingProfiles}
95+
<div class="h-8 w-8 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
96+
{:else if pf(alt)?.avatarfull}
97+
<img class="h-8 w-8 rounded" src={pf(alt).avatarfull} alt="avatar" />
98+
{:else}
99+
<div class="h-8 w-8 rounded bg-gray-200 dark:bg-gray-700"></div>
100+
{/if}
101+
<div class="min-w-0">
102+
<a href={profileUrl(alt)} target="_blank" rel="noreferrer" class="text-sm font-medium text-blue-600 hover:underline dark:text-blue-400">{pf(alt)?.personaname || alt}</a>
103+
<div class="font-mono truncate text-xs text-gray-500">{alt}</div>
104+
</div>
105+
</div>
106+
<div class="flex items-center gap-2">
107+
<form method="POST" use:enhance>
108+
<input type="hidden" name="alt" value={alt} />
109+
<input type="hidden" name="main" value={g.main} />
110+
<button name="/delete_alt" formaction="?/delete_alt" class="rounded bg-red-600 px-3 py-1 text-xs text-white">Delete</button>
111+
</form>
112+
</div>
113+
</div>
114+
{/each}
115+
{/if}
116+
</div>
117+
118+
<div class="mt-3">
119+
<form method="POST" use:enhance class="flex gap-2">
120+
<input type="hidden" name="main" value={g.main} />
121+
<input name="alt" type="text" placeholder="Alt SteamID64" class="w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm text-gray-700 placeholder:text-gray-400 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200" />
122+
<button name="/add_alt" formaction="?/add_alt" class="rounded bg-emerald-600 px-4 py-2 text-white">Add Alt</button>
123+
</form>
124+
</div>
125+
</div>
126+
{/each}
127+
</section>
128+
</div>
129+
</div>
130+
131+

0 commit comments

Comments
 (0)