diff --git a/package-lock.json b/package-lock.json index 3cb88d9..94e1dbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "gaap-web", "version": "0.1.0", "dependencies": { + "@bufbuild/protobuf": "^2.10.2", "@marsidev/react-turnstile": "^1.4.0", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", @@ -23,12 +24,15 @@ "@tanstack/react-query": "^5.90.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "decimal.js": "^10.6.0", "i18next": "^25.6.3", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", + "long": "^5.3.2", "lucide-react": "^0.554.0", "next": "^16.1.0", "next-themes": "^0.4.6", + "protobufjs": "^8.0.0", "qrcode.react": "^4.2.0", "react": "19.2.0", "react-dom": "19.2.0", @@ -44,6 +48,7 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", + "@types/decimal.js": "^0.0.32", "@types/node": "^20.19.25", "@types/react": "^19", "@types/react-dom": "^19", @@ -53,6 +58,7 @@ "jsdom": "^27.3.0", "nodemon": "^3.1.9", "tailwindcss": "^4", + "ts-proto": "^2.10.1", "tw-animate-css": "^1.4.0", "typescript": "^5", "vitest": "^4.0.16" @@ -432,6 +438,12 @@ "node": ">=6.9.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.2.tgz", + "integrity": "sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -1995,6 +2007,70 @@ "node": ">=12.4.0" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -4674,6 +4750,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/decimal.js": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/decimal.js/-/decimal.js-0.0.32.tgz", + "integrity": "sha512-qiZoeFWRa6SaedYkSV8VrGV8xDGV3C6usFlUKOOl/fpvVdKVx+eHm+yHjJbGIuHgNsoe24wUddwLJGVBZFM5Ow==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -4704,9 +4787,7 @@ "version": "20.19.25", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5924,6 +6005,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/case-anything": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz", + "integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/chai": { "version": "6.2.1", "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.1.tgz", @@ -6318,9 +6412,8 @@ }, "node_modules/decimal.js": { "version": "10.6.0", - "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, "license": "MIT" }, "node_modules/decimal.js-light": { @@ -6417,6 +6510,29 @@ "dev": true, "license": "MIT" }, + "node_modules/dprint-node": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.8.tgz", + "integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3" + } + }, + "node_modules/dprint-node/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -8770,6 +8886,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9484,6 +9606,30 @@ "react-is": "^16.13.1" } }, + "node_modules/protobufjs": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.0.tgz", + "integrity": "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -10730,6 +10876,42 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-poet": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.12.0.tgz", + "integrity": "sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dprint-node": "^1.0.8" + } + }, + "node_modules/ts-proto": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.10.1.tgz", + "integrity": "sha512-4sOE1hCs0uobJgdRCtcEwdbc8MAyKP+LJqUIKxZIiKac0rPBlVKsRGEGo2oQ1MnKA2Wwk0KuGP2POkiCwPtebw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bufbuild/protobuf": "^2.10.2", + "case-anything": "^2.1.13", + "ts-poet": "^6.12.0", + "ts-proto-descriptors": "2.1.0" + }, + "bin": { + "protoc-gen-ts_proto": "protoc-gen-ts_proto" + } + }, + "node_modules/ts-proto-descriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-2.1.0.tgz", + "integrity": "sha512-S5EZYEQ6L9KLFfjSRpZWDIXDV/W7tAj8uW7pLsihIxyr62EAVSiKuVPwE8iWnr849Bqa53enex1jhDUcpgquzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bufbuild/protobuf": "^2.0.0" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -10931,7 +11113,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { diff --git a/package.json b/package.json index 9e728a9..2e26c0b 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,11 @@ "build": "next build", "start": "next start", "lint": "eslint", - "test": "vitest" + "test": "vitest", + "proto": "node scripts/generate-proto.js" }, "dependencies": { + "@bufbuild/protobuf": "^2.10.2", "@marsidev/react-turnstile": "^1.4.0", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", @@ -27,12 +29,15 @@ "@tanstack/react-query": "^5.90.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "decimal.js": "^10.6.0", "i18next": "^25.6.3", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", + "long": "^5.3.2", "lucide-react": "^0.554.0", "next": "^16.1.0", "next-themes": "^0.4.6", + "protobufjs": "^8.0.0", "qrcode.react": "^4.2.0", "react": "19.2.0", "react-dom": "19.2.0", @@ -48,6 +53,7 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", + "@types/decimal.js": "^0.0.32", "@types/node": "^20.19.25", "@types/react": "^19", "@types/react-dom": "^19", @@ -57,6 +63,7 @@ "jsdom": "^27.3.0", "nodemon": "^3.1.9", "tailwindcss": "^4", + "ts-proto": "^2.10.1", "tw-animate-css": "^1.4.0", "typescript": "^5", "vitest": "^4.0.16" diff --git a/scripts/generate-proto.js b/scripts/generate-proto.js new file mode 100644 index 0000000..16122b8 --- /dev/null +++ b/scripts/generate-proto.js @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const projectRoot = path.resolve(__dirname, '..'); +const protoRoot = path.resolve(projectRoot, '../gaap-api/manifest/protobuf'); +const outDir = path.resolve(projectRoot, 'src/lib/proto'); +const pluginPath = path.resolve(projectRoot, 'node_modules/.bin/protoc-gen-ts_proto' + (process.platform === 'win32' ? '.cmd' : '')); + +function getFiles(dir, extension, files = []) { + if (!fs.existsSync(dir)) return files; + const list = fs.readdirSync(dir); + for (const file of list) { + const name = path.join(dir, file); + if (fs.statSync(name).isDirectory()) { + getFiles(name, extension, files); + } else if (name.endsWith(extension)) { + // Use relative path from protoRoot for protoc, ensuring forward slashes + const relativePath = path.relative(protoRoot, name).split(path.sep).join('/'); + files.push(relativePath); + } + } + return files; +} + +if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); +} + +console.log(`Searching for .proto files in ${protoRoot}...`); +const protoFiles = getFiles(protoRoot, '.proto'); + +if (protoFiles.length === 0) { + console.error('No .proto files found in', protoRoot); + process.exit(1); +} + +const command = [ + 'protoc', + `--plugin=protoc-gen-ts_proto="${pluginPath}"`, + `--ts_proto_out="${outDir}"`, + `--proto_path="${protoRoot}"`, + ...protoFiles.map(f => `"${f}"`), + '--ts_proto_opt=esModuleInterop=true,forceLong=string' +].join(' '); + +console.log(`Generating ${protoFiles.length} protos...`); +try { + execSync(command, { cwd: protoRoot, stdio: 'inherit' }); + console.log('Successfully generated protos.'); +} catch { + console.error('Error generating protos.'); + process.exit(1); +} diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx index 253fbad..c599f24 100644 --- a/src/app/register/page.tsx +++ b/src/app/register/page.tsx @@ -8,19 +8,22 @@ import { Label } from '@/components/ui/label'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; +import { useGlobal } from '@/context/GlobalContext'; -import apiRequest, { ApiError } from '@/lib/api'; import { sha256 } from '@/lib/utils'; +import { useRegister } from '@/lib/hooks'; + export default function RegisterPage() { const { t } = useTranslation(['auth', 'common', 'settings']); const router = useRouter(); + const { login: contextLogin } = useGlobal(); + const registerMutation = useRegister(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [nickname, setNickname] = useState(''); const [turnstileToken, setTurnstileToken] = useState(''); - const [loading, setLoading] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -32,35 +35,35 @@ export default function RegisterPage() { toast.error(t('auth:password_mismatch')); return; } - setLoading(true); try { const hashedPassword = await sha256(password); - await apiRequest('/api/auth/register', { - method: 'POST', - body: JSON.stringify({ - email, - password: hashedPassword, - nickname, - cf_turnstile_response: turnstileToken - }) + const data = await registerMutation.mutateAsync({ + email, + password: hashedPassword, + nickname, + cfTurnstileResponse: turnstileToken }); - toast.success(t('auth:register_success')); - router.push('/login'); - } catch (err: unknown) { - if (err instanceof ApiError) { - toast.error(err.message); - } else if (err instanceof Error) { - toast.error(err.message); - } else { - toast.error(t('auth:unknown_error')); + // Registration with auto-login successful - update global context with user data + if (data && data.auth && data.auth.user) { + contextLogin({ + email: data.auth.user.email, + nickname: data.auth.user.nickname, + avatar: data.auth.user.avatar, + plan: data.auth.user.plan + }); } - } finally { - setLoading(false); + + router.push('/dashboard'); + } catch { + // Error handled by hook + toast.error(t('auth:register_failed'), { duration: 4000 }); } }; + const loading = registerMutation.isPending; + return (
{t('accounts:initial_balance_info')}
+ )}