mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: sign out of all sessions (#1797)
This commit is contained in:
@ -2,6 +2,7 @@ import { sha256 } from '@oslojs/crypto/sha2';
|
||||
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
|
||||
import { type Session, type User, UserSecurityAuditLogType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -129,18 +130,46 @@ export const validateSessionToken = async (token: string): Promise<SessionValida
|
||||
return { session, user, isAuthenticated: true };
|
||||
};
|
||||
|
||||
export const invalidateSession = async (
|
||||
sessionId: string,
|
||||
metadata: RequestMetadata,
|
||||
): Promise<void> => {
|
||||
const session = await prisma.session.delete({ where: { id: sessionId } });
|
||||
type InvalidateSessionsOptions = {
|
||||
userId: number;
|
||||
sessionIds: string[];
|
||||
metadata: RequestMetadata;
|
||||
isRevoke?: boolean;
|
||||
};
|
||||
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId: session.userId,
|
||||
ipAddress: metadata.ipAddress,
|
||||
userAgent: metadata.userAgent,
|
||||
type: UserSecurityAuditLogType.SIGN_OUT,
|
||||
},
|
||||
export const invalidateSessions = async ({
|
||||
userId,
|
||||
sessionIds,
|
||||
metadata,
|
||||
isRevoke,
|
||||
}: InvalidateSessionsOptions): Promise<void> => {
|
||||
if (sessionIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const { count } = await tx.session.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
id: { in: sessionIds },
|
||||
},
|
||||
});
|
||||
|
||||
if (count !== sessionIds.length) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'One or more sessions are not valid.',
|
||||
});
|
||||
}
|
||||
|
||||
await tx.userSecurityAuditLog.createMany({
|
||||
data: sessionIds.map(() => ({
|
||||
userId,
|
||||
ipAddress: metadata.ipAddress,
|
||||
userAgent: metadata.userAgent,
|
||||
type: isRevoke
|
||||
? UserSecurityAuditLogType.SESSION_REVOKED
|
||||
: UserSecurityAuditLogType.SIGN_OUT,
|
||||
})),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { Session } from '@prisma/client';
|
||||
import type { Context } from 'hono';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AuthenticationErrorCode } from '../errors/error-codes';
|
||||
import type { SessionValidationResult } from '../session/session';
|
||||
@ -37,6 +39,33 @@ export const getOptionalSession = async (
|
||||
return await validateSessionToken(sessionId);
|
||||
};
|
||||
|
||||
export type ActiveSession = Omit<Session, 'sessionToken'>;
|
||||
|
||||
export const getActiveSessions = async (c: Context | Request): Promise<ActiveSession[]> => {
|
||||
const { user } = await getSession(c);
|
||||
|
||||
return await prisma.session.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
expiresAt: {
|
||||
gt: new Date(),
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
expiresAt: true,
|
||||
updatedAt: true,
|
||||
createdAt: true,
|
||||
ipAddress: true,
|
||||
userAgent: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Todo: (RR7) Rethink, this is pretty sketchy.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user