feat: begin working on user auth

This commit is contained in:
Aarnav Tale
2025-03-29 14:12:15 -04:00
parent 8429b19c4a
commit bf02015dc7
11 changed files with 562 additions and 118 deletions
+83
View File
@@ -0,0 +1,83 @@
import { CircleUser } from 'lucide-react';
import StatusCircle from '~/components/StatusCircle';
import { Machine, User } from '~/types';
import cn from '~/utils/cn';
interface UserRowProps {
role: string;
user: User & { machines: Machine[] };
}
export default function UserRow({ user, role }: UserRowProps) {
const isOnline = user.machines.some((machine) => machine.online);
const lastSeen = user.machines.reduce(
(acc, machine) => Math.max(acc, new Date(machine.lastSeen).getTime()),
0,
);
return (
<tr key={user.id}>
<td className="pl-0.5 py-2">
<div className="flex items-center">
{user.profilePicUrl ? (
<img
src={user.profilePicUrl}
alt={user.name}
className="w-10 h-10 rounded-full"
/>
) : (
<CircleUser className="w-10 h-10" />
)}
<div className="ml-4">
<p className={cn('font-semibold leading-snug')}>{user.name}</p>
<p className="text-sm opacity-50">{user.email}</p>
</div>
</div>
</td>
<td className="pl-0.5 py-2">
<p>{mapRoleToName(role)}</p>
</td>
<td className="pl-0.5 py-2">
<p className="text-sm text-headplane-600 dark:text-headplane-300">
{new Date(user.createdAt).toLocaleDateString()}
</p>
</td>
<td className="pl-0.5 py-2">
<span
className={cn(
'flex items-center gap-x-1 text-sm',
'text-headplane-600 dark:text-headplane-300',
)}
>
<StatusCircle isOnline={isOnline} className="w-4 h-4" />
<p>{isOnline ? 'Connected' : new Date(lastSeen).toLocaleString()}</p>
</span>
</td>
</tr>
);
}
function mapRoleToName(role: string) {
switch (role) {
case 'no-oidc':
return <p className="opacity-50">Unmanaged</p>;
case 'invalid-oidc':
return <p className="opacity-50">Invalid</p>;
case 'no-role':
return <p className="opacity-50">No Role</p>;
case 'owner':
return 'Owner';
case 'admin':
return 'Admin';
case 'network_admin':
return 'Network Admin';
case 'it_admin':
return 'IT Admin';
case 'auditor':
return 'Auditor';
case 'member':
return 'Member';
default:
return 'Unknown';
}
}
+54 -2
View File
@@ -12,6 +12,7 @@ import type { LoadContext } from '~/server';
import type { Machine, User } from '~/types';
import cn from '~/utils/cn';
import ManageBanner from './components/manage-banner';
import UserRow from './components/user-row';
import DeleteUser from './dialogs/delete-user';
import RenameUser from './dialogs/rename-user';
import { userAction } from './user-actions';
@@ -34,6 +35,28 @@ export async function loader({
machines: machines.nodes.filter((machine) => machine.user.id === user.id),
}));
const roles = users
.sort((a, b) => a.name.localeCompare(b.name))
.map((user) => {
if (user.provider !== 'oidc') {
return 'no-oidc';
}
if (user.provider === 'oidc' && user.providerId) {
// For some reason, headscale makes providerID a url where the
// last component is the subject, so we need to strip that out
const subject = user.providerId.split('/').pop();
if (!subject) {
return 'invalid-oidc';
}
const role = context.sessions.roleForSubject(subject);
return role ?? 'no-role';
}
return 'no-role';
});
let magic: string | undefined;
if (context.hs.readable()) {
if (context.hs.c?.dns.magic_dns) {
@@ -43,6 +66,7 @@ export async function loader({
return {
oidc: context.config.oidc,
roles,
magic,
users,
};
@@ -72,7 +96,35 @@ export default function Page() {
drag machines between users to change ownership.
</p>
<ManageBanner oidc={data.oidc} />
<ClientOnly fallback={<Users users={users} />}>
<table className="table-auto w-full rounded-lg">
<thead className="text-headplane-600 dark:text-headplane-300">
<tr className="text-left px-0.5">
<th className="uppercase text-xs font-bold pb-2">User</th>
<th className="uppercase text-xs font-bold pb-2">Role</th>
<th className="uppercase text-xs font-bold pb-2">Created At</th>
<th className="uppercase text-xs font-bold pb-2">Last Seen</th>
</tr>
</thead>
<tbody
className={cn(
'divide-y divide-headplane-100 dark:divide-headplane-800 align-top',
'border-t border-headplane-100 dark:border-headplane-800',
)}
>
{users
.sort((a, b) => a.name.localeCompare(b.name))
.map((user) => (
<UserRow
key={user.id}
user={user}
role={data.roles[users.indexOf(user)]}
/>
))}
</tbody>
</table>
{/* <Users users={users} /> */}
{/* <ClientOnly fallback={<Users users={users} />}>
{() => (
<InteractiveUsers
users={users}
@@ -80,7 +132,7 @@ export default function Page() {
magic={data.magic}
/>
)}
</ClientOnly>
</ClientOnly> */}
</>
);
}