diff --git a/next-ui/src/colada/users.ts b/next-ui/src/colada/users.ts
index 5739c0dc..cbfa6ce9 100644
--- a/next-ui/src/colada/users.ts
+++ b/next-ui/src/colada/users.ts
@@ -1,4 +1,11 @@
-import { defineMutation, defineQuery, useMutation, useQuery, useQueryCache } from '@pinia/colada'
+import {
+ defineMutation,
+ defineQuery,
+ defineQueryOptions,
+ useMutation,
+ useQuery,
+ useQueryCache,
+} from '@pinia/colada'
import { komgaClient } from '@/api/komga-client'
import { UserRoles } from '@/types/UserRoles'
import type { components } from '@/generated/openapi/komga'
@@ -150,3 +157,61 @@ export const useDeleteApiKey = defineMutation(() => {
},
})
})
+
+///////////
+// Authentication Activity
+///////////
+
+export const authenticationActivityQuery = defineQueryOptions(
+ ({
+ page,
+ size,
+ sort,
+ unpaged,
+ }: {
+ page?: number
+ size?: number
+ sort?: string[]
+ unpaged?: boolean
+ }) => ({
+ key: ['authentication-activity', { page: page, size: size, sort: sort, unpaged: unpaged }],
+ query: () =>
+ komgaClient
+ .GET('/api/v2/users/authentication-activity', {
+ params: {
+ query: { page: page, size: size, sort: sort, unpaged: unpaged },
+ },
+ })
+ // unwrap the openapi-fetch structure on success
+ .then((res) => res.data),
+ }),
+)
+
+export const myAuthenticationActivityQuery = defineQueryOptions(
+ ({
+ page,
+ size,
+ sort,
+ unpaged,
+ }: {
+ page?: number
+ size?: number
+ sort?: string[]
+ unpaged?: boolean
+ }) => ({
+ key: [
+ ...QUERY_KEYS_USERS.currentUser,
+ 'authentication-activity',
+ { page: page, size: size, sort: sort, unpaged: unpaged },
+ ],
+ query: () =>
+ komgaClient
+ .GET('/api/v2/users/me/authentication-activity', {
+ params: {
+ query: { page: page, size: size, sort: sort, unpaged: unpaged },
+ },
+ })
+ // unwrap the openapi-fetch structure on success
+ .then((res) => res.data),
+ }),
+)
diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts
index 43c102e6..0d4e1c6c 100644
--- a/next-ui/src/components.d.ts
+++ b/next-ui/src/components.d.ts
@@ -25,6 +25,7 @@ declare module 'vue' {
FragmentLocaleSelector: typeof import('./fragments/fragment/LocaleSelector.vue')['default']
FragmentSnackQueue: typeof import('./fragments/fragment/SnackQueue.vue')['default']
FragmentThemeSelector: typeof import('./fragments/fragment/ThemeSelector.vue')['default']
+ FragmentUserAuthenticationActivityTable: typeof import('./fragments/fragment/user/AuthenticationActivityTable.vue')['default']
FragmentUserFormCreateEdit: typeof import('./fragments/fragment/user/form/CreateEdit.vue')['default']
FragmentUserTable: typeof import('./fragments/fragment/user/Table.vue')['default']
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
@@ -42,6 +43,7 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ServerSettings: typeof import('./components/server/Settings.vue')['default']
+ UserAuthenticationActivityTable: typeof import('./components/user/AuthenticationActivityTable.vue')['default']
UserDeletionWarning: typeof import('./components/user/DeletionWarning.vue')['default']
UserDetails: typeof import('./components/user/Details.vue')['default']
UserFormChangePassword: typeof import('./components/user/form/ChangePassword.vue')['default']
diff --git a/next-ui/src/fragments/fragment/user/AuthenticationActivityTable.stories.ts b/next-ui/src/fragments/fragment/user/AuthenticationActivityTable.stories.ts
new file mode 100644
index 00000000..82e3b028
--- /dev/null
+++ b/next-ui/src/fragments/fragment/user/AuthenticationActivityTable.stories.ts
@@ -0,0 +1,64 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import AuthenticationActivityTable from './AuthenticationActivityTable.vue'
+import { delay, http } from 'msw'
+import { response401Unauthorized } from '@/mocks/api/handlers'
+import { httpTyped } from '@/mocks/api/httpTyped'
+import { mockPage } from '@/mocks/api/pageable'
+import { PageRequest } from '@/types/PageRequest'
+
+const meta = {
+ component: AuthenticationActivityTable,
+ render: (args: object) => ({
+ components: { AuthenticationActivityTable },
+ setup() {
+ return { args }
+ },
+ template: '',
+ }),
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
+ },
+ args: {},
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {},
+}
+
+export const ForMe: Story = {
+ args: {
+ forMe: true,
+ },
+}
+
+export const Loading: Story = {
+ parameters: {
+ msw: {
+ handlers: [http.all('*', async () => await delay(5_000))],
+ },
+ },
+}
+
+export const NoData: Story = {
+ parameters: {
+ msw: {
+ handlers: [
+ httpTyped.get('/api/v2/users/authentication-activity', ({ response }) =>
+ response(200).json(mockPage([], new PageRequest())),
+ ),
+ ],
+ },
+ },
+}
+
+export const Error: Story = {
+ parameters: {
+ msw: {
+ handlers: [http.all('*', response401Unauthorized)],
+ },
+ },
+}
diff --git a/next-ui/src/fragments/fragment/user/AuthenticationActivityTable.vue b/next-ui/src/fragments/fragment/user/AuthenticationActivityTable.vue
new file mode 100644
index 00000000..5ab1f438
--- /dev/null
+++ b/next-ui/src/fragments/fragment/user/AuthenticationActivityTable.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+ {{
+ $formatMessage({
+ description: 'Authentication Activity table global header',
+ defaultMessage: 'Authentication Activity',
+ id: 'LaMAsc',
+ })
+ }}
+
+
+
+
+
+
+
+ {{
+ $formatMessage({
+ description: 'Authentication Activity table: shown when table has no data',
+ defaultMessage: 'No recent activity',
+ id: 'kGC6Gu',
+ })
+ }}
+
+
+
+
+
+
+
+
+
+ {{ $formatDate(value, { dateStyle: 'medium', timeStyle: 'short' }) }}
+
+
+
+
+
diff --git a/next-ui/src/fragments/layout/app/drawer/menu/Account.vue b/next-ui/src/fragments/layout/app/drawer/menu/Account.vue
index 92691b7e..56f97d0c 100644
--- a/next-ui/src/fragments/layout/app/drawer/menu/Account.vue
+++ b/next-ui/src/fragments/layout/app/drawer/menu/Account.vue
@@ -49,8 +49,8 @@
:title="
$formatMessage({
description: 'Drawer menu for My Account > Activity',
- defaultMessage: 'Activity',
- id: 'cGFtPg',
+ defaultMessage: 'My activity',
+ id: 'Xx+ITC',
})
"
/>
diff --git a/next-ui/src/fragments/layout/app/drawer/menu/Server.vue b/next-ui/src/fragments/layout/app/drawer/menu/Server.vue
index 55f99fb7..e9b988de 100644
--- a/next-ui/src/fragments/layout/app/drawer/menu/Server.vue
+++ b/next-ui/src/fragments/layout/app/drawer/menu/Server.vue
@@ -34,6 +34,18 @@
})
"
/>
+
+
+
+
+
-
+
diff --git a/next-ui/src/pages/server/activity.vue b/next-ui/src/pages/server/activity.vue
new file mode 100644
index 00000000..04a2f312
--- /dev/null
+++ b/next-ui/src/pages/server/activity.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/next-ui/src/pages/server/announcements.vue b/next-ui/src/pages/server/announcements.vue
index 092b6bc8..ea79fb58 100644
--- a/next-ui/src/pages/server/announcements.vue
+++ b/next-ui/src/pages/server/announcements.vue
@@ -55,7 +55,6 @@