@tabl.io/auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -0
- package/dist/index.css +3 -0
- package/dist/index.d.ts +149 -0
- package/dist/index.js +210 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @tabl.io/auth
|
|
2
|
+
|
|
3
|
+
Autenticação e autorização compartilhada para os apps Tabl com login real (Manager, Cashier).
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tabl.io/auth
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
O pacote tem `@supabase/supabase-js` e `react` como `peerDependencies` — devem estar instalados no app consumidor.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
### 1. Criar o cliente Supabase (`src/api/supabase.ts`)
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createTablaClient } from '@tabl.io/auth'
|
|
21
|
+
|
|
22
|
+
export const supabase = createTablaClient({
|
|
23
|
+
url: import.meta.env.VITE_TABL_SERVER_URL,
|
|
24
|
+
anonKey: import.meta.env.VITE_TABL_SERVER_ANON_KEY,
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Inicializar no boot do app (`src/App.tsx`)
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { useEffect } from 'react'
|
|
32
|
+
import { useAuthStore, LoginPage } from '@tabl.io/auth'
|
|
33
|
+
import { supabase } from './api/supabase'
|
|
34
|
+
|
|
35
|
+
export default function App() {
|
|
36
|
+
const { init, isInitializing, isAuthenticated } = useAuthStore()
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const unsubscribe = init(supabase)
|
|
40
|
+
return unsubscribe // cleanup do listener ao desmontar
|
|
41
|
+
}, [])
|
|
42
|
+
|
|
43
|
+
if (isInitializing) return <Loading />
|
|
44
|
+
|
|
45
|
+
if (!isAuthenticated) {
|
|
46
|
+
return (
|
|
47
|
+
<div data-module="manager"> {/* ativa tema de cor do módulo */}
|
|
48
|
+
<LoginPage supabase={supabase} moduleName="Manager" />
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div data-module="manager">
|
|
55
|
+
<RouterProvider router={router} />
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Para o Cashier, substituir `data-module="manager"` por `data-module="cashier"` e `moduleName="Caixa"`.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Proteger rotas
|
|
66
|
+
|
|
67
|
+
### Via hook — redireciona se não autorizado
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { useRequireRole } from '@tabl.io/auth'
|
|
71
|
+
|
|
72
|
+
export function SettingsPage() {
|
|
73
|
+
const navigate = useNavigate()
|
|
74
|
+
useRequireRole('manager', () => navigate('/'))
|
|
75
|
+
// ...
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Via componente — oculta elemento de UI
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { RoleGuard } from '@tabl.io/auth'
|
|
83
|
+
|
|
84
|
+
<RoleGuard role="manager">
|
|
85
|
+
<DeleteButton />
|
|
86
|
+
</RoleGuard>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Verificar permissão em código
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { useAuth } from '@tabl.io/auth'
|
|
95
|
+
|
|
96
|
+
const { user, role, can } = useAuth()
|
|
97
|
+
|
|
98
|
+
can('manager') // true se role >= manager
|
|
99
|
+
can('cashier') // true se role >= cashier
|
|
100
|
+
can('supervisor') // true para qualquer role autenticado
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Criar usuários com role
|
|
106
|
+
|
|
107
|
+
Via Supabase Dashboard ou edge function administrativa.
|
|
108
|
+
O campo `role` deve ficar em `app_metadata` — nunca em `user_metadata` (editável pelo próprio usuário).
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
await supabase.auth.admin.createUser({
|
|
112
|
+
email: 'gerente@restaurante.com',
|
|
113
|
+
password: 'senha-segura',
|
|
114
|
+
email_confirm: true,
|
|
115
|
+
app_metadata: { role: 'manager' }, // 'manager' | 'cashier' | 'supervisor'
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Hierarquia de roles
|
|
122
|
+
|
|
123
|
+
| Role | Acesso |
|
|
124
|
+
|--------------|-------------------------------------|
|
|
125
|
+
| `manager` | Total — gestão, caixa, supervisão |
|
|
126
|
+
| `cashier` | Operação de caixa |
|
|
127
|
+
| `supervisor` | Visualização e aprovações |
|
|
128
|
+
|
|
129
|
+
`hasRole('manager', 'cashier')` → `true` (manager pode tudo que cashier pode)
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Tokens de cor por módulo (`@tabl.io/ui`)
|
|
134
|
+
|
|
135
|
+
Adicionar `cashier-token.patch.css` em `@tabl.io/ui/src/foundation/tokens.css`:
|
|
136
|
+
|
|
137
|
+
| `data-module` | Cor | App |
|
|
138
|
+
|---------------|---------|------------|
|
|
139
|
+
| `pdv` | Laranja | PDV |
|
|
140
|
+
| `kds` | Azul | KDS |
|
|
141
|
+
| `manager` | Roxo | Manager |
|
|
142
|
+
| `cashier` | Teal | Cashier |
|
package/dist/index.css
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--container-sm:24rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-widest:.1em;--radius-md:.375rem;--radius-xl:.75rem;--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.mb-2{margin-bottom:calc(var(--spacing) * 2)}.flex{display:flex}.inline-flex{display:inline-flex}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-sm{max-width:var(--container-sm)}.animate-spin{animation:var(--animate-spin)}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.uppercase{text-transform:uppercase}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@keyframes spin{to{transform:rotate(360deg)}}
|
|
3
|
+
/*$vite$:1*/
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { JSX } from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from '../../node_modules/react';
|
|
3
|
+
import { StoreApi } from '../../node_modules/zustand';
|
|
4
|
+
import { SupabaseClient } from '../../node_modules/@supabase/supabase-js';
|
|
5
|
+
import { UseBoundStore } from '../../node_modules/zustand';
|
|
6
|
+
import { User } from '../../node_modules/@supabase/supabase-js';
|
|
7
|
+
import { User as User_2 } from '@supabase/auth-js';
|
|
8
|
+
|
|
9
|
+
export declare interface AuthState {
|
|
10
|
+
user: User | null;
|
|
11
|
+
role: UserRole | null;
|
|
12
|
+
isInitializing: boolean;
|
|
13
|
+
isAuthenticated: boolean;
|
|
14
|
+
error: string | null;
|
|
15
|
+
/** Inicializa o listener de sessão — chamar uma vez no boot do app, retorna unsubscribe */
|
|
16
|
+
init: (supabase: SupabaseClient) => () => void;
|
|
17
|
+
/** Login com email e senha */
|
|
18
|
+
signIn: (supabase: SupabaseClient, email: string, password: string) => Promise<void>;
|
|
19
|
+
/** Logout */
|
|
20
|
+
signOut: (supabase: SupabaseClient) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cria um cliente Supabase com as configurações padrão dos apps Tabl.
|
|
25
|
+
* Cada app instancia o seu próprio cliente via esta factory.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // src/api/supabase.ts de cada app
|
|
29
|
+
* export const supabase = createTablaClient({
|
|
30
|
+
* url: import.meta.env.VITE_TABL_SERVER_URL,
|
|
31
|
+
* anonKey: import.meta.env.VITE_TABL_SERVER_ANON_KEY,
|
|
32
|
+
* })
|
|
33
|
+
*/
|
|
34
|
+
export declare function createTablaClient(config: TablaClientConfig): SupabaseClient;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extrai o role do app_metadata do JWT Supabase.
|
|
38
|
+
* Retorna null se ausente ou inválido — nunca lança exceção.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getRoleFromMetadata(appMetadata: Record<string, unknown> | null | undefined): UserRole | null;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Verifica se o role do usuário satisfaz o role mínimo requerido.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* hasRole('manager', 'cashier') // true — manager pode tudo que cashier pode
|
|
47
|
+
* hasRole('cashier', 'manager') // false — cashier não acessa funções de manager
|
|
48
|
+
* hasRole('cashier', 'cashier') // true — mesmo nível
|
|
49
|
+
*/
|
|
50
|
+
export declare function hasRole(userRole: UserRole, requiredRole: UserRole): boolean;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Tela de login reutilizável para Manager e Cashier.
|
|
54
|
+
* Estilizada com tokens CSS do @tabl.io/ui — aplica `data-module` no elemento
|
|
55
|
+
* raiz do app para herdar a cor primária correta de cada módulo.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // App.tsx do manager
|
|
59
|
+
* <div data-module="manager">
|
|
60
|
+
* <LoginPage supabase={supabase} moduleName="Manager" />
|
|
61
|
+
* </div>
|
|
62
|
+
*
|
|
63
|
+
* // App.tsx do cashier
|
|
64
|
+
* <div data-module="cashier">
|
|
65
|
+
* <LoginPage supabase={supabase} moduleName="Caixa" />
|
|
66
|
+
* </div>
|
|
67
|
+
*/
|
|
68
|
+
export declare function LoginPage({ supabase, moduleName, logo }: LoginPageProps): JSX.Element;
|
|
69
|
+
|
|
70
|
+
export declare interface LoginPageProps {
|
|
71
|
+
/** Cliente Supabase do app consumidor */
|
|
72
|
+
supabase: SupabaseClient;
|
|
73
|
+
/** Nome do módulo exibido como título — ex: "Manager", "Caixa" */
|
|
74
|
+
moduleName: string;
|
|
75
|
+
/** Logo ou ícone exibido acima do form */
|
|
76
|
+
logo?: React.ReactNode;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Wrapper declarativo que protege conteúdo por role.
|
|
81
|
+
* Não redireciona — use `useRequireRole` para redirecionamento de rota.
|
|
82
|
+
* Use RoleGuard para ocultar elementos de UI condicionalmente.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* <RoleGuard role="manager">
|
|
86
|
+
* <DeleteButton />
|
|
87
|
+
* </RoleGuard>
|
|
88
|
+
*/
|
|
89
|
+
export declare function RoleGuard({ role, fallback, children }: RoleGuardProps): JSX.Element;
|
|
90
|
+
|
|
91
|
+
export declare interface RoleGuardProps {
|
|
92
|
+
/** Role mínimo requerido para renderizar children */
|
|
93
|
+
role: UserRole;
|
|
94
|
+
/** Conteúdo exibido enquanto verifica autenticação ou se não autorizado */
|
|
95
|
+
fallback?: ReactNode;
|
|
96
|
+
children: ReactNode;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export declare interface TablaClientConfig {
|
|
100
|
+
url: string;
|
|
101
|
+
anonKey: string;
|
|
102
|
+
/** Padrão 10 — reduzir em dispositivos com banda limitada */
|
|
103
|
+
realtimeEventsPerSecond?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Hook principal de autenticação — estado atual e helper de permissão.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* const { user, role, isAuthenticated, can } = useAuth()
|
|
111
|
+
*
|
|
112
|
+
* if (can('manager')) {
|
|
113
|
+
* // exibe funções restritas a manager
|
|
114
|
+
* }
|
|
115
|
+
*/
|
|
116
|
+
export declare function useAuth(): {
|
|
117
|
+
user: User_2 | null;
|
|
118
|
+
role: UserRole | null;
|
|
119
|
+
isInitializing: boolean;
|
|
120
|
+
isAuthenticated: boolean;
|
|
121
|
+
error: string | null;
|
|
122
|
+
can: (requiredRole: UserRole) => boolean;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export declare const useAuthStore: UseBoundStore<StoreApi<AuthState>>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Guard declarativo para rotas protegidas por role.
|
|
129
|
+
* Chama `onUnauthorized` se o usuário não tiver o role mínimo.
|
|
130
|
+
* Não age enquanto `isInitializing` for true.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* export function SettingsPage() {
|
|
134
|
+
* const navigate = useNavigate()
|
|
135
|
+
* useRequireRole('manager', () => navigate('/'))
|
|
136
|
+
* // ...
|
|
137
|
+
* }
|
|
138
|
+
*/
|
|
139
|
+
export declare function useRequireRole(requiredRole: UserRole, onUnauthorized: () => void): void;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Roles de usuário humano — autenticação real via email/senha.
|
|
143
|
+
* Distinto de DeviceMode (staff/client) que é autenticação de dispositivo anônimo.
|
|
144
|
+
*
|
|
145
|
+
* Armazenado em auth.users.app_metadata.role via Supabase Admin API.
|
|
146
|
+
*/
|
|
147
|
+
export declare type UserRole = 'manager' | 'cashier' | 'supervisor';
|
|
148
|
+
|
|
149
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { create as v } from "zustand";
|
|
2
|
+
import { useEffect as w, useState as m } from "react";
|
|
3
|
+
import { Loader2 as N } from "lucide-react";
|
|
4
|
+
import { Fragment as f, jsx as i, jsxs as l } from "react/jsx-runtime";
|
|
5
|
+
import { createClient as E } from "@supabase/supabase-js";
|
|
6
|
+
var g = {
|
|
7
|
+
supervisor: 1,
|
|
8
|
+
cashier: 2,
|
|
9
|
+
manager: 3
|
|
10
|
+
};
|
|
11
|
+
function I(t, e) {
|
|
12
|
+
return g[t] >= g[e];
|
|
13
|
+
}
|
|
14
|
+
function p(t) {
|
|
15
|
+
const e = t?.role;
|
|
16
|
+
return e === "manager" || e === "cashier" || e === "supervisor" ? e : null;
|
|
17
|
+
}
|
|
18
|
+
const b = v()((t) => ({
|
|
19
|
+
user: null,
|
|
20
|
+
role: null,
|
|
21
|
+
isInitializing: !0,
|
|
22
|
+
isAuthenticated: !1,
|
|
23
|
+
error: null,
|
|
24
|
+
init: (e) => {
|
|
25
|
+
e.auth.getSession().then(({ data: { session: r } }) => {
|
|
26
|
+
r?.user ? t({
|
|
27
|
+
user: r.user,
|
|
28
|
+
role: p(r.user.app_metadata),
|
|
29
|
+
isAuthenticated: !0,
|
|
30
|
+
isInitializing: !1
|
|
31
|
+
}) : t({ isInitializing: !1 });
|
|
32
|
+
});
|
|
33
|
+
const { data: { subscription: a } } = e.auth.onAuthStateChange((r, n) => {
|
|
34
|
+
n?.user ? t({
|
|
35
|
+
user: n.user,
|
|
36
|
+
role: p(n.user.app_metadata),
|
|
37
|
+
isAuthenticated: !0,
|
|
38
|
+
isInitializing: !1,
|
|
39
|
+
error: null
|
|
40
|
+
}) : t({
|
|
41
|
+
user: null,
|
|
42
|
+
role: null,
|
|
43
|
+
isAuthenticated: !1,
|
|
44
|
+
isInitializing: !1
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
return () => a.unsubscribe();
|
|
48
|
+
},
|
|
49
|
+
signIn: async (e, a, r) => {
|
|
50
|
+
t({ error: null });
|
|
51
|
+
const { data: n, error: s } = await e.auth.signInWithPassword({
|
|
52
|
+
email: a,
|
|
53
|
+
password: r
|
|
54
|
+
});
|
|
55
|
+
if (s)
|
|
56
|
+
throw t({ error: s.message }), s;
|
|
57
|
+
if (!p(n.user?.app_metadata)) {
|
|
58
|
+
await e.auth.signOut();
|
|
59
|
+
const o = "Usuário sem permissão de acesso.";
|
|
60
|
+
throw t({ error: o }), new Error(o);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
signOut: async (e) => {
|
|
64
|
+
await e.auth.signOut();
|
|
65
|
+
}
|
|
66
|
+
}));
|
|
67
|
+
function x() {
|
|
68
|
+
const { user: t, role: e, isInitializing: a, isAuthenticated: r, error: n } = b();
|
|
69
|
+
function s(o) {
|
|
70
|
+
return e ? I(e, o) : !1;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
user: t,
|
|
74
|
+
role: e,
|
|
75
|
+
isInitializing: a,
|
|
76
|
+
isAuthenticated: r,
|
|
77
|
+
error: n,
|
|
78
|
+
can: s
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function L(t, e) {
|
|
82
|
+
const { isInitializing: a, isAuthenticated: r, can: n } = x();
|
|
83
|
+
w(() => {
|
|
84
|
+
a || (!r || !n(t)) && e();
|
|
85
|
+
}, [
|
|
86
|
+
a,
|
|
87
|
+
r,
|
|
88
|
+
t
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
function T({ supabase: t, moduleName: e, logo: a }) {
|
|
92
|
+
const { signIn: r, error: n } = b(), [s, o] = m(""), [c, y] = m(""), [d, h] = m(!1);
|
|
93
|
+
return /* @__PURE__ */ i("div", {
|
|
94
|
+
className: "flex min-h-screen flex-col items-center justify-center bg-background p-4",
|
|
95
|
+
children: /* @__PURE__ */ l("div", {
|
|
96
|
+
className: "w-full max-w-sm space-y-8",
|
|
97
|
+
children: [/* @__PURE__ */ l("div", {
|
|
98
|
+
className: "flex flex-col items-center gap-3 text-center",
|
|
99
|
+
children: [a && /* @__PURE__ */ i("div", {
|
|
100
|
+
className: "mb-2",
|
|
101
|
+
children: a
|
|
102
|
+
}), /* @__PURE__ */ l("div", { children: [/* @__PURE__ */ i("p", {
|
|
103
|
+
className: "text-xs font-bold uppercase tracking-widest text-muted-foreground",
|
|
104
|
+
children: "tabl.io"
|
|
105
|
+
}), /* @__PURE__ */ i("h1", {
|
|
106
|
+
className: "text-2xl font-bold",
|
|
107
|
+
children: e
|
|
108
|
+
})] })]
|
|
109
|
+
}), /* @__PURE__ */ l("div", {
|
|
110
|
+
className: "rounded-xl border bg-card p-6 shadow-sm space-y-4",
|
|
111
|
+
children: [/* @__PURE__ */ l("div", {
|
|
112
|
+
className: "space-y-1",
|
|
113
|
+
children: [/* @__PURE__ */ i("h2", {
|
|
114
|
+
className: "text-base font-semibold",
|
|
115
|
+
children: "Entrar"
|
|
116
|
+
}), /* @__PURE__ */ i("p", {
|
|
117
|
+
className: "text-sm text-muted-foreground",
|
|
118
|
+
children: "Use suas credenciais de acesso"
|
|
119
|
+
})]
|
|
120
|
+
}), /* @__PURE__ */ l("form", {
|
|
121
|
+
onSubmit: async (u) => {
|
|
122
|
+
if (u.preventDefault(), !(!s || !c)) {
|
|
123
|
+
h(!0);
|
|
124
|
+
try {
|
|
125
|
+
await r(t, s, c);
|
|
126
|
+
} finally {
|
|
127
|
+
h(!1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
className: "space-y-3",
|
|
132
|
+
children: [
|
|
133
|
+
/* @__PURE__ */ l("div", {
|
|
134
|
+
className: "space-y-1.5",
|
|
135
|
+
children: [/* @__PURE__ */ i("label", {
|
|
136
|
+
htmlFor: "email",
|
|
137
|
+
className: "text-sm font-medium",
|
|
138
|
+
children: "E-mail"
|
|
139
|
+
}), /* @__PURE__ */ i("input", {
|
|
140
|
+
id: "email",
|
|
141
|
+
type: "email",
|
|
142
|
+
autoComplete: "email",
|
|
143
|
+
autoFocus: !0,
|
|
144
|
+
required: !0,
|
|
145
|
+
value: s,
|
|
146
|
+
onChange: (u) => o(u.target.value),
|
|
147
|
+
disabled: d,
|
|
148
|
+
placeholder: "voce@restaurante.com",
|
|
149
|
+
className: "w-full rounded-md border bg-background px-3 py-2 text-sm outline-none transition focus:ring-2 focus:ring-ring/50 disabled:opacity-50"
|
|
150
|
+
})]
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ l("div", {
|
|
153
|
+
className: "space-y-1.5",
|
|
154
|
+
children: [/* @__PURE__ */ i("label", {
|
|
155
|
+
htmlFor: "password",
|
|
156
|
+
className: "text-sm font-medium",
|
|
157
|
+
children: "Senha"
|
|
158
|
+
}), /* @__PURE__ */ i("input", {
|
|
159
|
+
id: "password",
|
|
160
|
+
type: "password",
|
|
161
|
+
autoComplete: "current-password",
|
|
162
|
+
required: !0,
|
|
163
|
+
value: c,
|
|
164
|
+
onChange: (u) => y(u.target.value),
|
|
165
|
+
disabled: d,
|
|
166
|
+
placeholder: "••••••••",
|
|
167
|
+
className: "w-full rounded-md border bg-background px-3 py-2 text-sm outline-none transition focus:ring-2 focus:ring-ring/50 disabled:opacity-50"
|
|
168
|
+
})]
|
|
169
|
+
}),
|
|
170
|
+
n && /* @__PURE__ */ i("p", {
|
|
171
|
+
className: "rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive",
|
|
172
|
+
children: n
|
|
173
|
+
}),
|
|
174
|
+
/* @__PURE__ */ i("button", {
|
|
175
|
+
type: "submit",
|
|
176
|
+
disabled: d || !s || !c,
|
|
177
|
+
className: "inline-flex w-full items-center justify-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50",
|
|
178
|
+
children: d ? /* @__PURE__ */ i(N, { className: "size-4 animate-spin" }) : "Entrar"
|
|
179
|
+
})
|
|
180
|
+
]
|
|
181
|
+
})]
|
|
182
|
+
})]
|
|
183
|
+
})
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function F({ role: t, fallback: e = null, children: a }) {
|
|
187
|
+
const { isInitializing: r, can: n } = x();
|
|
188
|
+
return r ? /* @__PURE__ */ i(f, { children: e }) : n(t) ? /* @__PURE__ */ i(f, { children: a }) : /* @__PURE__ */ i(f, { children: e });
|
|
189
|
+
}
|
|
190
|
+
function O(t) {
|
|
191
|
+
const { url: e, anonKey: a, realtimeEventsPerSecond: r = 10 } = t;
|
|
192
|
+
if (!e || !a) throw new Error("[tabl.io/auth] VITE_TABL_SERVER_URL e VITE_TABL_SERVER_ANON_KEY são obrigatórios.");
|
|
193
|
+
return E(e, a, {
|
|
194
|
+
auth: {
|
|
195
|
+
persistSession: !0,
|
|
196
|
+
autoRefreshToken: !0
|
|
197
|
+
},
|
|
198
|
+
realtime: { params: { eventsPerSecond: r } }
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export {
|
|
202
|
+
T as LoginPage,
|
|
203
|
+
F as RoleGuard,
|
|
204
|
+
O as createTablaClient,
|
|
205
|
+
p as getRoleFromMetadata,
|
|
206
|
+
I as hasRole,
|
|
207
|
+
x as useAuth,
|
|
208
|
+
b as useAuthStore,
|
|
209
|
+
L as useRequireRole
|
|
210
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tabl.io/auth",
|
|
3
|
+
"private": false,
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"style": "./dist/index.css",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "vite build",
|
|
24
|
+
"lint": "eslint .",
|
|
25
|
+
"typecheck": "tsc -p tsconfig.app.json"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"lucide-react": "^0.563.0",
|
|
29
|
+
"zustand": "^5.0.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@supabase/supabase-js": "^2.0.0",
|
|
33
|
+
"react": "^19",
|
|
34
|
+
"react-dom": "^19"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@eslint/js": "^9.39.1",
|
|
38
|
+
"@supabase/supabase-js": "^2.0.0",
|
|
39
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
40
|
+
"@types/node": "^24.10.1",
|
|
41
|
+
"@types/react": "^19.2.7",
|
|
42
|
+
"@types/react-dom": "^19.2.3",
|
|
43
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
44
|
+
"esbuild": "^0.27.3",
|
|
45
|
+
"eslint": "^9.39.1",
|
|
46
|
+
"react": "^19",
|
|
47
|
+
"react-dom": "^19",
|
|
48
|
+
"tailwindcss": "^4.1.18",
|
|
49
|
+
"tw-animate-css": "^1.4.0",
|
|
50
|
+
"typescript": "~5.9.3",
|
|
51
|
+
"typescript-eslint": "^8.48.0",
|
|
52
|
+
"vite": "^8.0.0-beta.13",
|
|
53
|
+
"vite-plugin-dts": "^4.5.4"
|
|
54
|
+
}
|
|
55
|
+
}
|