Merge pull request #25 from rede5/codex/implementar-no-marketplace

Implement marketplace authentication login integration
This commit is contained in:
Tiago Yamamoto 2025-12-21 23:40:47 -03:00 committed by GitHub
commit 55342c5375
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 118 additions and 43 deletions

View file

@ -150,7 +150,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@ -509,7 +508,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@ -553,7 +551,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@ -1544,7 +1541,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@ -1644,7 +1642,6 @@
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@ -1656,7 +1653,6 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@ -1824,6 +1820,7 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@ -1834,6 +1831,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@ -2028,7 +2026,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@ -2297,7 +2294,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/dunder-proto": {
"version": "1.0.1",
@ -2902,7 +2900,6 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@ -2919,7 +2916,6 @@
"integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@ -2984,8 +2980,7 @@
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause",
"peer": true
"license": "BSD-2-Clause"
},
"node_modules/lilconfig": {
"version": "3.1.3",
@ -3035,6 +3030,7 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@ -3340,7 +3336,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -3490,6 +3485,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@ -3541,7 +3537,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@ -3554,7 +3549,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@ -3568,7 +3562,8 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/react-leaflet": {
"version": "4.2.1",
@ -4046,7 +4041,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -4197,7 +4191,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@ -4814,7 +4807,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -4828,7 +4820,6 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",

View file

@ -1,8 +1,9 @@
import { createContext, ReactNode, useContext, useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { apiClient } from '../services/apiClient'
import { authService } from '../services/auth'
export type UserRole = 'farmacia' | 'admin'
export type UserRole = 'admin' | 'seller' | 'customer'
export interface AuthUser {
name: string
@ -49,6 +50,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
const logout = () => {
authService.logout().catch(() => undefined)
setUser(null)
navigate('/login', { replace: true })
}

View file

@ -1,14 +1,50 @@
import { FormEvent, useState } from 'react'
import axios from 'axios'
import { useAuth, UserRole } from '../context/AuthContext'
import { authService } from '../services/auth'
import { decodeJwtPayload } from '../utils/jwt'
export function LoginPage() {
const { login } = useAuth()
const [role, setRole] = useState<UserRole>('farmacia')
const [name, setName] = useState('Dra. Silva')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const onSubmit = (event: FormEvent) => {
const resolveRole = (role?: string): UserRole => {
switch (role?.toLowerCase()) {
case 'admin':
return 'admin'
case 'customer':
return 'customer'
case 'seller':
default:
return 'seller'
}
}
const onSubmit = async (event: FormEvent) => {
event.preventDefault()
login('fake-jwt-token', role, name)
setLoading(true)
setErrorMessage(null)
try {
const { token } = await authService.login({ username, password })
const payload = decodeJwtPayload<{ role?: string }>(token)
const role = resolveRole(payload?.role)
login(token, role, username)
} catch (error) {
const fallback = 'Não foi possível autenticar. Verifique suas credenciais.'
if (axios.isAxiosError(error)) {
setErrorMessage(error.response?.data?.error ?? fallback)
} else if (error instanceof Error) {
setErrorMessage(error.message)
} else {
setErrorMessage(fallback)
}
} finally {
setLoading(false)
}
}
return (
@ -18,34 +54,45 @@ export function LoginPage() {
className="w-full max-w-md space-y-4 rounded-lg bg-white p-8 shadow-lg"
>
<h1 className="text-2xl font-bold text-medicalBlue">Acesso ao Marketplace</h1>
<p className="text-sm text-gray-600">Use o nível de acesso para testar as rotas protegidas.</p>
<p className="text-sm text-gray-600">Informe suas credenciais para acessar o marketplace.</p>
{errorMessage && (
<div className="rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
{errorMessage}
</div>
)}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700" htmlFor="name">
Nome do usuário
<label className="text-sm font-medium text-gray-700" htmlFor="username">
Usuário
</label>
<input
id="name"
id="username"
name="username"
autoComplete="username"
className="w-full rounded border border-gray-200 px-3 py-2"
value={name}
onChange={(e) => setName(e.target.value)}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700" htmlFor="role">
Perfil
<label className="text-sm font-medium text-gray-700" htmlFor="password">
Senha
</label>
<select
id="role"
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
className="w-full rounded border border-gray-200 px-3 py-2"
value={role}
onChange={(e) => setRole(e.target.value as UserRole)}
>
<option value="farmacia">Farmácia</option>
<option value="admin">Admin</option>
</select>
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit" className="w-full rounded bg-healthGreen px-4 py-2 font-semibold text-white">
Entrar
<button
type="submit"
className="w-full rounded bg-healthGreen px-4 py-2 font-semibold text-white disabled:cursor-not-allowed disabled:opacity-70"
disabled={loading}
>
{loading ? 'Entrando...' : 'Entrar'}
</button>
</form>
</div>

View file

@ -0,0 +1,21 @@
import { apiClient } from './apiClient'
interface AuthResponse {
token: string
expires_at: string
}
export interface AuthLoginPayload {
username: string
password: string
}
export const authService = {
login: async (payload: AuthLoginPayload) => {
const { data } = await apiClient.post<AuthResponse>('/v1/auth/login', payload)
return { token: data.token, expiresAt: data.expires_at }
},
logout: async () => {
await apiClient.post('/v1/auth/logout')
}
}

View file

@ -0,0 +1,14 @@
const toBase64 = (value: string) =>
value.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(value.length / 4) * 4, '=')
export const decodeJwtPayload = <T extends Record<string, unknown>>(token: string): T | null => {
const [, payload] = token.split('.')
if (!payload) return null
try {
const decoded = atob(toBase64(payload))
return JSON.parse(decoded) as T
} catch {
return null
}
}