adição de recuperação de senha e limpeza de remanescentes
This commit is contained in:
@@ -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_HOST="10.77.77.10"
|
||||||
DB_PORT="1433"
|
DB_PORT="1433"
|
||||||
DB_NAME="GINSENG"
|
DB_NAME="GINSENG"
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@supabase/supabase-js": "^2.111.0",
|
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@tanstack/react-query": "^5.101.1",
|
"@tanstack/react-query": "^5.101.1",
|
||||||
"@tanstack/react-router": "^1.170.18",
|
"@tanstack/react-router": "^1.170.18",
|
||||||
@@ -379,20 +378,6 @@
|
|||||||
|
|
||||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
||||||
|
|||||||
Generated
-102
@@ -33,7 +33,6 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@supabase/supabase-js": "^2.111.0",
|
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@tanstack/react-query": "^5.101.1",
|
"@tanstack/react-query": "^5.101.1",
|
||||||
"@tanstack/react-router": "^1.170.18",
|
"@tanstack/react-router": "^1.170.18",
|
||||||
@@ -3151,98 +3150,6 @@
|
|||||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@tabby_ai/hijri-converter": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz",
|
||||||
@@ -6346,15 +6253,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@supabase/supabase-js": "^2.111.0",
|
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@tanstack/react-query": "^5.101.1",
|
"@tanstack/react-query": "^5.101.1",
|
||||||
"@tanstack/react-router": "^1.170.18",
|
"@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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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 () => {
|
export const sair = createServerFn({ method: "POST" }).handler(async () => {
|
||||||
const { clearAuthSession } = await import("@/lib/auth.server");
|
const { clearAuthSession } = await import("@/lib/auth.server");
|
||||||
await clearAuthSession();
|
await clearAuthSession();
|
||||||
|
|||||||
+15
-1
@@ -35,6 +35,7 @@ export const iniciarColeta = createServerFn({ method: "POST" })
|
|||||||
const sessao = await obterSessaoAprovada();
|
const sessao = await obterSessaoAprovada();
|
||||||
const [{ getPool, sql }] = await Promise.all([import("@/lib/db.server")]);
|
const [{ getPool, sql }] = await Promise.all([import("@/lib/db.server")]);
|
||||||
const pool = await getPool();
|
const pool = await getPool();
|
||||||
|
const codigo = data.codigo.trim();
|
||||||
|
|
||||||
const dataAberta = await pool
|
const dataAberta = await pool
|
||||||
.request()
|
.request()
|
||||||
@@ -48,10 +49,23 @@ export const iniciarColeta = createServerFn({ method: "POST" })
|
|||||||
|
|
||||||
if (dataAberta.recordset[0]) throw new Error("Já existe uma coleta em andamento.");
|
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
|
const result = await pool
|
||||||
.request()
|
.request()
|
||||||
.input("usuario", sql.NVarChar(150), sessao.nome)
|
.input("usuario", sql.NVarChar(150), sessao.nome)
|
||||||
.input("codigo", sql.NVarChar(100), data.codigo)
|
.input("codigo", sql.NVarChar(100), codigo)
|
||||||
.query<ColetaRow>(`
|
.query<ColetaRow>(`
|
||||||
INSERT INTO slalog.coletas (usuario, codigo_barras, inicio_coleta, created_at)
|
INSERT INTO slalog.coletas (usuario, codigo_barras, inicio_coleta, created_at)
|
||||||
OUTPUT
|
OUTPUT
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ export const Route = createFileRoute("/_authenticated/coleta")({
|
|||||||
{
|
{
|
||||||
name: "description",
|
name: "description",
|
||||||
content:
|
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:title", content: "Coleta de Caixas | Registro de pedidos" },
|
||||||
{
|
{
|
||||||
property: "og:description",
|
property: "og:description",
|
||||||
content:
|
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" },
|
{ property: "og:type", content: "website" },
|
||||||
{ name: "twitter:card", content: "summary" },
|
{ name: "twitter:card", content: "summary" },
|
||||||
@@ -129,7 +129,7 @@ function ColetaScreen() {
|
|||||||
async function handleIniciar() {
|
async function handleIniciar() {
|
||||||
const valor = codigo.trim();
|
const valor = codigo.trim();
|
||||||
if (!valor) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (coletaAtiva) {
|
if (coletaAtiva) {
|
||||||
@@ -146,7 +146,14 @@ function ColetaScreen() {
|
|||||||
toast.success("Coleta iniciada.");
|
toast.success("Coleta iniciada.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(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 {
|
} finally {
|
||||||
setCarregando(false);
|
setCarregando(false);
|
||||||
}
|
}
|
||||||
@@ -162,7 +169,7 @@ function ColetaScreen() {
|
|||||||
inicioRef.current = null;
|
inicioRef.current = null;
|
||||||
setConfirmarFim(false);
|
setConfirmarFim(false);
|
||||||
toast.success(
|
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);
|
if (adicionarOutro) window.setTimeout(() => inputRef.current?.focus(), 50);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -242,7 +249,7 @@ function ColetaScreen() {
|
|||||||
|
|
||||||
<section className="space-y-3 rounded-2xl border border-border bg-card p-4 shadow-soft">
|
<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">
|
<label htmlFor="codigo" className="text-sm font-medium text-muted-foreground">
|
||||||
Código do pedido
|
Número de caixa
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
@@ -257,7 +264,7 @@ function ColetaScreen() {
|
|||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
pattern="[0-9]*"
|
pattern="[0-9]*"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder="Digite ou cole o número do pedido"
|
placeholder="Digite ou cole o número de caixa"
|
||||||
disabled={Boolean(coletaAtiva) || verificando}
|
disabled={Boolean(coletaAtiva) || verificando}
|
||||||
className="h-12 flex-1 rounded-xl text-base tabular-nums"
|
className="h-12 flex-1 rounded-xl text-base tabular-nums"
|
||||||
/>
|
/>
|
||||||
@@ -325,7 +332,7 @@ function ColetaScreen() {
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Deseja realmente finalizar esta coleta?</AlertDialogTitle>
|
<AlertDialogTitle>Deseja realmente finalizar esta coleta?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<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>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
|
|||||||
+98
-11
@@ -1,12 +1,12 @@
|
|||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
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")({
|
export const Route = createFileRoute("/auth")({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
@@ -32,11 +32,14 @@ export const Route = createFileRoute("/auth")({
|
|||||||
|
|
||||||
function AuthPage() {
|
function AuthPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [modo, setModo] = useState<"entrar" | "criar">("entrar");
|
const [modo, setModo] = useState<"entrar" | "criar" | "recuperar">("entrar");
|
||||||
const [nome, setNome] = useState("");
|
const [nome, setNome] = useState("");
|
||||||
const [senha, setSenha] = useState("");
|
const [senha, setSenha] = useState("");
|
||||||
|
const [confirmarSenha, setConfirmarSenha] = useState("");
|
||||||
|
const [mostrarSenha, setMostrarSenha] = useState(false);
|
||||||
const [carregando, setCarregando] = useState(false);
|
const [carregando, setCarregando] = useState(false);
|
||||||
const [verificando, setVerificando] = useState(true);
|
const [verificando, setVerificando] = useState(true);
|
||||||
|
const precisaConfirmarSenha = modo !== "entrar";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelado = false;
|
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.");
|
toast.error("Informe um nome com pelo menos 3 caracteres e senha com 6 ou mais.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (precisaConfirmarSenha && senha !== confirmarSenha) {
|
||||||
|
toast.error("As senhas digitadas não coincidem.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setCarregando(true);
|
setCarregando(true);
|
||||||
try {
|
try {
|
||||||
if (modo === "criar") {
|
if (modo === "criar") {
|
||||||
@@ -66,6 +73,10 @@ function AuthPage() {
|
|||||||
? "Conta criada e liberada. Faça login."
|
? "Conta criada e liberada. Faça login."
|
||||||
: "Conta criada! Aguarde a liberação de um administrador.",
|
: "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 {
|
} else {
|
||||||
await entrar({ data: { nome: usuario, senha } });
|
await entrar({ data: { nome: usuario, senha } });
|
||||||
void navigate({ to: "/coleta", replace: true });
|
void navigate({ to: "/coleta", replace: true });
|
||||||
@@ -76,6 +87,8 @@ function AuthPage() {
|
|||||||
toast.error(
|
toast.error(
|
||||||
msg.includes("incorretos")
|
msg.includes("incorretos")
|
||||||
? "Nome de usuário ou senha incorretos."
|
? "Nome de usuário ou senha incorretos."
|
||||||
|
: msg.includes("não encontrado")
|
||||||
|
? "Nome de usuário não encontrado."
|
||||||
: msg.includes("já existe")
|
: msg.includes("já existe")
|
||||||
? "Este nome de usuário já existe. Faça login."
|
? "Este nome de usuário já existe. Faça login."
|
||||||
: "Não foi possível concluir. Tente novamente.",
|
: "Não foi possível concluir. Tente novamente.",
|
||||||
@@ -104,7 +117,9 @@ function AuthPage() {
|
|||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{modo === "entrar"
|
{modo === "entrar"
|
||||||
? "Entre com seu nome de usuário e senha."
|
? "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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -124,16 +139,51 @@ function AuthPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="senha">Senha</Label>
|
<Label htmlFor="senha">Senha</Label>
|
||||||
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
id="senha"
|
id="senha"
|
||||||
type="password"
|
type={mostrarSenha ? "text" : "password"}
|
||||||
autoComplete={modo === "entrar" ? "current-password" : "new-password"}
|
autoComplete={modo === "entrar" ? "current-password" : "new-password"}
|
||||||
value={senha}
|
value={senha}
|
||||||
onChange={(e) => setSenha(e.target.value)}
|
onChange={(e) => setSenha(e.target.value)}
|
||||||
placeholder="Mínimo de 6 caracteres"
|
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>
|
||||||
|
</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
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -144,20 +194,57 @@ function AuthPage() {
|
|||||||
<Loader2 className="h-5 w-5 animate-spin" />
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
) : modo === "entrar" ? (
|
) : modo === "entrar" ? (
|
||||||
<LogIn className="h-5 w-5" />
|
<LogIn className="h-5 w-5" />
|
||||||
) : (
|
) : modo === "criar" ? (
|
||||||
<UserPlus className="h-5 w-5" />
|
<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>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div className="space-y-2 text-center text-sm">
|
||||||
|
{modo !== "entrar" ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full text-center text-sm text-muted-foreground underline-offset-4 hover:underline"
|
className="w-full text-muted-foreground underline-offset-4 hover:underline"
|
||||||
onClick={() => setModo(modo === "entrar" ? "criar" : "entrar")}
|
onClick={() => {
|
||||||
|
setModo("entrar");
|
||||||
|
setConfirmarSenha("");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{modo === "entrar" ? "Não tenho conta — criar agora" : "Já tenho conta — entrar"}
|
Já tenho conta — entrar
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
|
||||||
Reference in New Issue
Block a user