diff --git a/marketplace/package-lock.json b/marketplace/package-lock.json index 583698f..2ed97c7 100644 --- a/marketplace/package-lock.json +++ b/marketplace/package-lock.json @@ -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", diff --git a/marketplace/src/context/AuthContext.tsx b/marketplace/src/context/AuthContext.tsx index b915012..823eb76 100644 --- a/marketplace/src/context/AuthContext.tsx +++ b/marketplace/src/context/AuthContext.tsx @@ -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 }) } diff --git a/marketplace/src/pages/Login.tsx b/marketplace/src/pages/Login.tsx index 5f9899b..7de83c9 100644 --- a/marketplace/src/pages/Login.tsx +++ b/marketplace/src/pages/Login.tsx @@ -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('farmacia') - const [name, setName] = useState('Dra. Silva') + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [errorMessage, setErrorMessage] = useState(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" >

Acesso ao Marketplace

-

Use o nível de acesso para testar as rotas protegidas.

+

Informe suas credenciais para acessar o marketplace.

+ {errorMessage && ( +
+ {errorMessage} +
+ )}
-
-
- diff --git a/marketplace/src/services/auth.ts b/marketplace/src/services/auth.ts new file mode 100644 index 0000000..8e55593 --- /dev/null +++ b/marketplace/src/services/auth.ts @@ -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('/v1/auth/login', payload) + return { token: data.token, expiresAt: data.expires_at } + }, + logout: async () => { + await apiClient.post('/v1/auth/logout') + } +} diff --git a/marketplace/src/utils/jwt.ts b/marketplace/src/utils/jwt.ts new file mode 100644 index 0000000..a09226c --- /dev/null +++ b/marketplace/src/utils/jwt.ts @@ -0,0 +1,14 @@ +const toBase64 = (value: string) => + value.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(value.length / 4) * 4, '=') + +export const decodeJwtPayload = >(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 + } +}