rl-core-front 0.1.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.
Files changed (112) hide show
  1. package/README.md +129 -0
  2. package/package.json +113 -0
  3. package/src/_services/api/axios.factory.ts +97 -0
  4. package/src/_services/api/list-query.ts +26 -0
  5. package/src/_services/api/schema.d.ts +2527 -0
  6. package/src/_services/auth/index.ts +127 -0
  7. package/src/_utils/advanced-filter.ts +386 -0
  8. package/src/_utils/calendar.ts +103 -0
  9. package/src/_utils/crop-image.ts +65 -0
  10. package/src/_utils/filter.ts +313 -0
  11. package/src/_utils/format.ts +35 -0
  12. package/src/_utils/initials.ts +24 -0
  13. package/src/_utils/password.ts +7 -0
  14. package/src/components/app-shell.tsx +314 -0
  15. package/src/components/breadcrumbs.tsx +55 -0
  16. package/src/components/index.ts +7 -0
  17. package/src/components/language-selector.tsx +51 -0
  18. package/src/components/password-field.tsx +40 -0
  19. package/src/components/theme-toggle.tsx +27 -0
  20. package/src/components/ui/alert.tsx +27 -0
  21. package/src/components/ui/avatar.tsx +50 -0
  22. package/src/components/ui/badge.tsx +36 -0
  23. package/src/components/ui/button.tsx +62 -0
  24. package/src/components/ui/card.tsx +86 -0
  25. package/src/components/ui/checkbox.tsx +30 -0
  26. package/src/components/ui/column-visibility-modal.tsx +110 -0
  27. package/src/components/ui/confirm-dialog.tsx +69 -0
  28. package/src/components/ui/data-table.tsx +401 -0
  29. package/src/components/ui/date-range-picker.tsx +250 -0
  30. package/src/components/ui/dialog.tsx +138 -0
  31. package/src/components/ui/dropdown-menu.tsx +108 -0
  32. package/src/components/ui/field.tsx +20 -0
  33. package/src/components/ui/filter-chips.tsx +88 -0
  34. package/src/components/ui/filter-field.tsx +137 -0
  35. package/src/components/ui/filter-group.tsx +155 -0
  36. package/src/components/ui/filter-rule.tsx +221 -0
  37. package/src/components/ui/filter-sheet.tsx +201 -0
  38. package/src/components/ui/index.ts +30 -0
  39. package/src/components/ui/input.tsx +25 -0
  40. package/src/components/ui/label.tsx +23 -0
  41. package/src/components/ui/row-actions.tsx +32 -0
  42. package/src/components/ui/select.tsx +97 -0
  43. package/src/components/ui/separator.tsx +31 -0
  44. package/src/components/ui/sheet.tsx +119 -0
  45. package/src/components/ui/sonner.tsx +28 -0
  46. package/src/components/ui/spinner.tsx +12 -0
  47. package/src/components/ui/switch.tsx +29 -0
  48. package/src/components/ui/table.tsx +84 -0
  49. package/src/components/ui/tabs.tsx +52 -0
  50. package/src/components/user-avatar.tsx +54 -0
  51. package/src/contexts/auth-context.tsx +90 -0
  52. package/src/contexts/color-mode-context.tsx +105 -0
  53. package/src/contexts/i18n-context.tsx +76 -0
  54. package/src/contexts/index.ts +6 -0
  55. package/src/contexts/socket-context.tsx +153 -0
  56. package/src/contexts/toast-context.tsx +40 -0
  57. package/src/features/audit/audit-screen.tsx +299 -0
  58. package/src/features/audit/hooks/use-audit-trail.ts +68 -0
  59. package/src/features/audit/services/audit.service.ts +24 -0
  60. package/src/features/dashboard/dashboard-screen.tsx +73 -0
  61. package/src/features/login/components/backup-codes-dialog.tsx +66 -0
  62. package/src/features/login/components/index.ts +5 -0
  63. package/src/features/login/components/login-form.tsx +62 -0
  64. package/src/features/login/components/two-factor-setup.tsx +71 -0
  65. package/src/features/login/components/two-factor-verify.tsx +56 -0
  66. package/src/features/login/hooks/use-login-flow.ts +119 -0
  67. package/src/features/login/login-screen.tsx +83 -0
  68. package/src/features/login/validation/schemas.ts +30 -0
  69. package/src/features/logs/hooks/use-error-logs.ts +65 -0
  70. package/src/features/logs/hooks/use-request-logs.ts +65 -0
  71. package/src/features/logs/logs-screen.tsx +31 -0
  72. package/src/features/logs/panels/error-logs-panel.tsx +183 -0
  73. package/src/features/logs/panels/index.ts +3 -0
  74. package/src/features/logs/panels/request-logs-panel.tsx +124 -0
  75. package/src/features/logs/services/logs.service.ts +68 -0
  76. package/src/features/notifications/components/notification-item.tsx +67 -0
  77. package/src/features/notifications/hooks/use-notifications.ts +78 -0
  78. package/src/features/notifications/notifications-center.tsx +119 -0
  79. package/src/features/notifications/services/notifications.service.ts +38 -0
  80. package/src/features/profile/components/avatar-crop-dialog.tsx +133 -0
  81. package/src/features/profile/components/index.ts +2 -0
  82. package/src/features/profile/force-password-change.tsx +114 -0
  83. package/src/features/profile/profile-screen.tsx +385 -0
  84. package/src/features/rbac/panels/actions-panel.tsx +20 -0
  85. package/src/features/rbac/panels/catalog-panel.tsx +243 -0
  86. package/src/features/rbac/panels/groups-panel.tsx +269 -0
  87. package/src/features/rbac/panels/index.ts +8 -0
  88. package/src/features/rbac/panels/permissions-panel.tsx +204 -0
  89. package/src/features/rbac/panels/resources-panel.tsx +20 -0
  90. package/src/features/rbac/panels/roles-panel.tsx +224 -0
  91. package/src/features/rbac/panels/scopes-panel.tsx +20 -0
  92. package/src/features/rbac/rbac-screen.tsx +80 -0
  93. package/src/features/rbac/services/rbac.service.ts +110 -0
  94. package/src/features/recovery/forgot-password-screen.tsx +69 -0
  95. package/src/features/recovery/reset-password-screen.tsx +163 -0
  96. package/src/features/users/components/user-form-dialog.tsx +187 -0
  97. package/src/features/users/hooks/use-users.ts +181 -0
  98. package/src/features/users/services/users.service.ts +83 -0
  99. package/src/features/users/users-screen.tsx +331 -0
  100. package/src/hooks/use-column-visibility.ts +54 -0
  101. package/src/hooks/use-confirm.tsx +70 -0
  102. package/src/hooks/use-filter-schema.ts +85 -0
  103. package/src/hooks/use-filters.ts +148 -0
  104. package/src/hooks/use-list-query.ts +69 -0
  105. package/src/hooks/use-request.ts +114 -0
  106. package/src/i18n/messages/en.ts +296 -0
  107. package/src/i18n/messages/pt.ts +305 -0
  108. package/src/index.ts +60 -0
  109. package/src/lib/utils.ts +6 -0
  110. package/src/middleware.ts +42 -0
  111. package/src/styles/core.css +142 -0
  112. package/tailwind-preset.ts +103 -0
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import { AlertTriangle } from "lucide-react";
4
+ import type { JSX } from "react";
5
+
6
+ import {
7
+ Button,
8
+ Dialog,
9
+ DialogContent,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from "#core/components/ui";
14
+ import { useI18n, useToast } from "#core/contexts";
15
+
16
+ interface Props {
17
+ open: boolean;
18
+ codes: string[];
19
+ onClose: () => void;
20
+ }
21
+
22
+ export function BackupCodesDialog({ open, codes, onClose }: Props): JSX.Element {
23
+ const { notify } = useToast();
24
+ const { t } = useI18n();
25
+
26
+ const copyAll = async (): Promise<void> => {
27
+ await navigator.clipboard.writeText(codes.join("\n"));
28
+ notify(t("auth.codesCopied"), "success");
29
+ };
30
+
31
+ return (
32
+ <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
33
+ {/*
34
+ Clique fora continua bloqueado de propósito — o usuário precisa guardar
35
+ os códigos antes de sair. `X` e `Esc` fecham porque só o `onOpenChange`
36
+ avisa o componente; sem ele o modal não fechava por nenhum caminho.
37
+ */}
38
+ <DialogContent
39
+ className="max-w-md"
40
+ onInteractOutside={(e) => e.preventDefault()}
41
+ >
42
+ <DialogHeader>
43
+ <DialogTitle>{t("auth.backupTitle")}</DialogTitle>
44
+ </DialogHeader>
45
+
46
+ <div className="flex items-start gap-2 rounded-md border border-warning/30 bg-warning/10 px-3 py-2 text-sm text-warning">
47
+ <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
48
+ <span>{t("auth.backupWarning")}</span>
49
+ </div>
50
+
51
+ <div className="grid grid-cols-2 gap-2 rounded-lg bg-muted p-4 font-mono text-sm text-foreground">
52
+ {codes.map((c) => (
53
+ <span key={c}>{c}</span>
54
+ ))}
55
+ </div>
56
+
57
+ <DialogFooter className="gap-2">
58
+ <Button variant="outline" onClick={copyAll}>
59
+ {t("auth.copy")}
60
+ </Button>
61
+ <Button onClick={onClose}>{t("auth.savedContinue")}</Button>
62
+ </DialogFooter>
63
+ </DialogContent>
64
+ </Dialog>
65
+ );
66
+ }
@@ -0,0 +1,5 @@
1
+ // Barrel da pasta — importe daqui de fora; dentro da pasta use o caminho direto.
2
+ export * from "./backup-codes-dialog";
3
+ export * from "./login-form";
4
+ export * from "./two-factor-setup";
5
+ export * from "./two-factor-verify";
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { yupResolver } from "@hookform/resolvers/yup";
4
+ import NextLink from "next/link";
5
+ import type { JSX } from "react";
6
+ import { useMemo } from "react";
7
+ import { useForm } from "react-hook-form";
8
+
9
+ import { PasswordField } from "#core/components";
10
+ import { Button, Field, Input } from "#core/components/ui";
11
+ import { useI18n } from "#core/contexts";
12
+ import {
13
+ LoginFormData,
14
+ makeLoginSchema,
15
+ } from "#core/features/login/validation/schemas";
16
+
17
+ interface Props {
18
+ loading: boolean;
19
+ onSubmit: (email: string, password: string) => void;
20
+ }
21
+
22
+ export function LoginForm({ loading, onSubmit }: Props): JSX.Element {
23
+ const { t } = useI18n();
24
+ const schema = useMemo(() => makeLoginSchema(t), [t]);
25
+ const {
26
+ register,
27
+ handleSubmit,
28
+ formState: { errors },
29
+ } = useForm<LoginFormData>({ resolver: yupResolver(schema) });
30
+
31
+ return (
32
+ <form
33
+ onSubmit={handleSubmit((d) => onSubmit(d.email, d.password))}
34
+ className="flex flex-col gap-4"
35
+ >
36
+ <Field label={t("auth.email")} error={errors.email?.message}>
37
+ <Input
38
+ type="email"
39
+ autoComplete="email"
40
+ autoFocus
41
+ {...register("email")}
42
+ />
43
+ </Field>
44
+ <Field label={t("auth.password")} error={errors.password?.message}>
45
+ <PasswordField
46
+ autoComplete="current-password"
47
+ error={!!errors.password}
48
+ {...register("password")}
49
+ />
50
+ </Field>
51
+ <Button type="submit" size="lg" disabled={loading}>
52
+ {loading ? t("auth.entering") : t("auth.enter")}
53
+ </Button>
54
+ <NextLink
55
+ href="/forgot-password"
56
+ className="self-center text-sm text-primary hover:underline"
57
+ >
58
+ {t("auth.forgotPassword")}
59
+ </NextLink>
60
+ </form>
61
+ );
62
+ }
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ /* eslint-disable @next/next/no-img-element */
4
+ import { yupResolver } from "@hookform/resolvers/yup";
5
+ import type { JSX } from "react";
6
+ import { useMemo } from "react";
7
+ import { useForm } from "react-hook-form";
8
+
9
+ import { TotpSetup } from "#core/_services/auth";
10
+ import { Button, Field, Input } from "#core/components/ui";
11
+ import { useI18n } from "#core/contexts";
12
+ import {
13
+ CodeFormData,
14
+ makeCodeSchema,
15
+ } from "#core/features/login/validation/schemas";
16
+
17
+ interface Props {
18
+ setup: TotpSetup;
19
+ loading: boolean;
20
+ onConfirm: (code: string) => void;
21
+ }
22
+
23
+ export function TwoFactorSetup({ setup, loading, onConfirm }: Props): JSX.Element {
24
+ const { t } = useI18n();
25
+ const schema = useMemo(() => makeCodeSchema(t), [t]);
26
+ const {
27
+ register,
28
+ handleSubmit,
29
+ formState: { errors },
30
+ } = useForm<CodeFormData>({ resolver: yupResolver(schema) });
31
+
32
+ return (
33
+ <div className="flex flex-col gap-4">
34
+ <p className="text-sm text-muted-foreground">{t("auth.setupHint")}</p>
35
+
36
+ <div className="flex justify-center">
37
+ <div className="rounded-lg border border-border bg-white p-3">
38
+ <img
39
+ src={setup.qrDataUrl}
40
+ alt="QR code 2FA"
41
+ width={200}
42
+ height={200}
43
+ />
44
+ </div>
45
+ </div>
46
+
47
+ <p className="text-center text-xs text-muted-foreground">
48
+ {t("auth.manualKey")}
49
+ <br />
50
+ <span className="break-all font-mono">{setup.secret}</span>
51
+ </p>
52
+
53
+ <form
54
+ onSubmit={handleSubmit((d) => onConfirm(d.code))}
55
+ className="flex flex-col gap-4"
56
+ >
57
+ <Field label={t("auth.codeApp")} error={errors.code?.message}>
58
+ <Input
59
+ autoFocus
60
+ inputMode="numeric"
61
+ maxLength={6}
62
+ {...register("code")}
63
+ />
64
+ </Field>
65
+ <Button type="submit" size="lg" disabled={loading}>
66
+ {loading ? t("auth.confirming") : t("auth.confirmActivate")}
67
+ </Button>
68
+ </form>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { yupResolver } from "@hookform/resolvers/yup";
4
+ import type { JSX } from "react";
5
+ import { useMemo } from "react";
6
+ import { useForm } from "react-hook-form";
7
+
8
+ import { Button, Field, Input } from "#core/components/ui";
9
+ import { useI18n } from "#core/contexts";
10
+ import {
11
+ CodeFormData,
12
+ makeCodeSchema,
13
+ } from "#core/features/login/validation/schemas";
14
+
15
+ interface Props {
16
+ loading: boolean;
17
+ onVerify: (code: string, method?: "totp" | "email" | "backup") => void;
18
+ onRequestEmail: () => void;
19
+ }
20
+
21
+ export function TwoFactorVerify({ loading, onVerify, onRequestEmail }: Props): JSX.Element {
22
+ const { t } = useI18n();
23
+ const schema = useMemo(() => makeCodeSchema(t), [t]);
24
+ const {
25
+ register,
26
+ handleSubmit,
27
+ formState: { errors },
28
+ } = useForm<CodeFormData>({ resolver: yupResolver(schema) });
29
+
30
+ return (
31
+ <div className="flex flex-col gap-4">
32
+ <p className="text-sm text-muted-foreground">{t("auth.verifyHint")}</p>
33
+
34
+ <form
35
+ onSubmit={handleSubmit((d) => onVerify(d.code))}
36
+ className="flex flex-col gap-4"
37
+ >
38
+ <Field label={t("auth.codeAny")} error={errors.code?.message}>
39
+ <Input autoFocus {...register("code")} />
40
+ </Field>
41
+ <Button type="submit" size="lg" disabled={loading}>
42
+ {loading ? t("auth.verifying") : t("auth.verify")}
43
+ </Button>
44
+ </form>
45
+
46
+ <Button
47
+ variant="link"
48
+ size="sm"
49
+ onClick={onRequestEmail}
50
+ disabled={loading}
51
+ >
52
+ {loading ? t("auth.sendingEmail") : t("auth.receiveByEmail")}
53
+ </Button>
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import {
5
+ Dispatch,
6
+ SetStateAction,
7
+ useCallback,
8
+ useState,
9
+ } from "react";
10
+
11
+ import { authService, TotpSetup } from "#core/_services/auth";
12
+ import { useAuth, useI18n, useToast } from "#core/contexts";
13
+ import { useRequest } from "#core/hooks/use-request";
14
+
15
+ export type LoginStep = "credentials" | "setup" | "verify" | "backup";
16
+
17
+ /**
18
+ * Máquina de estados do login com 2FA obrigatório:
19
+ * credentials → (setup → verify | verify) → [backup] → /dashboard
20
+ */
21
+ export interface UseLoginFlowResult {
22
+ step: LoginStep;
23
+ setStep: Dispatch<SetStateAction<LoginStep>>;
24
+ setup: TotpSetup | null;
25
+ backupCodes: string[];
26
+ loading: boolean;
27
+ error: string | null;
28
+ setError: Dispatch<SetStateAction<string | null>>;
29
+ submitCredentials: (email: string, password: string) => Promise<void>;
30
+ submitCode: (
31
+ code: string,
32
+ method?: "totp" | "email" | "backup",
33
+ ) => Promise<void>;
34
+ requestEmailCode: () => Promise<void>;
35
+ finishBackup: () => Promise<void>;
36
+ }
37
+
38
+ export function useLoginFlow(): UseLoginFlowResult {
39
+ const router = useRouter();
40
+ const { notify } = useToast();
41
+ const { t } = useI18n();
42
+ const { run, loading, error, setError } = useRequest();
43
+ const { refreshProfile } = useAuth();
44
+
45
+ const [step, setStep] = useState<LoginStep>("credentials");
46
+ const [pendingToken, setPendingToken] = useState("");
47
+ const [setup, setSetup] = useState<TotpSetup | null>(null);
48
+ const [backupCodes, setBackupCodes] = useState<string[]>([]);
49
+
50
+ const submitCredentials = useCallback(
51
+ async (email: string, password: string) => {
52
+ const res = await run(() => authService.login(email, password));
53
+ if (!res) {return;}
54
+ setPendingToken(res.pendingToken);
55
+
56
+ if (res.status === "SETUP_REQUIRED") {
57
+ const s = await run(() => authService.setupTotp(res.pendingToken));
58
+ if (!s) {return;}
59
+ setSetup(s);
60
+ setStep("setup");
61
+ } else {
62
+ setStep("verify");
63
+ // Já dispara o código por email ao chegar na tela — evita depender do
64
+ // usuário ter o app autenticador à mão; ele ainda pode digitar o TOTP direto.
65
+ authService
66
+ .requestEmailCode(res.pendingToken)
67
+ .then((r) => {
68
+ if (r?.sent) {notify(t("auth.codeSent"), "success");}
69
+ })
70
+ .catch(() => {});
71
+ }
72
+ },
73
+ [run, notify, t],
74
+ );
75
+
76
+ const submitCode = useCallback(
77
+ async (code: string, method?: "totp" | "email" | "backup") => {
78
+ const res = await run(() =>
79
+ authService.verify(pendingToken, code, method),
80
+ );
81
+ if (!res) {return;}
82
+
83
+ if (res.backupCodes?.length) {
84
+ setBackupCodes(res.backupCodes);
85
+ setStep("backup");
86
+ } else {
87
+ await refreshProfile();
88
+ notify(t("auth.loginSuccess"), "success");
89
+ router.replace("/dashboard");
90
+ }
91
+ },
92
+ [run, pendingToken, notify, router, t, refreshProfile],
93
+ );
94
+
95
+ const requestEmailCode = useCallback(async () => {
96
+ const res = await run(() => authService.requestEmailCode(pendingToken));
97
+ if (res?.sent) {notify(t("auth.codeSent"), "success");}
98
+ }, [run, pendingToken, notify, t]);
99
+
100
+ const finishBackup = useCallback(async () => {
101
+ await refreshProfile();
102
+ notify(t("auth.twoFactorConfigured"), "success");
103
+ router.replace("/dashboard");
104
+ }, [notify, router, t, refreshProfile]);
105
+
106
+ return {
107
+ step,
108
+ setStep,
109
+ setup,
110
+ backupCodes,
111
+ loading,
112
+ error,
113
+ setError,
114
+ submitCredentials,
115
+ submitCode,
116
+ requestEmailCode,
117
+ finishBackup,
118
+ };
119
+ }
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ import { Lock } from "lucide-react";
4
+ import type { JSX } from "react";
5
+ import { useEffect } from "react";
6
+
7
+ import { LanguageSelector, ThemeToggle } from "#core/components";
8
+ import { Card } from "#core/components/ui";
9
+ import { useI18n } from "#core/contexts";
10
+ import {
11
+ BackupCodesDialog,
12
+ LoginForm,
13
+ TwoFactorSetup,
14
+ TwoFactorVerify,
15
+ } from "#core/features/login/components";
16
+ import { useLoginFlow } from "#core/features/login/hooks/use-login-flow";
17
+
18
+ export function LoginScreen(): JSX.Element {
19
+ const flow = useLoginFlow();
20
+ const { t } = useI18n();
21
+
22
+ const titles: Record<string, string> = {
23
+ credentials: t("auth.loginTitle"),
24
+ setup: t("auth.twoFactorSetupTitle"),
25
+ verify: t("auth.twoFactorVerifyTitle"),
26
+ backup: t("auth.backupTitle"),
27
+ };
28
+
29
+ useEffect(() => {
30
+ flow.setError(null);
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
32
+ }, [flow.step]);
33
+
34
+ return (
35
+ <div className="relative flex min-h-screen items-center justify-center bg-background p-4">
36
+ <div className="absolute right-3 top-3 flex gap-1">
37
+ <LanguageSelector />
38
+ <ThemeToggle />
39
+ </div>
40
+
41
+ <Card className="w-full max-w-md p-8">
42
+ <div className="mb-6 flex flex-col items-center">
43
+ <div className="mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-primary text-primary-foreground">
44
+ <Lock className="h-6 w-6" />
45
+ </div>
46
+ <h1 className="text-xl font-semibold">{titles[flow.step]}</h1>
47
+ </div>
48
+
49
+ {flow.error && (
50
+ <div className="mb-4 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
51
+ {flow.error}
52
+ </div>
53
+ )}
54
+
55
+ {flow.step === "credentials" && (
56
+ <LoginForm loading={flow.loading} onSubmit={flow.submitCredentials} />
57
+ )}
58
+
59
+ {flow.step === "setup" && flow.setup && (
60
+ <TwoFactorSetup
61
+ setup={flow.setup}
62
+ loading={flow.loading}
63
+ onConfirm={(code) => flow.submitCode(code, "totp")}
64
+ />
65
+ )}
66
+
67
+ {flow.step === "verify" && (
68
+ <TwoFactorVerify
69
+ loading={flow.loading}
70
+ onVerify={flow.submitCode}
71
+ onRequestEmail={flow.requestEmailCode}
72
+ />
73
+ )}
74
+
75
+ <BackupCodesDialog
76
+ open={flow.step === "backup"}
77
+ codes={flow.backupCodes}
78
+ onClose={flow.finishBackup}
79
+ />
80
+ </Card>
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,30 @@
1
+ import * as yup from "yup";
2
+
3
+ type TFn = (key: string) => string;
4
+
5
+ export interface LoginFormData {
6
+ email: string;
7
+ password: string;
8
+ }
9
+
10
+ export interface CodeFormData {
11
+ code: string;
12
+ }
13
+
14
+ export const makeLoginSchema = (t: TFn): yup.ObjectSchema<LoginFormData> =>
15
+ yup.object({
16
+ email: yup
17
+ .string()
18
+ .required(t("validation.required"))
19
+ .email(t("validation.email")),
20
+ password: yup.string().required(t("validation.required")),
21
+ });
22
+
23
+ export const makeCodeSchema = (t: TFn): yup.ObjectSchema<CodeFormData> =>
24
+ yup.object({
25
+ code: yup
26
+ .string()
27
+ .required(t("validation.required"))
28
+ .min(6, t("validation.codeShort"))
29
+ .max(9),
30
+ });
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import type { SortingState } from "@tanstack/react-table";
4
+ import {
5
+ Dispatch,
6
+ SetStateAction,
7
+ useCallback,
8
+ useEffect,
9
+ useState,
10
+ } from "react";
11
+
12
+ import { ErrorLog, logsService } from "#core/features/logs/services/logs.service";
13
+ import type { FiltersState } from "#core/hooks/use-filters";
14
+ import { useListQuery } from "#core/hooks/use-list-query";
15
+ import { useRequest } from "#core/hooks/use-request";
16
+
17
+ export interface UseErrorLogsResult {
18
+ rows: ErrorLog[];
19
+ total: number;
20
+ page: number;
21
+ pageSize: number;
22
+ sorting: SortingState;
23
+ loading: boolean;
24
+ setPage: Dispatch<SetStateAction<number>>;
25
+ setPageSize: Dispatch<SetStateAction<number>>;
26
+ setSorting: Dispatch<SetStateAction<SortingState>>;
27
+ /** Filtros dinâmicos — repassar para o `DataTable`. */
28
+ filters: FiltersState;
29
+ refresh: () => Promise<void>;
30
+ }
31
+
32
+ export function useErrorLogs(): UseErrorLogsResult {
33
+ const { run, loading } = useRequest();
34
+
35
+ const list = useListQuery("/audit/errors/filter-schema", 20);
36
+
37
+ const [rows, setRows] = useState<ErrorLog[]>([]);
38
+ const [total, setTotal] = useState(0);
39
+
40
+ const fetch = useCallback(async () => {
41
+ const res = await run(() => logsService.listErrors(list.query));
42
+ if (res) {
43
+ setRows(res.items);
44
+ setTotal(res.meta.total);
45
+ }
46
+ }, [run, list.query]);
47
+
48
+ useEffect(() => {
49
+ void fetch();
50
+ }, [fetch]);
51
+
52
+ return {
53
+ rows,
54
+ total,
55
+ page: list.page,
56
+ pageSize: list.pageSize,
57
+ sorting: list.sorting,
58
+ loading,
59
+ setPage: list.setPage,
60
+ setPageSize: list.setPageSize,
61
+ setSorting: list.setSorting,
62
+ filters: list.filters,
63
+ refresh: fetch,
64
+ };
65
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import type { SortingState } from "@tanstack/react-table";
4
+ import {
5
+ Dispatch,
6
+ SetStateAction,
7
+ useCallback,
8
+ useEffect,
9
+ useState,
10
+ } from "react";
11
+
12
+ import { logsService, RequestLog } from "#core/features/logs/services/logs.service";
13
+ import type { FiltersState } from "#core/hooks/use-filters";
14
+ import { useListQuery } from "#core/hooks/use-list-query";
15
+ import { useRequest } from "#core/hooks/use-request";
16
+
17
+ export interface UseRequestLogsResult {
18
+ rows: RequestLog[];
19
+ total: number;
20
+ page: number;
21
+ pageSize: number;
22
+ sorting: SortingState;
23
+ loading: boolean;
24
+ setPage: Dispatch<SetStateAction<number>>;
25
+ setPageSize: Dispatch<SetStateAction<number>>;
26
+ setSorting: Dispatch<SetStateAction<SortingState>>;
27
+ /** Filtros dinâmicos — repassar para o `DataTable`. */
28
+ filters: FiltersState;
29
+ refresh: () => Promise<void>;
30
+ }
31
+
32
+ export function useRequestLogs(): UseRequestLogsResult {
33
+ const { run, loading } = useRequest();
34
+
35
+ const list = useListQuery("/audit/changes/filter-schema", 20);
36
+
37
+ const [rows, setRows] = useState<RequestLog[]>([]);
38
+ const [total, setTotal] = useState(0);
39
+
40
+ const fetch = useCallback(async () => {
41
+ const res = await run(() => logsService.listRequests(list.query));
42
+ if (res) {
43
+ setRows(res.items);
44
+ setTotal(res.meta.total);
45
+ }
46
+ }, [run, list.query]);
47
+
48
+ useEffect(() => {
49
+ void fetch();
50
+ }, [fetch]);
51
+
52
+ return {
53
+ rows,
54
+ total,
55
+ page: list.page,
56
+ pageSize: list.pageSize,
57
+ sorting: list.sorting,
58
+ loading,
59
+ setPage: list.setPage,
60
+ setPageSize: list.setPageSize,
61
+ setSorting: list.setSorting,
62
+ filters: list.filters,
63
+ refresh: fetch,
64
+ };
65
+ }
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import type { JSX } from "react";
4
+
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "#core/components/ui";
6
+ import { useI18n } from "#core/contexts";
7
+ import { ErrorLogsPanel, RequestLogsPanel } from "#core/features/logs/panels";
8
+
9
+ export function LogsScreen(): JSX.Element {
10
+ const { t } = useI18n();
11
+
12
+ return (
13
+ <div>
14
+ <h1 className="mb-1 text-3xl font-bold">{t("logs.title")}</h1>
15
+ <p className="mb-4 text-sm text-muted-foreground">{t("logs.subtitle")}</p>
16
+
17
+ <Tabs defaultValue="requests">
18
+ <TabsList>
19
+ <TabsTrigger value="requests">{t("logs.tabRequests")}</TabsTrigger>
20
+ <TabsTrigger value="errors">{t("logs.tabErrors")}</TabsTrigger>
21
+ </TabsList>
22
+ <TabsContent value="requests">
23
+ <RequestLogsPanel />
24
+ </TabsContent>
25
+ <TabsContent value="errors">
26
+ <ErrorLogsPanel />
27
+ </TabsContent>
28
+ </Tabs>
29
+ </div>
30
+ );
31
+ }