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
-6
View File
@@ -1,9 +1,3 @@
SUPABASE_PROJECT_ID="iiknzpjckdzlpglimmtq"
SUPABASE_PUBLISHABLE_KEY="sb_publishable_K0u7m8HzX7LZ39cEhDoD-A_v4KDxHce"
SUPABASE_URL="https://iiknzpjckdzlpglimmtq.supabase.co"
VITE_SUPABASE_PROJECT_ID="iiknzpjckdzlpglimmtq"
VITE_SUPABASE_PUBLISHABLE_KEY="sb_publishable_K0u7m8HzX7LZ39cEhDoD-A_v4KDxHce"
VITE_SUPABASE_URL="https://iiknzpjckdzlpglimmtq.supabase.co"
DB_HOST="10.77.77.10"
DB_PORT="1433"
DB_NAME="GINSENG"
-15
View File
@@ -32,7 +32,6 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@supabase/supabase-js": "^2.111.0",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "^5.101.1",
"@tanstack/react-router": "^1.170.18",
@@ -379,20 +378,6 @@
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
"@supabase/auth-js": ["@supabase/auth-js@2.111.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@supabase/auth-js/-/auth-js-2.111.0.tgz", { "dependencies": { "tslib": "2.8.1" } }, "sha512-hbRLgyQZEX0SDyF4LYXpv94qOIQyFATfpT5SIs2V0SisHnisVRkYgiPQWzv80l4S2mO3qof78rmoH9j5zoFsYQ=="],
"@supabase/functions-js": ["@supabase/functions-js@2.111.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@supabase/functions-js/-/functions-js-2.111.0.tgz", { "dependencies": { "tslib": "2.8.1" } }, "sha512-RW/OCsd6MO592zU8ifzP8/f8XzxxIdpb+Up5XaOtE26Fw+3zTp475WX7+GuuktiD1WF8pFUDe6khUPbMp77RCw=="],
"@supabase/phoenix": ["@supabase/phoenix@0.4.5", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@supabase/phoenix/-/phoenix-0.4.5.tgz", {}, "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw=="],
"@supabase/postgrest-js": ["@supabase/postgrest-js@2.111.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@supabase/postgrest-js/-/postgrest-js-2.111.0.tgz", { "dependencies": { "tslib": "2.8.1" } }, "sha512-pcqeDsnWP0lx9GawduYxNZJHeuTm53O7L0SC8RF8tniV3GWIPY6me6OTdnwzdwNUmNy1dzUVtSyIfE6+OflzPQ=="],
"@supabase/realtime-js": ["@supabase/realtime-js@2.111.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@supabase/realtime-js/-/realtime-js-2.111.0.tgz", { "dependencies": { "@supabase/phoenix": "0.4.5", "tslib": "2.8.1" } }, "sha512-6oRf/vZyRwg8f8GbFSJkrD2w4HAu/yTvyMViHXHS+H5hNJzdXCrUR7cP5oW7daT3YlRnzRPY9LcGSJKZAmfMSg=="],
"@supabase/storage-js": ["@supabase/storage-js@2.111.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@supabase/storage-js/-/storage-js-2.111.0.tgz", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-UEViNmTzVOxE8dqUA81wls+n9xgmlvSFfhfwo6QxrO4kQOytCYyw3ciYFoi4XoD4Jl95NJ3jnndHN5iIudWzqw=="],
"@supabase/supabase-js": ["@supabase/supabase-js@2.111.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@supabase/supabase-js/-/supabase-js-2.111.0.tgz", { "dependencies": { "@supabase/auth-js": "2.111.0", "@supabase/functions-js": "2.111.0", "@supabase/postgrest-js": "2.111.0", "@supabase/realtime-js": "2.111.0", "@supabase/storage-js": "2.111.0" } }, "sha512-9q0/AULthQnWeiDh1vGyjoJZbSY04bu6qHcWit70pqEYn5Kv/dkCPY62Ja1123jEnJbB9Vd2pjY7Kvk/lK3peA=="],
"@tabby_ai/hijri-converter": ["@tabby_ai/hijri-converter@1.0.5", "", {}, "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ=="],
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
-102
View File
@@ -33,7 +33,6 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@supabase/supabase-js": "^2.111.0",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "^5.101.1",
"@tanstack/react-router": "^1.170.18",
@@ -3151,98 +3150,6 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@supabase/auth-js": {
"version": "2.112.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.112.0.tgz",
"integrity": "sha512-8qAdObNQHKbSeVBLmf2WLNT7+bCE8zBofpCFiNUqGBAl5qw9VagSvcmPXlh78McAU7iHrGik1oeJxiB2A6B29w==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.112.0",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.112.0.tgz",
"integrity": "sha512-2DdaEZs0vq86orMIZBO+eM5w5/UxZb1EZyg2JraBKS4W9BzfFO+fOFD6YStmZ7iAOWvxNTGPjPNItsofpGgTCA==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@supabase/phoenix": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz",
"integrity": "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==",
"license": "MIT"
},
"node_modules/@supabase/postgrest-js": {
"version": "2.112.0",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.112.0.tgz",
"integrity": "sha512-4HKCVq32Jlk/wS8Ud8QgaAuQ4u6w1hZfw/gS5IcIN7wYddElMj4kfiCZpHMPfncomMnYzAKBUhwBZPpRcTH2Yw==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.112.0",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.112.0.tgz",
"integrity": "sha512-McFFP+ivFDMTaCEh8JDpG+sPEmv5IjKvrP0uTH3Lbsriai4KbxB8ycY8TqPQnbjhOVjEifQm1q0y2tL1CUw9Zg==",
"license": "MIT",
"dependencies": {
"@supabase/phoenix": "0.4.5",
"tslib": "2.8.1"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.112.0",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.112.0.tgz",
"integrity": "sha512-X44Bl045X/e5e2tJqWsY+JmQvgtm04BJuijiWprIWYLn0mDvGnu8hLdzPOPcTVji7wlqbt2ZUHttsv+rGKQYXw==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.112.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.112.0.tgz",
"integrity": "sha512-dHVOgog58GOagtrZuPxJYg/R45ZV2U0qqgXffH+lMlt1OS+267Pw4g7bw3iXCGxN85OufiE0nI1baxDXlgEyfQ==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.112.0",
"@supabase/functions-js": "2.112.0",
"@supabase/postgrest-js": "2.112.0",
"@supabase/realtime-js": "2.112.0",
"@supabase/storage-js": "2.112.0"
},
"engines": {
"node": ">=22.0.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
}
}
},
"node_modules/@tabby_ai/hijri-converter": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz",
@@ -6346,15 +6253,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
-1
View File
@@ -40,7 +40,6 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@supabase/supabase-js": "^2.111.0",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "^5.101.1",
"@tanstack/react-router": "^1.170.18",
@@ -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>
+98 -11
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,16 +139,51 @@ function AuthPage() {
</div>
<div className="space-y-2">
<Label htmlFor="senha">Senha</Label>
<div className="relative">
<Input
id="senha"
type="password"
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"
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"
@@ -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>
<div className="space-y-2 text-center text-sm">
{modo !== "entrar" ? (
<button
type="button"
className="w-full text-center text-sm text-muted-foreground underline-offset-4 hover:underline"
onClick={() => setModo(modo === "entrar" ? "criar" : "entrar")}
className="w-full text-muted-foreground underline-offset-4 hover:underline"
onClick={() => {
setModo("entrar");
setConfirmarSenha("");
}}
>
{modo === "entrar" ? "Não tenho conta — criar agora" : "Já tenho conta — entrar"}
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>
);
-1
View File
@@ -1 +0,0 @@
project_id = "iiknzpjckdzlpglimmtq"
@@ -1,20 +0,0 @@
CREATE TABLE public.coletas (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
usuario TEXT NOT NULL,
codigo_barras TEXT NOT NULL,
inicio_coleta TIMESTAMPTZ NOT NULL DEFAULT now(),
fim_coleta TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
GRANT SELECT, INSERT, UPDATE ON public.coletas TO anon;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.coletas TO authenticated;
GRANT ALL ON public.coletas TO service_role;
ALTER TABLE public.coletas ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Coletas sao visiveis publicamente" ON public.coletas FOR SELECT TO anon, authenticated USING (true);
CREATE POLICY "Qualquer um pode criar coleta" ON public.coletas FOR INSERT TO anon, authenticated WITH CHECK (true);
CREATE POLICY "Qualquer um pode atualizar coleta" ON public.coletas FOR UPDATE TO anon, authenticated USING (true) WITH CHECK (true);
CREATE INDEX idx_coletas_usuario_abertas ON public.coletas (usuario) WHERE fim_coleta IS NULL;
@@ -1,141 +0,0 @@
-- Perfis
CREATE TABLE public.perfis (
id uuid PRIMARY KEY,
email text NOT NULL,
aprovado boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
GRANT SELECT, INSERT, UPDATE ON public.perfis TO authenticated;
GRANT ALL ON public.perfis TO service_role;
ALTER TABLE public.perfis ENABLE ROW LEVEL SECURITY;
-- Papeis
CREATE TYPE public.app_role AS ENUM ('admin', 'user');
CREATE TABLE public.papeis_usuario (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL,
role public.app_role NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (user_id, role)
);
GRANT SELECT ON public.papeis_usuario TO authenticated;
GRANT ALL ON public.papeis_usuario TO service_role;
ALTER TABLE public.papeis_usuario ENABLE ROW LEVEL SECURITY;
CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role public.app_role)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT EXISTS (
SELECT 1 FROM public.papeis_usuario WHERE user_id = _user_id AND role = _role
)
$$;
CREATE OR REPLACE FUNCTION public.is_aprovado(_user_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT EXISTS (
SELECT 1 FROM public.perfis WHERE id = _user_id AND aprovado = true
)
$$;
-- Policies perfis
CREATE POLICY "Ver proprio perfil" ON public.perfis
FOR SELECT TO authenticated USING (id = auth.uid());
CREATE POLICY "Admins veem todos os perfis" ON public.perfis
FOR SELECT TO authenticated USING (public.has_role(auth.uid(), 'admin'));
CREATE POLICY "Criar proprio perfil" ON public.perfis
FOR INSERT TO authenticated WITH CHECK (id = auth.uid());
CREATE POLICY "Admins atualizam perfis" ON public.perfis
FOR UPDATE TO authenticated
USING (public.has_role(auth.uid(), 'admin'))
WITH CHECK (public.has_role(auth.uid(), 'admin'));
-- Policies papeis
CREATE POLICY "Ver proprios papeis" ON public.papeis_usuario
FOR SELECT TO authenticated USING (user_id = auth.uid());
CREATE POLICY "Admins veem todos os papeis" ON public.papeis_usuario
FOR SELECT TO authenticated USING (public.has_role(auth.uid(), 'admin'));
-- Registro de perfil: primeiro usuario vira admin aprovado
CREATE OR REPLACE FUNCTION public.registrar_perfil()
RETURNS public.perfis
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_uid uuid := auth.uid();
v_email text;
v_primeiro boolean;
v_perfil public.perfis;
BEGIN
IF v_uid IS NULL THEN
RAISE EXCEPTION 'Nao autenticado';
END IF;
SELECT * INTO v_perfil FROM public.perfis WHERE id = v_uid;
IF FOUND THEN
RETURN v_perfil;
END IF;
SELECT email INTO v_email FROM auth.users WHERE id = v_uid;
SELECT NOT EXISTS (SELECT 1 FROM public.perfis) INTO v_primeiro;
INSERT INTO public.perfis (id, email, aprovado)
VALUES (v_uid, coalesce(v_email, ''), v_primeiro)
RETURNING * INTO v_perfil;
IF v_primeiro THEN
INSERT INTO public.papeis_usuario (user_id, role) VALUES (v_uid, 'admin')
ON CONFLICT DO NOTHING;
END IF;
RETURN v_perfil;
END;
$$;
GRANT EXECUTE ON FUNCTION public.registrar_perfil() TO authenticated;
CREATE OR REPLACE FUNCTION public.set_updated_at()
RETURNS TRIGGER
LANGUAGE plpgsql
SET search_path = public
AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$;
CREATE TRIGGER perfis_updated_at BEFORE UPDATE ON public.perfis
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
-- Coletas: apenas usuarios logados e aprovados
DROP POLICY IF EXISTS "Coletas sao visiveis publicamente" ON public.coletas;
DROP POLICY IF EXISTS "Qualquer um pode atualizar coleta" ON public.coletas;
DROP POLICY IF EXISTS "Qualquer um pode criar coleta" ON public.coletas;
REVOKE ALL ON public.coletas FROM anon;
GRANT SELECT, INSERT, UPDATE ON public.coletas TO authenticated;
GRANT ALL ON public.coletas TO service_role;
CREATE POLICY "Aprovados veem coletas" ON public.coletas
FOR SELECT TO authenticated USING (public.is_aprovado(auth.uid()));
CREATE POLICY "Aprovados criam coletas" ON public.coletas
FOR INSERT TO authenticated WITH CHECK (public.is_aprovado(auth.uid()));
CREATE POLICY "Aprovados atualizam coletas" ON public.coletas
FOR UPDATE TO authenticated
USING (public.is_aprovado(auth.uid()))
WITH CHECK (public.is_aprovado(auth.uid()));
@@ -1,8 +0,0 @@
REVOKE ALL ON FUNCTION public.has_role(uuid, public.app_role) FROM PUBLIC, anon;
REVOKE ALL ON FUNCTION public.is_aprovado(uuid) FROM PUBLIC, anon;
REVOKE ALL ON FUNCTION public.registrar_perfil() FROM PUBLIC, anon;
REVOKE ALL ON FUNCTION public.set_updated_at() FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.has_role(uuid, public.app_role) TO authenticated;
GRANT EXECUTE ON FUNCTION public.is_aprovado(uuid) TO authenticated;
GRANT EXECUTE ON FUNCTION public.registrar_perfil() TO authenticated;
@@ -1,46 +0,0 @@
ALTER TABLE public.perfis ADD COLUMN IF NOT EXISTS nome text;
UPDATE public.perfis SET nome = split_part(email, '@', 1) WHERE nome IS NULL;
ALTER TABLE public.perfis ALTER COLUMN nome SET NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS perfis_nome_unico ON public.perfis (lower(nome));
CREATE OR REPLACE FUNCTION public.registrar_perfil()
RETURNS perfis
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public'
AS $function$
DECLARE
v_uid uuid := auth.uid();
v_email text;
v_nome text;
v_primeiro boolean;
v_perfil public.perfis;
BEGIN
IF v_uid IS NULL THEN
RAISE EXCEPTION 'Nao autenticado';
END IF;
SELECT * INTO v_perfil FROM public.perfis WHERE id = v_uid;
IF FOUND THEN
RETURN v_perfil;
END IF;
SELECT email INTO v_email FROM auth.users WHERE id = v_uid;
v_nome := split_part(coalesce(v_email, ''), '@', 1);
SELECT NOT EXISTS (SELECT 1 FROM public.perfis) INTO v_primeiro;
INSERT INTO public.perfis (id, email, nome, aprovado)
VALUES (v_uid, coalesce(v_email, ''), v_nome, v_primeiro)
RETURNING * INTO v_perfil;
IF v_primeiro THEN
INSERT INTO public.papeis_usuario (user_id, role) VALUES (v_uid, 'admin')
ON CONFLICT DO NOTHING;
END IF;
RETURN v_perfil;
END;
$function$;
@@ -1,4 +0,0 @@
REVOKE ALL ON FUNCTION public.has_role(uuid, public.app_role) FROM PUBLIC, anon, authenticated;
REVOKE ALL ON FUNCTION public.is_aprovado(uuid) FROM PUBLIC, anon, authenticated;
REVOKE ALL ON FUNCTION public.registrar_perfil() FROM PUBLIC, anon;
GRANT EXECUTE ON FUNCTION public.registrar_perfil() TO authenticated;
@@ -1,58 +0,0 @@
CREATE SCHEMA IF NOT EXISTS private;
REVOKE ALL ON SCHEMA private FROM PUBLIC, anon, authenticated;
GRANT USAGE ON SCHEMA private TO postgres, service_role;
CREATE OR REPLACE FUNCTION private.has_role(_user_id uuid, _role public.app_role)
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
AS $$ SELECT EXISTS (SELECT 1 FROM public.papeis_usuario WHERE user_id = _user_id AND role = _role) $$;
CREATE OR REPLACE FUNCTION private.is_aprovado(_user_id uuid)
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public'
AS $$ SELECT EXISTS (SELECT 1 FROM public.perfis WHERE id = _user_id AND aprovado = true) $$;
DROP POLICY "Admins veem todos os papeis" ON public.papeis_usuario;
CREATE POLICY "Admins veem todos os papeis" ON public.papeis_usuario FOR SELECT TO authenticated USING (private.has_role(auth.uid(), 'admin'::public.app_role));
DROP POLICY "Admins atualizam perfis" ON public.perfis;
CREATE POLICY "Admins atualizam perfis" ON public.perfis FOR UPDATE TO authenticated USING (private.has_role(auth.uid(), 'admin'::public.app_role)) WITH CHECK (private.has_role(auth.uid(), 'admin'::public.app_role));
DROP POLICY "Admins veem todos os perfis" ON public.perfis;
CREATE POLICY "Admins veem todos os perfis" ON public.perfis FOR SELECT TO authenticated USING (private.has_role(auth.uid(), 'admin'::public.app_role));
DROP POLICY "Aprovados atualizam coletas" ON public.coletas;
CREATE POLICY "Aprovados atualizam coletas" ON public.coletas FOR UPDATE TO authenticated USING (private.is_aprovado(auth.uid())) WITH CHECK (private.is_aprovado(auth.uid()));
DROP POLICY "Aprovados criam coletas" ON public.coletas;
CREATE POLICY "Aprovados criam coletas" ON public.coletas FOR INSERT TO authenticated WITH CHECK (private.is_aprovado(auth.uid()));
DROP POLICY "Aprovados veem coletas" ON public.coletas;
CREATE POLICY "Aprovados veem coletas" ON public.coletas FOR SELECT TO authenticated USING (private.is_aprovado(auth.uid()));
DROP FUNCTION IF EXISTS public.has_role(uuid, public.app_role);
DROP FUNCTION IF EXISTS public.is_aprovado(uuid);
CREATE OR REPLACE FUNCTION private.registrar_perfil_para(_uid uuid, _email text)
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
AS $$
DECLARE v_primeiro boolean;
BEGIN
IF EXISTS (SELECT 1 FROM public.perfis WHERE id = _uid) THEN RETURN; END IF;
SELECT NOT EXISTS (SELECT 1 FROM public.perfis) INTO v_primeiro;
INSERT INTO public.perfis (id, email, nome, aprovado)
VALUES (_uid, coalesce(_email, ''), split_part(coalesce(_email, ''), '@', 1), v_primeiro);
IF v_primeiro THEN
INSERT INTO public.papeis_usuario (user_id, role) VALUES (_uid, 'admin') ON CONFLICT DO NOTHING;
END IF;
END;
$$;
CREATE OR REPLACE FUNCTION private.on_auth_user_created()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public'
AS $$
BEGIN
PERFORM private.registrar_perfil_para(NEW.id, NEW.email);
RETURN NEW;
END;
$$;
DROP FUNCTION IF EXISTS public.registrar_perfil();