Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions backend/src/modules/types/credentials.types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
import z from "zod";

export const RegisterSchema = z.object({
email: z.string().email("Email inválido"),
password: z.string().min(8, "Senha deve ter no mínimo 8 caracteres"),
name: z.string().min(1, "Nome é obrigatório").optional(),
phone: z.string().optional(),
cpf: z.string().optional(),
email: z
.string()
.email("Email inválido")
.max(254, "Email deve ter no máximo 254 caracteres"),
password: z
.string()
.min(8, "Senha deve ter no mínimo 8 caracteres")
.max(128, "Senha deve ter no máximo 128 caracteres"),
name: z
.string()
.min(1, "Nome é obrigatório")
.max(100, "Nome deve ter no máximo 100 caracteres")
.optional(),
phone: z
.string()
.max(16, "Telefone deve ter no máximo 15 dígitos")
.refine(
(value) => value.replace(/\D/g, "").length <= 15,
"Telefone deve ter no máximo 15 dígitos",
)
.optional(),
cpf: z
.string()
.max(14, "CPF deve ter no máximo 11 dígitos")
.refine(
(value) => value.replace(/\D/g, "").length <= 11,
"CPF deve ter no máximo 11 dígitos",
)
.optional(),
technologies: z.array(z.string()).optional(),
level: z.string().optional(),
});
Expand Down
34 changes: 24 additions & 10 deletions backend/tests/unit/modules/auth/credentials.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,19 @@ describe("CredentialsService", () => {
};
});
mocks.insertReturning.mockResolvedValue([mockUser]);
mocks.transaction.mockImplementation(async (callback) => callback({
insert: vi.fn(() => ({
values: mocks.insertValues.mockImplementation(() => ({
returning: mocks.insertReturning,
mocks.transaction.mockImplementation(async (callback) =>
callback({
insert: vi.fn(() => ({
values: mocks.insertValues.mockImplementation(() => ({
returning: mocks.insertReturning,
})),
})),
})),
query: {
credentials: { findFirst: mocks.credentialsFindFirst },
users: { findFirst: mocks.usersFindFirst },
},
}));
query: {
credentials: { findFirst: mocks.credentialsFindFirst },
users: { findFirst: mocks.usersFindFirst },
},
}),
);

// Define retornos seguros e limpos para evitar falhas colaterais
mocks.credentialsFindFirst.mockResolvedValue(null);
Expand Down Expand Up @@ -178,6 +180,18 @@ describe("CredentialsService", () => {
).rejects.toThrow();
});

it.each([
["nome", { name: "a".repeat(101) }],
["email", { email: `${"a".repeat(250)}@x.com` }],
["telefone", { phone: "1".repeat(16) }],
["senha", { password: "a".repeat(129) }],
["CPF", { cpf: "1".repeat(12) }],
])("rejeita %s acima do limite permitido", async (_field, override) => {
await expect(
service.register({ ...registerInput, ...override }),
).rejects.toThrow();
});

it("session contém userId e role", async () => {
const { session } = await service.register(registerInput);

Expand Down
9 changes: 8 additions & 1 deletion frontend/src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ export function AppRoutes() {

return (
<Routes>
<Route path="/" element={<LandingPage />} />
<Route
path="/"
element={
<PublicRoute>
<LandingPage />
</PublicRoute>
}
/>
<Route path="/home" element={dashboardElement} />
<Route path="/dashboard" element={dashboardElement} />
<Route path="/vagas" element={dashboardElement} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ const LEVEL_OPTIONS = [
{ value: "senior", label: "Sênior" },
];

const REGISTER_LIMITS = {
name: 100,
email: 254,
phoneDigits: 15,
password: 128,
cpf: 14,
} as const;

function getErrorMessage(error: unknown, fallback: string) {
return error instanceof Error && error.message ? error.message : fallback;
}
Expand Down Expand Up @@ -163,8 +171,8 @@ export default function RegisterSide() {
if (!password) {
setPasswordError("O campo de senha é obrigatório.");
isValid = false;
} else if (password.length < 6) {
setPasswordError("A senha precisa conter pelo menos 6 caracteres.");
} else if (password.length < 8) {
setPasswordError("A senha precisa conter pelo menos 8 caracteres.");
isValid = false;
}

Expand Down Expand Up @@ -196,7 +204,9 @@ export default function RegisterSide() {
window.location.href = "/login?registered=true";
} catch (error: unknown) {
console.error("Erro no cadastro:", error);
setApiError(getErrorMessage(error, "Erro ao cadastrar. Tente novamente."));
setApiError(
getErrorMessage(error, "Erro ao cadastrar. Tente novamente."),
);
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -260,6 +270,7 @@ export default function RegisterSide() {
type="text"
value={nome}
onChange={(e) => setNome(e.target.value)}
maxLength={REGISTER_LIMITS.name}
placeholder="benevanio"
disabled={isLoading}
className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${nomeError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`}
Expand All @@ -282,6 +293,7 @@ export default function RegisterSide() {
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
maxLength={REGISTER_LIMITS.email}
placeholder="benevanio@dev.com.br"
disabled={isLoading}
className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${emailError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`}
Expand All @@ -303,7 +315,15 @@ export default function RegisterSide() {
international
defaultCountry="BR"
value={telefone}
onChange={setTelefone}
onChange={(value) => {
if (
!value ||
value.replace(/\D/g, "").length <= REGISTER_LIMITS.phoneDigits
) {
setTelefone(value);
}
}}
numberInputProps={{ maxLength: REGISTER_LIMITS.phoneDigits + 1 }}
disabled={isLoading}
className="w-full px-4 py-3.5 text-gray-900 dark:text-white bg-transparent focus:outline-none phone-input-custom"
placeholder="(34) 23456-7890"
Expand All @@ -328,6 +348,7 @@ export default function RegisterSide() {
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
maxLength={REGISTER_LIMITS.password}
placeholder="Ex: ••••••••••••"
disabled={isLoading}
className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${passwordError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`}
Expand Down Expand Up @@ -366,6 +387,7 @@ export default function RegisterSide() {
type="text"
value={cpf}
onChange={(e) => setCpf(formatCpf(e.target.value))}
maxLength={REGISTER_LIMITS.cpf}
placeholder="091.000.000-00"
disabled={isLoading}
className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${cpfError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"} ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export function JobsFiltersCard({
<Input
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
maxLength={100}
className="h-14 w-full rounded-2xl border border-slate-300 bg-white pl-11 pr-14 text-base text-slate-900 placeholder:text-slate-500 focus-visible:ring-[#14AE5C]/40 dark:border-[#35506f] dark:bg-[#091224] dark:text-slate-100 dark:placeholder:text-slate-400"
placeholder="Buscar por título, empresa, local ou link"
/>
Expand Down Expand Up @@ -183,11 +184,12 @@ export function JobsFiltersCard({
selectedFilters.map((filter) => (
<Badge
key={filter}
className="gap-1.5 rounded-full border border-[#14AE5C]/30 bg-[#14AE5C]/12 px-2 py-1 text-xs font-semibold text-[#0c6b35] dark:text-[#8df0af]"
title={filter}
className="max-w-full gap-1.5 rounded-full border border-[#14AE5C]/30 bg-[#14AE5C]/12 px-2 py-1 text-xs font-semibold text-[#0c6b35] dark:text-[#8df0af]"
>
<FiCheck className="h-3 w-3" />
<FiTag className="h-3 w-3" />
<span>{filter}</span>
<span className="max-w-48 truncate">{filter}</span>
<button
type="button"
aria-label={`Remover filtro ${filter}`}
Expand All @@ -212,4 +214,4 @@ export function JobsFiltersCard({
)}
</>
);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { fetchKeywords, saveKeywords } from "@/domains/jobs/infrastructure/jobsApi";
import {
fetchKeywords,
saveKeywords,
} from "@/domains/jobs/infrastructure/jobsApi";
import { X } from "lucide-react";
import { useEffect, useState } from "react";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/shared/ui/card";
import { Input } from "@/shared/ui/input";

interface KeywordsModalProps {
Expand Down Expand Up @@ -61,7 +70,8 @@ export function KeywordsModal({ onClose }: KeywordsModalProps) {
<CardHeader>
<CardTitle>Gerenciar filtros</CardTitle>
<CardDescription>
Adicione ou remova as palavras-chave usadas na busca e nos filtros de vagas.
Adicione ou remova as palavras-chave usadas na busca e nos filtros
de vagas.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
Expand All @@ -71,6 +81,7 @@ export function KeywordsModal({ onClose }: KeywordsModalProps) {
aria-label="Nova keyword ou filtro"
value={newKeyword}
onChange={(e) => setNewKeyword(e.target.value)}
maxLength={100}
onKeyDown={(e) => e.key === "Enter" && handleAddKeyword()}
/>
<Button onClick={handleAddKeyword} disabled={isLoading || isSaving}>
Expand All @@ -80,14 +91,25 @@ export function KeywordsModal({ onClose }: KeywordsModalProps) {

<div className="flex flex-wrap gap-2 p-4 border rounded-md bg-muted/20">
{isLoading ? (
<p className="text-sm text-muted-foreground italic">Carregando keywords...</p>
<p className="text-sm text-muted-foreground italic">
Carregando keywords...
</p>
) : keywords.length === 0 ? (
<p className="text-sm text-muted-foreground italic">Nenhuma keyword cadastrada.</p>
<p className="text-sm text-muted-foreground italic">
Nenhuma keyword cadastrada.
</p>
) : (
keywords.map((kw) => (
<Badge key={kw} variant="secondary" className="pl-3 pr-1 gap-1">
{kw}
<Badge
key={kw}
variant="secondary"
title={kw}
className="max-w-full pl-3 pr-1 gap-1"
>
<span className="max-w-72 truncate">{kw}</span>
<button
type="button"
aria-label={`Remover keyword ${kw}`}
onClick={() => handleRemoveKeyword(kw)}
className="hover:bg-red-500/50 rounded-full p-0.5"
>
Expand Down
Loading
Loading