Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { mdiEmailLock, mdiGithub, mdiMicrosoft } from '@mdi/js';
import api from './base';
import { AxiosPromise } from 'axios';
import { IfmColors } from '@tdev-components/shared/Colors';
import { Session as BetterAuthSession } from 'better-auth';

export enum Role {
STUDENT = 'student',
Expand Down Expand Up @@ -45,6 +46,8 @@ export const AuthProviderColor = {
[AuthProvider.GITHUB]: IfmColors.black
};

export type Session = Omit<BetterAuthSession, 'userId' | 'token'>;

export type User = {
id: string;
email: string;
Expand All @@ -53,6 +56,7 @@ export type User = {
lastName: string;
role: Role;
authProviders?: AuthProvider[];
sessions?: Session[];
banned?: boolean;
banReason?: string;
banExpires?: Date;
Expand Down
42 changes: 38 additions & 4 deletions src/components/Admin/UserTable/User.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { AuthProviderColor, AuthProviderIcons, RoleColors, RoleNames } from '@td
import { useStore } from '@tdev-hooks/useStore';
import LiveStatusIndicator from '@tdev-components/LiveStatusIndicator';
import Icon from '@mdi/react';
import { mdiAccountCancel, mdiAccountEdit, mdiCloudQuestion, mdiDatabaseExport } from '@mdi/js';
import { mdiAccountCancel, mdiAccountEdit, mdiCloudQuestion } from '@mdi/js';
import { SIZE_S, SIZE_XS } from '@tdev-components/shared/iconSizes';
import Button from '@tdev-components/shared/Button';
import Popup from 'reactjs-popup';
Expand All @@ -20,6 +20,8 @@ import Badge from '@tdev-components/shared/Badge';
import NavReloadRequest from '../ActionRequest/NavReloadRequest';
import { IfmColors } from '@tdev-components/shared/Colors';
import ExportModal from '../ExportPanel/ExportModal';
import Card from '@tdev-components/shared/Card';
import DefinitionList from '@tdev-components/DefinitionList';

interface Props {
user: UserModel;
Expand All @@ -28,7 +30,6 @@ interface Props {
const UserTableRow = observer((props: Props) => {
const { user } = props;
const userStore = useStore('userStore');
const adminStore = useStore('adminStore');
const { current } = userStore;
const ref = React.useRef<PopupActions>(null);
if (!current) {
Expand Down Expand Up @@ -86,8 +87,41 @@ const UserTableRow = observer((props: Props) => {
/>
))}
</td>
<td>{formatDateTime(user.createdAt)}</td>
<td>{formatDateTime(user.updatedAt)}</td>
<td>{formatDateTime(user.createdAt, true)}</td>
<td>{formatDateTime(user.updatedAt, true)}</td>
<td>
{user.lastSeen ? (
<Popup
trigger={
<span>
<Badge>{formatDateTime(user.lastSeen, true)}</Badge>
</span>
}
on="hover"
position={['top left', 'left center']}
keepTooltipInside="#__docusaurus"
mouseEnterDelay={200}
repositionOnResize
>
<Card>
<DefinitionList small>
<dt>Expires</dt>
<dd>{formatDateTime(user.lastSession!.expiresAt, true)}</dd>
<dt>IP</dt>
<dd>{user.lastSession?.ipAddress ?? '-'}</dd>
<dt>Client</dt>
<dd>
<pre>
<code>
{(user.lastSession?.userAgent ?? '-').split(') ').join(')\n')}
</code>
</pre>
</dd>
</DefinitionList>
</Card>
</Popup>
) : null}
</td>
<td className={clsx(styles.limitWidth)}>
{user.studentGroups.map((group, idx) => (
<span className={clsx('badge badge--primary', styles.groupBadge)} key={idx}>
Expand Down
33 changes: 28 additions & 5 deletions src/components/Admin/UserTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ type SortColumn =
| 'linkedAccounts'
| 'createdAt'
| 'updatedAt'
| 'groups'
| 'authProviderNames'
| 'lastSeen'
| 'studentGroupNames'
| 'id'
| 'connectedClients';
interface Props {
Expand Down Expand Up @@ -137,7 +139,15 @@ const UserTable = observer((props: Props) => {
onClick={() => setSortColumn('lastName')}
/>
</th>
<th>Auth Provider</th>
<th>
<Button
size={SIZE_S}
iconSide="left"
icon={sortColumn === 'authProviderNames' && icon}
text="Auth Provider"
onClick={() => setSortColumn('authProviderNames')}
/>
</th>
<th>
<Button
size={SIZE_S}
Expand All @@ -160,9 +170,18 @@ const UserTable = observer((props: Props) => {
<Button
size={SIZE_S}
iconSide="left"
icon={sortColumn === 'groups' && icon}
icon={sortColumn === 'lastSeen' && icon}
text="Letztes Login"
onClick={() => setSortColumn('lastSeen')}
/>
</th>
<th>
<Button
size={SIZE_S}
iconSide="left"
icon={sortColumn === 'studentGroupNames' && icon}
text="Lerngruppen"
onClick={() => setSortColumn('groups')}
onClick={() => setSortColumn('studentGroupNames')}
/>
</th>
<th>
Expand All @@ -177,7 +196,11 @@ const UserTable = observer((props: Props) => {
</tr>
</thead>
<tbody>
{_.orderBy(userStore.managedUsers, [sortColumn], [sortDirection])
{_.orderBy(
userStore.managedUsers,
[(user) => (user as any)[sortColumn] === undefined, sortColumn],
['asc', sortDirection]
)
.filter((user) => searchRegex.test(user.searchTerm))
.slice(0, props.showAll ? userStore.managedUsers.length : itemsShown)
.map((user, idx) => {
Expand Down
35 changes: 34 additions & 1 deletion src/models/User.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { action, computed, observable } from 'mobx';
import { AuthProvider, Role, RoleAccessLevel, User as UserProps } from '@tdev-api/user';
import { AuthProvider, Role, RoleAccessLevel, Session, User as UserProps } from '@tdev-api/user';
import { UserStore } from '@tdev-stores/UserStore';
import { orderBy } from 'es-toolkit/array';

export default class User {
readonly store: UserStore;
Expand All @@ -12,6 +13,8 @@ export default class User {
readonly firstName: string;
readonly lastName: string;
readonly banned?: boolean;
readonly sessions: Session[];

@observable accessor banReason: string | undefined;
@observable accessor banExpires: Date | undefined;

Expand All @@ -31,6 +34,7 @@ export default class User {
this.banned = props.banned;
this.banReason = props.banReason;
this.banExpires = props.banExpires ? new Date(props.banExpires) : undefined;
this.sessions = props.sessions || [];
this.createdAt = new Date(props.createdAt);
this.updatedAt = new Date(props.updatedAt);
}
Expand Down Expand Up @@ -91,6 +95,23 @@ export default class User {
};
}

@computed
get lastSession() {
if (this.sessions.length === 0) {
return undefined;
}
const sessions = orderBy(this.sessions, ['updatedAt'], ['desc']);
return sessions[0];
}

@computed
get lastSeen() {
if (!this.lastSession) {
return undefined;
}
return new Date(this.lastSession.updatedAt);
}

@computed
get searchTerm(): string {
return `${this.firstName} ${this.lastName} ${this.email}`;
Expand Down Expand Up @@ -124,4 +145,16 @@ export default class User {
get hasEmailPasswordAuth() {
return this.authProviders.includes(AuthProvider.CREDENTIAL);
}

@computed
get authProviderNames() {
const names = [...this.authProviders].sort();
return names.join(', ');
}

@computed
get studentGroupNames() {
const names = this.studentGroups.map((group) => group.name).sort();
return names.join(', ');
}
}
15 changes: 11 additions & 4 deletions src/models/helpers/date.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
export const formatDate = (date: Date) => {
export const formatDate = (date: Date, useLocalTime?: boolean) => {
if (useLocalTime) {
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
return date.toISOString().slice(0, 10).split('-').reverse().join('.');
};

export const formatTime = (date: Date) => {
export const formatTime = (date: Date, useLocalTime?: boolean) => {
if (useLocalTime) {
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}
return date.toISOString().slice(11, 16);
};

export const formatDateTime = (date: Date) => {
return `${formatDate(date)} ${formatTime(date)}`;
export const formatDateTime = (date: Date | string, useLocalTime?: boolean) => {
const d = typeof date === 'string' ? new Date(date) : date;
return `${formatDate(d, useLocalTime)} ${formatTime(d, useLocalTime)}`;
};