adição de recuperação de senha e limpeza de remanescentes

This commit is contained in:
joao.herculano
2026-08-06 11:53:09 -03:00
parent 4e1ebe861c
commit 5b10e46e62
20 changed files with 183 additions and 925 deletions
@@ -1,15 +0,0 @@
// This file is automatically generated. Do not edit it directly.
import { createMiddleware } from '@tanstack/react-start'
import { supabase } from './client'
// Must be registered as a global `functionMiddleware` in `src/start.ts`; otherwise
// the browser never attaches the bearer token to serverFn RPCs.
export const attachSupabaseAuth = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
const { data } = await supabase.auth.getSession()
const token = data.session?.access_token
return next({
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
},
)
@@ -1,109 +0,0 @@
// This file is automatically generated. Do not edit it directly.
import { createMiddleware } from '@tanstack/react-start'
import { getRequest } from '@tanstack/react-start/server'
import { createClient } from '@supabase/supabase-js'
import type { Database } from './types'
function isNewSupabaseApiKey(value: string): boolean {
return value.startsWith('sb_publishable_') || value.startsWith('sb_secret_');
}
function createSupabaseFetch(supabaseKey: string): typeof fetch {
return (input, init) => {
const headers = new Headers(
typeof Request !== 'undefined' && input instanceof Request ? input.headers : undefined,
);
if (init?.headers) {
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
}
// New Supabase API keys are opaque strings, not bearer JWTs.
if (isNewSupabaseApiKey(supabaseKey) && headers.get('Authorization') === `Bearer ${supabaseKey}`) {
headers.delete('Authorization');
}
headers.set('apikey', supabaseKey);
return fetch(input, { ...init, headers });
};
}
export const requireSupabaseAuth = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
const SUPABASE_URL = process.env['SUPABASE_URL'];
const SUPABASE_PUBLISHABLE_KEY = process.env['SUPABASE_PUBLISHABLE_KEY'];
if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) {
const missing = [
...(!SUPABASE_URL ? ['SUPABASE_URL'] : []),
...(!SUPABASE_PUBLISHABLE_KEY ? ['SUPABASE_PUBLISHABLE_KEY'] : []),
];
const message = `Missing Supabase environment variable(s): ${missing.join(', ')}. Connect Supabase in Lovable Cloud.`;
console.error(`[Supabase] ${message}`);
throw new Error(message);
}
const request = getRequest();
if (!request?.headers) {
throw new Error('Unauthorized: No request headers available');
}
const authHeader = request.headers.get('authorization');
if (!authHeader) {
throw new Error('Unauthorized: No authorization header provided');
}
if (!authHeader.startsWith('Bearer ')) {
throw new Error('Unauthorized: Only Bearer tokens are supported');
}
const token = authHeader.replace('Bearer ', '');
if (!token) {
throw new Error('Unauthorized: No token provided');
}
if (token.split('.').length !== 3) {
throw new Error('Unauthorized: Invalid token');
}
const supabase = createClient<Database>(
SUPABASE_URL!,
SUPABASE_PUBLISHABLE_KEY!,
{
global: {
fetch: createSupabaseFetch(SUPABASE_PUBLISHABLE_KEY!),
headers: {
Authorization: `Bearer ${token}`,
},
},
auth: {
storage: undefined,
persistSession: false,
autoRefreshToken: false,
},
}
);
const { data, error } = await supabase.auth.getClaims(token);
if (error || !data?.claims) {
throw new Error('Unauthorized: Invalid token');
}
if (!data.claims.sub) {
throw new Error('Unauthorized: No user ID found in token');
}
return next({
context: {
supabase,
userId: data.claims.sub,
claims: data.claims,
},
});
},
);
@@ -1,69 +0,0 @@
// This file is automatically generated. Do not edit it directly.
// Server-side Supabase client with service role key - bypasses RLS.
// Use this for admin operations in server functions and server routes only.
// For user-authenticated queries (with RLS), use the auth middleware instead.
import { createClient } from '@supabase/supabase-js';
import type { Database } from './types';
function isNewSupabaseApiKey(value: string): boolean {
return value.startsWith('sb_publishable_') || value.startsWith('sb_secret_');
}
function createSupabaseFetch(supabaseKey: string): typeof fetch {
return (input, init) => {
const headers = new Headers(
typeof Request !== 'undefined' && input instanceof Request ? input.headers : undefined,
);
if (init?.headers) {
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
}
// New Supabase API keys are opaque strings, not bearer JWTs.
if (isNewSupabaseApiKey(supabaseKey) && headers.get('Authorization') === `Bearer ${supabaseKey}`) {
headers.delete('Authorization');
}
headers.set('apikey', supabaseKey);
return fetch(input, { ...init, headers });
};
}
function createSupabaseAdminClient() {
const SUPABASE_URL = process.env['SUPABASE_URL'];
const SUPABASE_SERVICE_ROLE_KEY = process.env['SUPABASE_SERVICE_ROLE_KEY'];
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
const missing = [
...(!SUPABASE_URL ? ['SUPABASE_URL'] : []),
...(!SUPABASE_SERVICE_ROLE_KEY ? ['SUPABASE_SERVICE_ROLE_KEY'] : []),
];
const message = `Missing Supabase environment variable(s): ${missing.join(', ')}. Connect Supabase in Lovable Cloud.`;
console.error(`[Supabase] ${message}`);
throw new Error(message);
}
return createClient<Database>(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
global: {
fetch: createSupabaseFetch(SUPABASE_SERVICE_ROLE_KEY),
},
auth: {
storage: undefined,
persistSession: false,
autoRefreshToken: false,
}
});
}
let _supabaseAdmin: ReturnType<typeof createSupabaseAdminClient> | undefined;
// Server-side Supabase client with service role - bypasses RLS
// SECURITY: Only use this for trusted server-side operations, never expose to client code
// Load inside server handlers: const { supabaseAdmin } = await import("@/integrations/supabase/client.server");
// Top-level import is safe only in other .server.ts modules - route files and *.functions.ts ship to the client bundle.
export const supabaseAdmin = new Proxy({} as ReturnType<typeof createSupabaseAdminClient>, {
get(_, prop, receiver) {
if (!_supabaseAdmin) _supabaseAdmin = createSupabaseAdminClient();
return Reflect.get(_supabaseAdmin, prop, receiver);
},
});
-68
View File
@@ -1,68 +0,0 @@
// This file is automatically generated. Do not edit it directly.
import { createClient } from '@supabase/supabase-js';
import type { Database } from './types';
function isNewSupabaseApiKey(value: string): boolean {
return value.startsWith('sb_publishable_') || value.startsWith('sb_secret_');
}
function createSupabaseFetch(supabaseKey: string): typeof fetch {
return (input, init) => {
const headers = new Headers(
typeof Request !== 'undefined' && input instanceof Request ? input.headers : undefined,
);
if (init?.headers) {
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
}
// New Supabase API keys are opaque strings, not bearer JWTs.
if (isNewSupabaseApiKey(supabaseKey) && headers.get('Authorization') === `Bearer ${supabaseKey}`) {
headers.delete('Authorization');
}
headers.set('apikey', supabaseKey);
return fetch(input, { ...init, headers });
};
}
function createSupabaseClient() {
// Use import.meta.env for client-side (Vite build-time replacement)
// Fall back to process.env for SSR (server-side rendering)
const SUPABASE_URL = import.meta.env['VITE_SUPABASE_URL'] || process.env['SUPABASE_URL'];
const SUPABASE_PUBLISHABLE_KEY = import.meta.env['VITE_SUPABASE_PUBLISHABLE_KEY'] || process.env['SUPABASE_PUBLISHABLE_KEY'];
if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) {
const missing = [
...(!SUPABASE_URL ? ['SUPABASE_URL'] : []),
...(!SUPABASE_PUBLISHABLE_KEY ? ['SUPABASE_PUBLISHABLE_KEY'] : []),
];
const message = `Missing Supabase environment variable(s): ${missing.join(', ')}. Connect Supabase in Lovable Cloud.`;
console.error(`[Supabase] ${message}`);
throw new Error(message);
}
return createClient<Database>(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
global: {
fetch: createSupabaseFetch(SUPABASE_PUBLISHABLE_KEY),
},
auth: {
storage: typeof window !== 'undefined' ? localStorage : undefined,
persistSession: true,
autoRefreshToken: true,
}
});
}
let _supabase: ReturnType<typeof createSupabaseClient> | undefined;
// Import the supabase client like this:
// import { supabase } from "@/integrations/supabase/client";
export const supabase = new Proxy({} as ReturnType<typeof createSupabaseClient>, {
get(_, prop, receiver) {
if (!_supabase) _supabase = createSupabaseClient();
return Reflect.get(_supabase, prop, receiver);
},
});
-231
View File
@@ -1,231 +0,0 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export type Database = {
// Allows to automatically instantiate createClient with right options
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
__InternalSupabase: {
PostgrestVersion: "14.15"
}
public: {
Tables: {
coletas: {
Row: {
codigo_barras: string
created_at: string
fim_coleta: string | null
id: number
inicio_coleta: string
usuario: string
}
Insert: {
codigo_barras: string
created_at?: string
fim_coleta?: string | null
id?: never
inicio_coleta?: string
usuario: string
}
Update: {
codigo_barras?: string
created_at?: string
fim_coleta?: string | null
id?: never
inicio_coleta?: string
usuario?: string
}
Relationships: []
}
papeis_usuario: {
Row: {
created_at: string
id: string
role: Database["public"]["Enums"]["app_role"]
user_id: string
}
Insert: {
created_at?: string
id?: string
role: Database["public"]["Enums"]["app_role"]
user_id: string
}
Update: {
created_at?: string
id?: string
role?: Database["public"]["Enums"]["app_role"]
user_id?: string
}
Relationships: []
}
perfis: {
Row: {
aprovado: boolean
created_at: string
email: string
id: string
nome: string
updated_at: string
}
Insert: {
aprovado?: boolean
created_at?: string
email: string
id: string
nome: string
updated_at?: string
}
Update: {
aprovado?: boolean
created_at?: string
email?: string
id?: string
nome?: string
updated_at?: string
}
Relationships: []
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
app_role: "admin" | "user"
}
CompositeTypes: {
[_ in never]: never
}
}
}
type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
export type Tables<
DefaultSchemaTableNameOrOptions extends
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
}
? R
: never
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
DefaultSchema["Views"])
? (DefaultSchema["Tables"] &
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
}
? I
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof DatabaseWithoutInternals },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = DefaultSchemaTableNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
}
? U
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
DefaultSchemaEnumNameOrOptions extends
| keyof DefaultSchema["Enums"]
| { schema: keyof DatabaseWithoutInternals },
EnumName extends DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
: never = never,
> = DefaultSchemaEnumNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
: never
export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends
| keyof DefaultSchema["CompositeTypes"]
| { schema: keyof DatabaseWithoutInternals },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
: never = never,
> = PublicCompositeTypeNameOrOptions extends {
schema: keyof DatabaseWithoutInternals
}
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
: never
export const Constants = {
public: {
Enums: {
app_role: ["admin", "user"],
},
},
} as const
+44
View File
@@ -255,6 +255,50 @@ export const criarConta = createServerFn({ method: "POST" })
};
});
export const recuperarSenha = createServerFn({ method: "POST" })
.validator(loginSchema)
.handler(async ({ data }) => {
const nome = normalizarNome(data.nome);
const [{ getPool, sql }, bcrypt] = await Promise.all([
import("@/lib/db.server"),
import("bcryptjs"),
]);
const pool = await getPool();
const existente = await pool
.request()
.input("nome", sql.NVarChar(150), nome)
.query<{ id: string; ativo: boolean }>(`
SELECT TOP 1 id, ativo
FROM slalog.usuarios
WHERE LOWER(nome) = LOWER(@nome)
`);
const usuario = existente.recordset[0];
if (!usuario || !usuario.ativo) {
throw new Error("Nome de usuário não encontrado.");
}
const senhaHash = await bcrypt.hash(data.senha, 10);
await pool
.request()
.input("id", sql.UniqueIdentifier, usuario.id)
.input("senhaHash", sql.NVarChar(255), senhaHash)
.query(`
UPDATE slalog.usuarios
SET senha_hash = @senhaHash,
aprovado = 0,
updated_at = SYSDATETIME()
WHERE id = @id
`);
const { clearAuthSession } = await import("@/lib/auth.server");
await clearAuthSession();
return { success: true };
});
export const sair = createServerFn({ method: "POST" }).handler(async () => {
const { clearAuthSession } = await import("@/lib/auth.server");
await clearAuthSession();
+15 -1
View File
@@ -35,6 +35,7 @@ export const iniciarColeta = createServerFn({ method: "POST" })
const sessao = await obterSessaoAprovada();
const [{ getPool, sql }] = await Promise.all([import("@/lib/db.server")]);
const pool = await getPool();
const codigo = data.codigo.trim();
const dataAberta = await pool
.request()
@@ -48,10 +49,23 @@ export const iniciarColeta = createServerFn({ method: "POST" })
if (dataAberta.recordset[0]) throw new Error("Já existe uma coleta em andamento.");
const duplicado = await pool
.request()
.input("codigo", sql.NVarChar(100), codigo)
.query<{ id: number }>(`
SELECT TOP 1 id
FROM slalog.coletas
WHERE codigo_barras = @codigo
`);
if (duplicado.recordset[0]) {
throw new Error("Este número de caixa já foi registrado anteriormente.");
}
const result = await pool
.request()
.input("usuario", sql.NVarChar(150), sessao.nome)
.input("codigo", sql.NVarChar(100), data.codigo)
.input("codigo", sql.NVarChar(100), codigo)
.query<ColetaRow>(`
INSERT INTO slalog.coletas (usuario, codigo_barras, inicio_coleta, created_at)
OUTPUT
+15 -8
View File
@@ -42,13 +42,13 @@ export const Route = createFileRoute("/_authenticated/coleta")({
{
name: "description",
content:
"Registre o início e o fim da coleta de caixas informando o código do pedido, com cronômetro em tempo real.",
"Registre o início e o fim da coleta de caixas informando o número de caixa, com cronômetro em tempo real.",
},
{ property: "og:title", content: "Coleta de Caixas | Registro de pedidos" },
{
property: "og:description",
content:
"Registre o início e o fim da coleta de caixas informando o código do pedido, com cronômetro em tempo real.",
"Registre o início e o fim da coleta de caixas informando o número de caixa, com cronômetro em tempo real.",
},
{ property: "og:type", content: "website" },
{ name: "twitter:card", content: "summary" },
@@ -129,7 +129,7 @@ function ColetaScreen() {
async function handleIniciar() {
const valor = codigo.trim();
if (!valor) {
toast.error("Digite o código do pedido antes de iniciar.");
toast.error("Digite o número de caixa antes de iniciar.");
return;
}
if (coletaAtiva) {
@@ -146,7 +146,14 @@ function ColetaScreen() {
toast.success("Coleta iniciada.");
} catch (error) {
console.error(error);
toast.error("Erro ao iniciar a coleta. Tente novamente.");
const msg = (error as { message?: string })?.message ?? "";
toast.error(
msg.includes("já foi registrado")
? "Este número de caixa já foi contabilizado."
: msg.includes("coleta em andamento")
? "Já existe uma coleta em andamento."
: "Erro ao iniciar a coleta. Tente novamente.",
);
} finally {
setCarregando(false);
}
@@ -162,7 +169,7 @@ function ColetaScreen() {
inicioRef.current = null;
setConfirmarFim(false);
toast.success(
adicionarOutro ? "Coleta finalizada. Digite o próximo pedido." : "Coleta finalizada.",
adicionarOutro ? "Coleta finalizada. Digite o próximo número de caixa." : "Coleta finalizada.",
);
if (adicionarOutro) window.setTimeout(() => inputRef.current?.focus(), 50);
} catch (error) {
@@ -242,7 +249,7 @@ function ColetaScreen() {
<section className="space-y-3 rounded-2xl border border-border bg-card p-4 shadow-soft">
<label htmlFor="codigo" className="text-sm font-medium text-muted-foreground">
Código do pedido
Número de caixa
</label>
<div className="flex gap-2">
<Input
@@ -257,7 +264,7 @@ function ColetaScreen() {
inputMode="numeric"
pattern="[0-9]*"
autoComplete="off"
placeholder="Digite ou cole o número do pedido"
placeholder="Digite ou cole o número de caixa"
disabled={Boolean(coletaAtiva) || verificando}
className="h-12 flex-1 rounded-xl text-base tabular-nums"
/>
@@ -325,7 +332,7 @@ function ColetaScreen() {
<AlertDialogHeader>
<AlertDialogTitle>Deseja realmente finalizar esta coleta?</AlertDialogTitle>
<AlertDialogDescription>
O horário de término será registrado e o campo ficará pronto para o próximo pedido.
O horário de término será registrado e o campo ficará pronto para o próximo número de caixa.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
+109 -22
View File
@@ -1,12 +1,12 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { Boxes, Loader2, LogIn, UserPlus } from "lucide-react";
import { Boxes, Eye, EyeOff, KeyRound, Loader2, LogIn, UserPlus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { criarConta, entrar, obterSessaoAtual, normalizarNome } from "@/lib/auth";
import { criarConta, entrar, obterSessaoAtual, normalizarNome, recuperarSenha } from "@/lib/auth";
export const Route = createFileRoute("/auth")({
head: () => ({
@@ -32,11 +32,14 @@ export const Route = createFileRoute("/auth")({
function AuthPage() {
const navigate = useNavigate();
const [modo, setModo] = useState<"entrar" | "criar">("entrar");
const [modo, setModo] = useState<"entrar" | "criar" | "recuperar">("entrar");
const [nome, setNome] = useState("");
const [senha, setSenha] = useState("");
const [confirmarSenha, setConfirmarSenha] = useState("");
const [mostrarSenha, setMostrarSenha] = useState(false);
const [carregando, setCarregando] = useState(false);
const [verificando, setVerificando] = useState(true);
const precisaConfirmarSenha = modo !== "entrar";
useEffect(() => {
let cancelado = false;
@@ -57,6 +60,10 @@ function AuthPage() {
toast.error("Informe um nome com pelo menos 3 caracteres e senha com 6 ou mais.");
return;
}
if (precisaConfirmarSenha && senha !== confirmarSenha) {
toast.error("As senhas digitadas não coincidem.");
return;
}
setCarregando(true);
try {
if (modo === "criar") {
@@ -66,6 +73,10 @@ function AuthPage() {
? "Conta criada e liberada. Faça login."
: "Conta criada! Aguarde a liberação de um administrador.",
);
} else if (modo === "recuperar") {
await recuperarSenha({ data: { nome: usuario, senha } });
toast.success("Senha redefinida. Aguarde a aprovação de um administrador.");
setModo("entrar");
} else {
await entrar({ data: { nome: usuario, senha } });
void navigate({ to: "/coleta", replace: true });
@@ -76,6 +87,8 @@ function AuthPage() {
toast.error(
msg.includes("incorretos")
? "Nome de usuário ou senha incorretos."
: msg.includes("não encontrado")
? "Nome de usuário não encontrado."
: msg.includes("já existe")
? "Este nome de usuário já existe. Faça login."
: "Não foi possível concluir. Tente novamente.",
@@ -104,7 +117,9 @@ function AuthPage() {
<p className="text-sm text-muted-foreground">
{modo === "entrar"
? "Entre com seu nome de usuário e senha."
: "Crie sua conta — o acesso é liberado por um administrador."}
: modo === "criar"
? "Crie sua conta — o acesso é liberado por um administrador."
: "Defina uma nova senha. Depois disso, a conta volta para aprovação do administrador."}
</p>
</div>
@@ -124,17 +139,52 @@ function AuthPage() {
</div>
<div className="space-y-2">
<Label htmlFor="senha">Senha</Label>
<Input
id="senha"
type="password"
autoComplete={modo === "entrar" ? "current-password" : "new-password"}
value={senha}
onChange={(e) => setSenha(e.target.value)}
placeholder="Mínimo de 6 caracteres"
className="h-12 rounded-xl"
/>
<div className="relative">
<Input
id="senha"
type={mostrarSenha ? "text" : "password"}
autoComplete={modo === "entrar" ? "current-password" : "new-password"}
value={senha}
onChange={(e) => setSenha(e.target.value)}
placeholder="Mínimo de 6 caracteres"
className="h-12 rounded-xl pr-12"
/>
<button
type="button"
className="absolute top-[14px] right-3 text-muted-foreground"
onClick={() => setMostrarSenha((atual) => !atual)}
aria-label={mostrarSenha ? "Ocultar senha" : "Mostrar senha"}
>
{mostrarSenha ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
{precisaConfirmarSenha ? (
<div className="space-y-2">
<Label htmlFor="confirmar-senha">Confirmar senha</Label>
<div className="relative">
<Input
id="confirmar-senha"
type={mostrarSenha ? "text" : "password"}
autoComplete="new-password"
value={confirmarSenha}
onChange={(e) => setConfirmarSenha(e.target.value)}
placeholder="Repita a senha"
className="h-12 rounded-xl pr-12"
/>
<button
type="button"
className="absolute top-[14px] right-3 text-muted-foreground"
onClick={() => setMostrarSenha((atual) => !atual)}
aria-label={mostrarSenha ? "Ocultar senha" : "Mostrar senha"}
>
{mostrarSenha ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
) : null}
<Button
type="submit"
className="h-12 w-full rounded-xl text-base font-semibold"
@@ -144,20 +194,57 @@ function AuthPage() {
<Loader2 className="h-5 w-5 animate-spin" />
) : modo === "entrar" ? (
<LogIn className="h-5 w-5" />
) : (
) : modo === "criar" ? (
<UserPlus className="h-5 w-5" />
) : (
<KeyRound className="h-5 w-5" />
)}
{modo === "entrar" ? "Entrar" : "Criar conta"}
{modo === "entrar"
? "Entrar"
: modo === "criar"
? "Criar conta"
: "Recuperar senha"}
</Button>
</form>
<button
type="button"
className="w-full text-center text-sm text-muted-foreground underline-offset-4 hover:underline"
onClick={() => setModo(modo === "entrar" ? "criar" : "entrar")}
>
{modo === "entrar" ? "Não tenho conta — criar agora" : "Já tenho conta — entrar"}
</button>
<div className="space-y-2 text-center text-sm">
{modo !== "entrar" ? (
<button
type="button"
className="w-full text-muted-foreground underline-offset-4 hover:underline"
onClick={() => {
setModo("entrar");
setConfirmarSenha("");
}}
>
tenho conta entrar
</button>
) : null}
{modo !== "criar" ? (
<button
type="button"
className="w-full text-muted-foreground underline-offset-4 hover:underline"
onClick={() => {
setModo("criar");
setConfirmarSenha("");
}}
>
Não tenho conta criar agora
</button>
) : null}
{modo !== "recuperar" ? (
<button
type="button"
className="w-full text-muted-foreground underline-offset-4 hover:underline"
onClick={() => {
setModo("recuperar");
setConfirmarSenha("");
}}
>
Esqueci minha senha
</button>
) : null}
</div>
</div>
</div>
);