feat: add user security audit logs (#884)

## Description

Adds the ability to see the events relating to the account.

Event data includes:
- Device
- IP Address
- Time
- Action

Actions are:

- Profile update
- Account linked to SSO (Example user signs in with Google after
creating a email/password account)
- Enable 2FA
- Disable 2FA
- Reset password
- Update password
- Sign out
- Sign in
- Sign in fail
- Sign in 2FA fail

## Changes

- Added audit logs
- Updated 2FA dialogs to have consistent footers
- Update `/settings/security/page` layout

## Testing Performed

Tested events:


![image](https://github.com/documenso/documenso/assets/20962767/8ab9e055-aa58-4621-86fe-24681cce6418)

More tested events:


![image](https://github.com/documenso/documenso/assets/20962767/b6b42e13-626e-4fed-8e1a-097e5324aa6d)

## Checklist

- [X] I have tested these changes locally and they work as expected.
- [X] I have followed the project's coding style guidelines.

## Additional Notes

- Not sure if we really want to record the sign out event or not
- Might want to design breadcrumbs for nested setting pages
This commit is contained in:
Lucas Smith
2024-02-02 09:42:25 +11:00
committed by GitHub
32 changed files with 816 additions and 184 deletions

View File

@ -19,27 +19,14 @@ export const AuthenticatorApp = ({ isTwoFactorEnabled }: AuthenticatorAppProps)
return (
<>
<div className="mt-4 flex flex-col justify-between gap-4 rounded-lg border p-4 md:flex-row md:items-center md:gap-8">
<div className="flex-1">
<p>Authenticator app</p>
<p className="text-muted-foreground mt-2 max-w-[50ch] text-sm">
Create one-time passwords that serve as a secondary authentication method for confirming
your identity when requested during the sign-in process.
</p>
</div>
<div>
{isTwoFactorEnabled ? (
<Button variant="destructive" onClick={() => setModalState('disable')} size="sm">
Disable 2FA
</Button>
) : (
<Button onClick={() => setModalState('enable')} size="sm">
Enable 2FA
</Button>
)}
</div>
<div className="flex-shrink-0">
{isTwoFactorEnabled ? (
<Button variant="destructive" onClick={() => setModalState('disable')}>
Disable 2FA
</Button>
) : (
<Button onClick={() => setModalState('enable')}>Enable 2FA</Button>
)}
</div>
<EnableAuthenticatorAppDialog

View File

@ -11,6 +11,7 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
@ -145,8 +146,8 @@ export const DisableAuthenticatorAppDialog = ({
/>
</fieldset>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
@ -157,7 +158,7 @@ export const DisableAuthenticatorAppDialog = ({
>
Disable 2FA
</Button>
</div>
</DialogFooter>
</form>
</Form>
</DialogContent>

View File

@ -15,6 +15,7 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
@ -190,15 +191,15 @@ export const EnableAuthenticatorAppDialog = ({
)}
/>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" loading={isSetupTwoFactorAuthenticationSubmitting}>
Continue
</Button>
</div>
</DialogFooter>
</form>
</Form>
);
@ -251,15 +252,15 @@ export const EnableAuthenticatorAppDialog = ({
)}
/>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" loading={isEnableTwoFactorAuthenticationSubmitting}>
Enable 2FA
</Button>
</div>
</DialogFooter>
</form>
</Form>
))

View File

@ -7,7 +7,6 @@ import { Button } from '@documenso/ui/primitives/button';
import { ViewRecoveryCodesDialog } from './view-recovery-codes-dialog';
type RecoveryCodesProps = {
// backupCodes: string[] | null;
isTwoFactorEnabled: boolean;
};
@ -16,22 +15,13 @@ export const RecoveryCodes = ({ isTwoFactorEnabled }: RecoveryCodesProps) => {
return (
<>
<div className="mt-4 flex flex-col justify-between gap-4 rounded-lg border p-4 md:flex-row md:items-center md:gap-8">
<div className="flex-1">
<p>Recovery Codes</p>
<p className="text-muted-foreground mt-2 max-w-[50ch] text-sm">
Recovery codes are used to access your account in the event that you lose access to your
authenticator app.
</p>
</div>
<div>
<Button onClick={() => setIsOpen(true)} disabled={!isTwoFactorEnabled} size="sm">
View Codes
</Button>
</div>
</div>
<Button
className="flex-shrink-0"
onClick={() => setIsOpen(true)}
disabled={!isTwoFactorEnabled}
>
View Codes
</Button>
<ViewRecoveryCodesDialog
key={isOpen ? 'open' : 'closed'}

View File

@ -11,6 +11,7 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
@ -119,15 +120,15 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode
)}
/>
<div className="flex w-full items-center justify-between">
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" loading={isViewRecoveryCodesSubmitting}>
Continue
</Button>
</div>
</DialogFooter>
</form>
</Form>
);

View File

@ -137,7 +137,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
/>
</fieldset>
<div className="mt-4">
<div className="ml-auto mt-4">
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? 'Updating password...' : 'Update password'}
</Button>

View File

@ -12,7 +12,13 @@ import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@documenso/ui/primitives/dialog';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
@ -111,7 +117,6 @@ export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) =
const result = await signIn('credentials', {
...credentials,
callbackUrl: LOGIN_REDIRECT_PATH,
redirect: false,
});
@ -270,21 +275,23 @@ export const SignInForm = ({ className, isGoogleSSOEnabled }: SignInFormProps) =
)}
/>
)}
<DialogFooter className="mt-4">
<Button
type="button"
variant="secondary"
onClick={onToggleTwoFactorAuthenticationMethodClick}
>
{twoFactorAuthenticationMethod === 'totp'
? 'Use Backup Code'
: 'Use Authenticator'}
</Button>
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
</DialogFooter>
</fieldset>
<div className="mt-4 flex items-center justify-between">
<Button
type="button"
variant="ghost"
onClick={onToggleTwoFactorAuthenticationMethodClick}
>
{twoFactorAuthenticationMethod === 'totp' ? 'Use Backup Code' : 'Use Authenticator'}
</Button>
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>