adição de recuperação de senha e limpeza de remanescentes
This commit is contained in:
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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("");
|
||||
}}
|
||||
>
|
||||
Já 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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user