@shipfox/api-auth 10.1.0 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +16 -0
- package/README.md +15 -0
- package/dist/core/administration.d.ts +17 -1
- package/dist/core/administration.d.ts.map +1 -1
- package/dist/core/administration.js +107 -14
- package/dist/core/administration.js.map +1 -1
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +11 -2
- package/dist/core/auth.js.map +1 -1
- package/dist/db/admin-command.d.ts +18 -0
- package/dist/db/admin-command.d.ts.map +1 -0
- package/dist/db/admin-command.js +37 -0
- package/dist/db/admin-command.js.map +1 -0
- package/dist/db/admin-grants.d.ts +1 -1
- package/dist/db/admin-grants.d.ts.map +1 -1
- package/dist/db/admin-grants.js +12 -30
- package/dist/db/admin-grants.js.map +1 -1
- package/dist/db/admin-user-moderation.d.ts +29 -0
- package/dist/db/admin-user-moderation.d.ts.map +1 -0
- package/dist/db/admin-user-moderation.js +156 -0
- package/dist/db/admin-user-moderation.js.map +1 -0
- package/dist/db/admin-user-summary.d.ts +14 -0
- package/dist/db/admin-user-summary.d.ts.map +1 -0
- package/dist/db/admin-user-summary.js +31 -0
- package/dist/db/admin-user-summary.js.map +1 -0
- package/dist/db/admin-users.d.ts +2 -11
- package/dist/db/admin-users.d.ts.map +1 -1
- package/dist/db/admin-users.js +2 -27
- package/dist/db/admin-users.js.map +1 -1
- package/dist/db/refresh-tokens.d.ts +6 -0
- package/dist/db/refresh-tokens.d.ts.map +1 -1
- package/dist/db/refresh-tokens.js +31 -0
- package/dist/db/refresh-tokens.js.map +1 -1
- package/dist/db/schema/admin-command-results.d.ts +19 -2
- package/dist/db/schema/admin-command-results.d.ts.map +1 -1
- package/dist/db/schema/admin-command-results.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/auth/bearer-token-auth.d.ts +1 -0
- package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -1
- package/dist/presentation/auth/bearer-token-auth.js +4 -3
- package/dist/presentation/auth/bearer-token-auth.js.map +1 -1
- package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
- package/dist/presentation/auth/jwt-auth.js +32 -4
- package/dist/presentation/auth/jwt-auth.js.map +1 -1
- package/dist/presentation/routes/administration.d.ts.map +1 -1
- package/dist/presentation/routes/administration.js +94 -3
- package/dist/presentation/routes/administration.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/core/administration.ts +137 -15
- package/src/core/auth.ts +9 -2
- package/src/db/admin-command.ts +85 -0
- package/src/db/admin-grants.ts +19 -57
- package/src/db/admin-user-moderation.ts +230 -0
- package/src/db/admin-user-summary.ts +51 -0
- package/src/db/admin-users.ts +4 -49
- package/src/db/refresh-tokens.ts +54 -0
- package/src/db/schema/admin-command-results.ts +23 -2
- package/src/index.ts +3 -0
- package/src/presentation/auth/bearer-token-auth.test.ts +17 -0
- package/src/presentation/auth/bearer-token-auth.ts +9 -2
- package/src/presentation/auth/jwt-auth.test.ts +1 -1
- package/src/presentation/auth/jwt-auth.ts +30 -1
- package/src/presentation/routes/administration.test.ts +245 -2
- package/src/presentation/routes/administration.ts +88 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import {ADMINISTRATION_ACTION_PERFORMED} from '@shipfox/api-common-dto';
|
|
2
|
-
import {sql} from 'drizzle-orm';
|
|
2
|
+
import {eq, sql} from 'drizzle-orm';
|
|
3
3
|
import type {FastifyInstance} from 'fastify';
|
|
4
4
|
import {type AuthRateLimitAction, hashAuthRateLimitIdentifier} from '#core/rate-limit.js';
|
|
5
5
|
import {db} from '#db/db.js';
|
|
6
6
|
import {authOutbox} from '#db/schema/outbox.js';
|
|
7
7
|
import {authRateLimits} from '#db/schema/rate-limits.js';
|
|
8
|
-
import {
|
|
8
|
+
import {refreshTokens} from '#db/schema/refresh-tokens.js';
|
|
9
|
+
import {users} from '#db/schema/users.js';
|
|
10
|
+
import {
|
|
11
|
+
cookieHeader,
|
|
12
|
+
createAuthTestApp,
|
|
13
|
+
createVerifiedSession,
|
|
14
|
+
getSetCookie,
|
|
15
|
+
login,
|
|
16
|
+
resetCapturedMail,
|
|
17
|
+
} from '#test/routes.js';
|
|
9
18
|
|
|
10
19
|
const BOOTSTRAP_TOKEN = 'test-bootstrap-token';
|
|
11
20
|
|
|
@@ -662,4 +671,238 @@ describe('Auth administration routes', () => {
|
|
|
662
671
|
expect(finalOwnerRevoke.statusCode).toBe(409);
|
|
663
672
|
expect(finalOwnerRevoke.json().code).toBe('last-owner');
|
|
664
673
|
});
|
|
674
|
+
|
|
675
|
+
test('suspends users, invalidates sessions, and reactivates without restoring them', async () => {
|
|
676
|
+
const owner = await createVerifiedSession('admin-user-moderation-owner');
|
|
677
|
+
const target = await createVerifiedSession('admin-user-moderation-target');
|
|
678
|
+
|
|
679
|
+
await app.inject({
|
|
680
|
+
method: 'POST',
|
|
681
|
+
url: '/admin/auth/admin-grants/bootstrap',
|
|
682
|
+
headers: authHeaders(owner.token, 'moderation-bootstrap'),
|
|
683
|
+
payload: {bootstrap_token: BOOTSTRAP_TOKEN},
|
|
684
|
+
});
|
|
685
|
+
await app.inject({
|
|
686
|
+
method: 'POST',
|
|
687
|
+
url: '/admin/auth/admin-grants',
|
|
688
|
+
headers: authHeaders(owner.token, 'moderation-grant'),
|
|
689
|
+
payload: {
|
|
690
|
+
user_id: owner.userId,
|
|
691
|
+
role: 'admin-operator',
|
|
692
|
+
reason: 'User moderation access',
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const rotatedBeforeSuspension = await app.inject({
|
|
697
|
+
method: 'POST',
|
|
698
|
+
url: '/auth/refresh',
|
|
699
|
+
headers: {cookie: target.refreshCookie},
|
|
700
|
+
payload: {},
|
|
701
|
+
});
|
|
702
|
+
expect(rotatedBeforeSuspension.statusCode).toBe(200);
|
|
703
|
+
const rotatedRefreshCookie = cookieHeader(getSetCookie(rotatedBeforeSuspension));
|
|
704
|
+
|
|
705
|
+
const suspended = await app.inject({
|
|
706
|
+
method: 'POST',
|
|
707
|
+
url: `/admin/auth/users/${target.userId}/suspend`,
|
|
708
|
+
headers: authHeaders(owner.token, 'suspend-user'),
|
|
709
|
+
payload: {reason: 'Account security review'},
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
expect(suspended.statusCode).toBe(200);
|
|
713
|
+
expect(suspended.json()).toMatchObject({
|
|
714
|
+
id: target.userId,
|
|
715
|
+
status: 'suspended',
|
|
716
|
+
admin_role: null,
|
|
717
|
+
correlation_id: expect.any(String),
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const suspendedMe = await app.inject({
|
|
721
|
+
method: 'GET',
|
|
722
|
+
url: '/auth/me',
|
|
723
|
+
headers: {authorization: `Bearer ${target.token}`},
|
|
724
|
+
});
|
|
725
|
+
const suspendedLogin = await login(app, {email: target.email, password: target.password});
|
|
726
|
+
expect(suspendedMe.statusCode).toBe(401);
|
|
727
|
+
expect(suspendedLogin.statusCode).toBe(401);
|
|
728
|
+
|
|
729
|
+
const suspendedDbUser = (
|
|
730
|
+
await db().select({status: users.status}).from(users).where(eq(users.id, target.userId))
|
|
731
|
+
)[0];
|
|
732
|
+
const targetSessions = await db()
|
|
733
|
+
.select({sessionId: refreshTokens.sessionId, revokedAt: refreshTokens.revokedAt})
|
|
734
|
+
.from(refreshTokens)
|
|
735
|
+
.where(eq(refreshTokens.userId, target.userId));
|
|
736
|
+
expect(suspendedDbUser?.status).toBe('suspended');
|
|
737
|
+
expect(targetSessions).toHaveLength(2);
|
|
738
|
+
expect(targetSessions.every(({revokedAt}) => revokedAt instanceof Date)).toBe(true);
|
|
739
|
+
|
|
740
|
+
const suspendedRefresh = await app.inject({
|
|
741
|
+
method: 'POST',
|
|
742
|
+
url: '/auth/refresh',
|
|
743
|
+
headers: {cookie: rotatedRefreshCookie},
|
|
744
|
+
payload: {},
|
|
745
|
+
});
|
|
746
|
+
expect(suspendedRefresh.statusCode).toBe(401);
|
|
747
|
+
|
|
748
|
+
const suspensionEvent = (await db().select().from(authOutbox)).find(
|
|
749
|
+
(event) =>
|
|
750
|
+
event.eventType === ADMINISTRATION_ACTION_PERFORMED &&
|
|
751
|
+
(event.payload as {command?: string}).command === 'auth.user.suspend',
|
|
752
|
+
);
|
|
753
|
+
expect(suspensionEvent?.payload).toMatchObject({
|
|
754
|
+
actorId: owner.userId,
|
|
755
|
+
actorRole: 'admin-owner',
|
|
756
|
+
requiredRole: 'admin-operator',
|
|
757
|
+
targetId: target.userId,
|
|
758
|
+
reason: 'Account security review',
|
|
759
|
+
result: 'succeeded',
|
|
760
|
+
});
|
|
761
|
+
expect(JSON.stringify(suspensionEvent?.payload)).not.toContain('suspend-user');
|
|
762
|
+
expect(JSON.stringify(suspensionEvent?.payload)).not.toContain(target.password);
|
|
763
|
+
|
|
764
|
+
const repeatedSuspension = await app.inject({
|
|
765
|
+
method: 'POST',
|
|
766
|
+
url: `/admin/auth/users/${target.userId}/suspend`,
|
|
767
|
+
headers: authHeaders(owner.token, 'suspend-user'),
|
|
768
|
+
payload: {reason: 'Account security review'},
|
|
769
|
+
});
|
|
770
|
+
expect(repeatedSuspension.statusCode).toBe(200);
|
|
771
|
+
expect(repeatedSuspension.json().correlation_id).toBe(suspended.json().correlation_id);
|
|
772
|
+
|
|
773
|
+
const reusedSuspensionKey = await app.inject({
|
|
774
|
+
method: 'POST',
|
|
775
|
+
url: `/admin/auth/users/${target.userId}/suspend`,
|
|
776
|
+
headers: authHeaders(owner.token, 'suspend-user'),
|
|
777
|
+
payload: {reason: 'A different reason'},
|
|
778
|
+
});
|
|
779
|
+
expect(reusedSuspensionKey.statusCode).toBe(409);
|
|
780
|
+
expect(reusedSuspensionKey.json().code).toBe('idempotency-key-reused');
|
|
781
|
+
|
|
782
|
+
const reactivated = await app.inject({
|
|
783
|
+
method: 'POST',
|
|
784
|
+
url: `/admin/auth/users/${target.userId}/reactivate`,
|
|
785
|
+
headers: authHeaders(owner.token, 'reactivate-user'),
|
|
786
|
+
payload: {},
|
|
787
|
+
});
|
|
788
|
+
expect(reactivated.statusCode).toBe(200);
|
|
789
|
+
expect(reactivated.json()).toMatchObject({
|
|
790
|
+
id: target.userId,
|
|
791
|
+
status: 'active',
|
|
792
|
+
correlation_id: expect.any(String),
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
const oldRefreshAfterReactivation = await app.inject({
|
|
796
|
+
method: 'POST',
|
|
797
|
+
url: '/auth/refresh',
|
|
798
|
+
headers: {cookie: target.refreshCookie},
|
|
799
|
+
payload: {},
|
|
800
|
+
});
|
|
801
|
+
expect(oldRefreshAfterReactivation.statusCode).toBe(401);
|
|
802
|
+
const rotatedRefreshAfterReactivation = await app.inject({
|
|
803
|
+
method: 'POST',
|
|
804
|
+
url: '/auth/refresh',
|
|
805
|
+
headers: {cookie: rotatedRefreshCookie},
|
|
806
|
+
payload: {},
|
|
807
|
+
});
|
|
808
|
+
expect(rotatedRefreshAfterReactivation.statusCode).toBe(401);
|
|
809
|
+
|
|
810
|
+
const newLogin = await login(app, {email: target.email, password: target.password});
|
|
811
|
+
expect(newLogin.statusCode).toBe(200);
|
|
812
|
+
const secondLogin = await login(app, {email: target.email, password: target.password});
|
|
813
|
+
expect(secondLogin.statusCode).toBe(200);
|
|
814
|
+
|
|
815
|
+
const revoked = await app.inject({
|
|
816
|
+
method: 'POST',
|
|
817
|
+
url: `/admin/auth/users/${target.userId}/revoke-sessions`,
|
|
818
|
+
headers: authHeaders(owner.token, 'revoke-user-sessions'),
|
|
819
|
+
payload: {reason: 'End all active sessions'},
|
|
820
|
+
});
|
|
821
|
+
expect(revoked.statusCode).toBe(200);
|
|
822
|
+
expect(revoked.json()).toMatchObject({
|
|
823
|
+
id: target.userId,
|
|
824
|
+
status: 'active',
|
|
825
|
+
sessions_revoked: 2,
|
|
826
|
+
correlation_id: expect.any(String),
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const newRefresh = await app.inject({
|
|
830
|
+
method: 'POST',
|
|
831
|
+
url: '/auth/refresh',
|
|
832
|
+
headers: {cookie: cookieHeader(getSetCookie(newLogin))},
|
|
833
|
+
payload: {},
|
|
834
|
+
});
|
|
835
|
+
expect(newRefresh.statusCode).toBe(401);
|
|
836
|
+
const secondRefresh = await app.inject({
|
|
837
|
+
method: 'POST',
|
|
838
|
+
url: '/auth/refresh',
|
|
839
|
+
headers: {cookie: cookieHeader(getSetCookie(secondLogin))},
|
|
840
|
+
payload: {},
|
|
841
|
+
});
|
|
842
|
+
expect(secondRefresh.statusCode).toBe(401);
|
|
843
|
+
|
|
844
|
+
const repeatedRevoke = await app.inject({
|
|
845
|
+
method: 'POST',
|
|
846
|
+
url: `/admin/auth/users/${target.userId}/revoke-sessions`,
|
|
847
|
+
headers: authHeaders(owner.token, 'revoke-user-sessions'),
|
|
848
|
+
payload: {reason: 'End all active sessions'},
|
|
849
|
+
});
|
|
850
|
+
expect(repeatedRevoke.statusCode).toBe(200);
|
|
851
|
+
expect(repeatedRevoke.json().sessions_revoked).toBe(2);
|
|
852
|
+
expect(repeatedRevoke.json().correlation_id).toBe(revoked.json().correlation_id);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test('protects the final active owner from suspension', async () => {
|
|
856
|
+
const owner = await createVerifiedSession('admin-suspend-final-owner');
|
|
857
|
+
|
|
858
|
+
await app.inject({
|
|
859
|
+
method: 'POST',
|
|
860
|
+
url: '/admin/auth/admin-grants/bootstrap',
|
|
861
|
+
headers: authHeaders(owner.token, 'suspend-final-bootstrap'),
|
|
862
|
+
payload: {bootstrap_token: BOOTSTRAP_TOKEN},
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
const suspended = await app.inject({
|
|
866
|
+
method: 'POST',
|
|
867
|
+
url: `/admin/auth/users/${owner.userId}/suspend`,
|
|
868
|
+
headers: authHeaders(owner.token, 'suspend-final-owner'),
|
|
869
|
+
payload: {reason: 'Attempt to suspend final owner'},
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
expect(suspended.statusCode).toBe(409);
|
|
873
|
+
expect(suspended.json().code).toBe('last-owner');
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test('rejects user moderation by an observer', async () => {
|
|
877
|
+
const owner = await createVerifiedSession('admin-moderation-role-owner');
|
|
878
|
+
const observer = await createVerifiedSession('admin-moderation-role-observer');
|
|
879
|
+
const target = await createVerifiedSession('admin-moderation-role-target');
|
|
880
|
+
|
|
881
|
+
await app.inject({
|
|
882
|
+
method: 'POST',
|
|
883
|
+
url: '/admin/auth/admin-grants/bootstrap',
|
|
884
|
+
headers: authHeaders(owner.token, 'moderation-role-bootstrap'),
|
|
885
|
+
payload: {bootstrap_token: BOOTSTRAP_TOKEN},
|
|
886
|
+
});
|
|
887
|
+
await app.inject({
|
|
888
|
+
method: 'POST',
|
|
889
|
+
url: '/admin/auth/admin-grants',
|
|
890
|
+
headers: authHeaders(owner.token, 'moderation-role-grant'),
|
|
891
|
+
payload: {
|
|
892
|
+
user_id: observer.userId,
|
|
893
|
+
role: 'admin-observer',
|
|
894
|
+
reason: 'Read-only administration access',
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
const response = await app.inject({
|
|
899
|
+
method: 'POST',
|
|
900
|
+
url: `/admin/auth/users/${target.userId}/suspend`,
|
|
901
|
+
headers: authHeaders(observer.token, 'moderation-role-suspend'),
|
|
902
|
+
payload: {reason: 'Observer should not mutate users'},
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
expect(response.statusCode).toBe(403);
|
|
906
|
+
expect(response.json().code).toBe('forbidden');
|
|
907
|
+
});
|
|
665
908
|
});
|
|
@@ -2,6 +2,7 @@ import {AUTH_USER} from '@shipfox/api-auth-context';
|
|
|
2
2
|
import {
|
|
3
3
|
adminBootstrapStateSchema,
|
|
4
4
|
administratorUserLookupQuerySchema,
|
|
5
|
+
administratorUserMutationResponseSchema,
|
|
5
6
|
administratorUserSummarySchema,
|
|
6
7
|
bootstrapAdminOwnerBodySchema,
|
|
7
8
|
bootstrapAdminOwnerResponseSchema,
|
|
@@ -9,8 +10,12 @@ import {
|
|
|
9
10
|
grantAdminRoleResponseSchema,
|
|
10
11
|
listAdminGrantsQuerySchema,
|
|
11
12
|
listAdminGrantsResponseSchema,
|
|
13
|
+
reactivateAdministratorUserBodySchema,
|
|
12
14
|
revokeAdminGrantBodySchema,
|
|
13
15
|
revokeAdminGrantResponseSchema,
|
|
16
|
+
revokeAdministratorUserSessionsBodySchema,
|
|
17
|
+
revokeAdministratorUserSessionsResponseSchema,
|
|
18
|
+
suspendAdministratorUserBodySchema,
|
|
14
19
|
} from '@shipfox/api-auth-dto';
|
|
15
20
|
import {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';
|
|
16
21
|
import {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';
|
|
@@ -23,7 +28,10 @@ import {
|
|
|
23
28
|
getAdminBootstrapState,
|
|
24
29
|
grantAdministratorRole,
|
|
25
30
|
listAdministratorGrantSummaries,
|
|
31
|
+
reactivateAdministratorUser,
|
|
26
32
|
revokeAdministratorGrant,
|
|
33
|
+
revokeAdministratorUserSessions,
|
|
34
|
+
suspendAdministratorUser,
|
|
27
35
|
} from '#core/administration.js';
|
|
28
36
|
import type {AdminGrant} from '#core/entities/admin-grant.js';
|
|
29
37
|
import type {
|
|
@@ -101,6 +109,16 @@ function toAdministratorGrantSummaryDto(grant: AdministratorGrantSummary) {
|
|
|
101
109
|
};
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
function toAdministratorUserMutationDto(result: {
|
|
113
|
+
user: AdministratorUserSummary;
|
|
114
|
+
correlationId: string;
|
|
115
|
+
}) {
|
|
116
|
+
return {
|
|
117
|
+
...toAdministratorUserSummaryDto(result.user),
|
|
118
|
+
correlation_id: result.correlationId,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
104
122
|
function translateAdministrationError(error: unknown): never {
|
|
105
123
|
if (error instanceof AdminRoleRequiredError) {
|
|
106
124
|
throw new ClientError('Administrator role required', 'forbidden', {
|
|
@@ -229,6 +247,75 @@ const userLookupRoute = defineRoute({
|
|
|
229
247
|
},
|
|
230
248
|
});
|
|
231
249
|
|
|
250
|
+
const suspendUserRoute = defineRoute({
|
|
251
|
+
method: 'POST',
|
|
252
|
+
path: '/:userId/suspend',
|
|
253
|
+
description: 'Suspend a user and revoke all of its active sessions.',
|
|
254
|
+
schema: {
|
|
255
|
+
params: z.object({userId: z.string().uuid()}),
|
|
256
|
+
body: suspendAdministratorUserBodySchema,
|
|
257
|
+
response: {200: administratorUserMutationResponseSchema},
|
|
258
|
+
},
|
|
259
|
+
errorHandler: translateAdministrationError,
|
|
260
|
+
handler: async (request) => {
|
|
261
|
+
const result = await suspendAdministratorUser({
|
|
262
|
+
actorId: requireActorId(request),
|
|
263
|
+
userId: request.params.userId,
|
|
264
|
+
reason: request.body.reason,
|
|
265
|
+
idempotencyKey: requireIdempotencyKey(request),
|
|
266
|
+
correlationId: request.id,
|
|
267
|
+
});
|
|
268
|
+
return toAdministratorUserMutationDto(result);
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const reactivateUserRoute = defineRoute({
|
|
273
|
+
method: 'POST',
|
|
274
|
+
path: '/:userId/reactivate',
|
|
275
|
+
description: 'Reactivate a suspended user without restoring revoked sessions.',
|
|
276
|
+
schema: {
|
|
277
|
+
params: z.object({userId: z.string().uuid()}),
|
|
278
|
+
body: reactivateAdministratorUserBodySchema,
|
|
279
|
+
response: {200: administratorUserMutationResponseSchema},
|
|
280
|
+
},
|
|
281
|
+
errorHandler: translateAdministrationError,
|
|
282
|
+
handler: async (request) => {
|
|
283
|
+
const result = await reactivateAdministratorUser({
|
|
284
|
+
actorId: requireActorId(request),
|
|
285
|
+
userId: request.params.userId,
|
|
286
|
+
...(request.body.reason ? {reason: request.body.reason} : {}),
|
|
287
|
+
idempotencyKey: requireIdempotencyKey(request),
|
|
288
|
+
correlationId: request.id,
|
|
289
|
+
});
|
|
290
|
+
return toAdministratorUserMutationDto(result);
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const revokeUserSessionsRoute = defineRoute({
|
|
295
|
+
method: 'POST',
|
|
296
|
+
path: '/:userId/revoke-sessions',
|
|
297
|
+
description: 'Revoke all active sessions for a user without changing account status.',
|
|
298
|
+
schema: {
|
|
299
|
+
params: z.object({userId: z.string().uuid()}),
|
|
300
|
+
body: revokeAdministratorUserSessionsBodySchema,
|
|
301
|
+
response: {200: revokeAdministratorUserSessionsResponseSchema},
|
|
302
|
+
},
|
|
303
|
+
errorHandler: translateAdministrationError,
|
|
304
|
+
handler: async (request) => {
|
|
305
|
+
const result = await revokeAdministratorUserSessions({
|
|
306
|
+
actorId: requireActorId(request),
|
|
307
|
+
userId: request.params.userId,
|
|
308
|
+
...(request.body.reason ? {reason: request.body.reason} : {}),
|
|
309
|
+
idempotencyKey: requireIdempotencyKey(request),
|
|
310
|
+
correlationId: request.id,
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
...toAdministratorUserMutationDto(result),
|
|
314
|
+
sessions_revoked: result.sessionsRevoked,
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
232
319
|
const grantRoute = defineRoute({
|
|
233
320
|
method: 'POST',
|
|
234
321
|
path: '/',
|
|
@@ -290,5 +377,5 @@ export const administrationBootstrapRoutes: RouteGroup = {
|
|
|
290
377
|
export const administrationUserRoutes: RouteGroup = {
|
|
291
378
|
prefix: '/admin/auth/users',
|
|
292
379
|
auth: AUTH_USER,
|
|
293
|
-
routes: [userLookupRoute],
|
|
380
|
+
routes: [userLookupRoute, suspendUserRoute, reactivateUserRoute, revokeUserSessionsRoute],
|
|
294
381
|
};
|