Merge pull request #25 from rede5/codex/implementar-no-marketplace
Implement marketplace authentication login integration
This commit is contained in:
commit
55342c5375
5 changed files with 118 additions and 43 deletions
31
marketplace/package-lock.json
generated
31
marketplace/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
21
marketplace/src/services/auth.ts
Normal file
21
marketplace/src/services/auth.ts
Normal 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')
|
||||
}
|
||||
}
|
||||
14
marketplace/src/utils/jwt.ts
Normal file
14
marketplace/src/utils/jwt.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue