@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 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*/
@@ -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
+ }