Merge branch 'pl-old-version'
Pedro Lucas Porcellis porcellis@eletrotupi.com · 6 months ago
b13456d98a5b20510f062a4d4fb619887728bd8f Browse filesChanged files
20
Additions
+833
Deletions
-40
api/ava.config.mjs
1 1
export default {
2 -
"typescript": {
3 -
"extensions": [
4 -
"ts",
5 -
"tsx"
6 -
],
7 -
"rewritePaths": {
8 -
"src/": "dist/"
9 -
},
10 -
compile: false
2 +
extensions: {
3 +
ts: 'commonjs'
11 4
},
12 -
// extensions: {
13 -
// ts: 'module'
14 -
// },
15 -
// nodeArguments: [
16 -
// '--import=tsx'
17 -
// ],
5 +
nodeArguments: [
6 +
'--require=tsx/cjs',
7 +
'--require=tsconfig-paths/register'
8 +
],
18 9
files: [
19 10
'tests/**/*.test.ts'
20 11
],
21 12
environmentVariables: {
22 -
NODE_ENV: 'test'
13 +
NODE_ENV: 'test',
14 +
TS_NODE_PROJECT: './tsconfig.json'
23 15
},
24 16
timeout: '30s',
25 17
concurrency: 1, // Run tests serially to avoid database conflicts
26 18
verbose: true
27 -
};
19 +
};/dev/null → api/nodemon.json
1 +
{
2 +
"watch": ["src"],
3 +
"ext": "ts",
4 +
"exec": "node -r ts-node/register -r tsconfig-paths/register ./src/index.ts"
5 +
}
6 +
api/package-lock.json
1 1
{
2 2
"name": "orbit-api",
3 3
"version": "0.0.0",
4 4
"lockfileVersion": 3,
5 5
"requires": true,
14 14
"@prisma/client": "^7.5.0",
15 15
"@types/cors": "^2.8.19",
16 16
"@types/jsonwebtoken": "^9.0.10",
17 17
"bcryptjs": "^3.0.3",
18 18
"better-sqlite3": "^12.5.0",
19 +
"body-parser": "^2.2.2",
19 20
"cors": "^2.8.6",
20 21
"date-fns": "^4.1.0",
21 22
"express": "^5.2.1",
22 23
"jsonwebtoken": "^9.0.3",
23 24
"morgan": "^1.10.1",
30 31
"@types/morgan": "^1.9.10",
31 32
"@types/node": "^25.5.2",
32 33
"ava": "^6.4.1",
33 34
"cross-var": "^1.1.0",
34 35
"dotenv-cli": "^11.0.0",
36 +
"nodemon": "^3.1.14",
35 37
"prisma": "^7.5.0",
36 38
"supertest": "^7.1.4",
37 39
"ts-node": "^10.9.2",
38 40
"tsc-alias": "^1.8.16",
39 41
"tsconfig-paths": "^4.2.0",
2691 2693
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==",
2692 2694
"dev": true,
2693 2695
"license": "MIT"
2694 2696
},
2695 2697
"node_modules/body-parser": {
2696 -
"version": "2.2.1",
2697 -
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
2698 -
"integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
2698 +
"version": "2.2.2",
2699 +
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
2700 +
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
2699 2701
"license": "MIT",
2700 2702
"dependencies": {
2701 2703
"bytes": "^3.1.2",
2702 2704
"content-type": "^1.0.5",
2703 2705
"debug": "^4.4.3",
2704 2706
"http-errors": "^2.0.0",
2705 2707
"iconv-lite": "^0.7.0",
2706 2708
"on-finished": "^2.4.1",
2707 -
"qs": "^6.14.0",
2709 +
"qs": "^6.14.1",
2708 2710
"raw-body": "^3.0.1",
2709 2711
"type-is": "^2.0.1"
2710 2712
},
2711 2713
"engines": {
2712 2714
"node": ">=18"
4513 4515
"license": "MIT",
4514 4516
"engines": {
4515 4517
"node": ">=0.10.0"
4516 4518
}
4517 4519
},
4520 +
"node_modules/has-flag": {
4521 +
"version": "3.0.0",
4522 +
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
4523 +
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
4524 +
"dev": true,
4525 +
"license": "MIT",
4526 +
"engines": {
4527 +
"node": ">=4"
4528 +
}
4529 +
},
4518 4530
"node_modules/has-symbols": {
4519 4531
"version": "1.1.0",
4520 4532
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
4521 4533
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
4522 4534
"license": "MIT",
5550 5562
"node-gyp-build": "bin.js",
5551 5563
"node-gyp-build-optional": "optional.js",
5552 5564
"node-gyp-build-test": "build-test.js"
5553 5565
}
5554 5566
},
5567 +
"node_modules/nodemon": {
5568 +
"version": "3.1.14",
5569 +
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
5570 +
"integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
5571 +
"dev": true,
5572 +
"license": "MIT",
5573 +
"dependencies": {
5574 +
"chokidar": "^3.5.2",
5575 +
"debug": "^4",
5576 +
"ignore-by-default": "^1.0.1",
5577 +
"minimatch": "^10.2.1",
5578 +
"pstree.remy": "^1.1.8",
5579 +
"semver": "^7.5.3",
5580 +
"simple-update-notifier": "^2.0.0",
5581 +
"supports-color": "^5.5.0",
5582 +
"touch": "^3.1.0",
5583 +
"undefsafe": "^2.0.5"
5584 +
},
5585 +
"bin": {
5586 +
"nodemon": "bin/nodemon.js"
5587 +
},
5588 +
"engines": {
5589 +
"node": ">=10"
5590 +
},
5591 +
"funding": {
5592 +
"type": "opencollective",
5593 +
"url": "https://opencollective.com/nodemon"
5594 +
}
5595 +
},
5596 +
"node_modules/nodemon/node_modules/balanced-match": {
5597 +
"version": "4.0.4",
5598 +
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
5599 +
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
5600 +
"dev": true,
5601 +
"license": "MIT",
5602 +
"engines": {
5603 +
"node": "18 || 20 || >=22"
5604 +
}
5605 +
},
5606 +
"node_modules/nodemon/node_modules/brace-expansion": {
5607 +
"version": "5.0.5",
5608 +
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
5609 +
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
5610 +
"dev": true,
5611 +
"license": "MIT",
5612 +
"dependencies": {
5613 +
"balanced-match": "^4.0.2"
5614 +
},
5615 +
"engines": {
5616 +
"node": "18 || 20 || >=22"
5617 +
}
5618 +
},
5619 +
"node_modules/nodemon/node_modules/chokidar": {
5620 +
"version": "3.6.0",
5621 +
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
5622 +
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
5623 +
"dev": true,
5624 +
"license": "MIT",
5625 +
"dependencies": {
5626 +
"anymatch": "~3.1.2",
5627 +
"braces": "~3.0.2",
5628 +
"glob-parent": "~5.1.2",
5629 +
"is-binary-path": "~2.1.0",
5630 +
"is-glob": "~4.0.1",
5631 +
"normalize-path": "~3.0.0",
5632 +
"readdirp": "~3.6.0"
5633 +
},
5634 +
"engines": {
5635 +
"node": ">= 8.10.0"
5636 +
},
5637 +
"funding": {
5638 +
"url": "https://paulmillr.com/funding/"
5639 +
},
5640 +
"optionalDependencies": {
5641 +
"fsevents": "~2.3.2"
5642 +
}
5643 +
},
5644 +
"node_modules/nodemon/node_modules/ignore-by-default": {
5645 +
"version": "1.0.1",
5646 +
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
5647 +
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
5648 +
"dev": true,
5649 +
"license": "ISC"
5650 +
},
5651 +
"node_modules/nodemon/node_modules/minimatch": {
5652 +
"version": "10.2.5",
5653 +
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
5654 +
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
5655 +
"dev": true,
5656 +
"license": "BlueOak-1.0.0",
5657 +
"dependencies": {
5658 +
"brace-expansion": "^5.0.5"
5659 +
},
5660 +
"engines": {
5661 +
"node": "18 || 20 || >=22"
5662 +
},
5663 +
"funding": {
5664 +
"url": "https://github.com/sponsors/isaacs"
5665 +
}
5666 +
},
5667 +
"node_modules/nodemon/node_modules/picomatch": {
5668 +
"version": "2.3.2",
5669 +
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
5670 +
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
5671 +
"dev": true,
5672 +
"license": "MIT",
5673 +
"engines": {
5674 +
"node": ">=8.6"
5675 +
},
5676 +
"funding": {
5677 +
"url": "https://github.com/sponsors/jonschlinkert"
5678 +
}
5679 +
},
5680 +
"node_modules/nodemon/node_modules/readdirp": {
5681 +
"version": "3.6.0",
5682 +
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
5683 +
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
5684 +
"dev": true,
5685 +
"license": "MIT",
5686 +
"dependencies": {
5687 +
"picomatch": "^2.2.1"
5688 +
},
5689 +
"engines": {
5690 +
"node": ">=8.10.0"
5691 +
}
5692 +
},
5693 +
"node_modules/nodemon/node_modules/supports-color": {
5694 +
"version": "5.5.0",
5695 +
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
5696 +
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
5697 +
"dev": true,
5698 +
"license": "MIT",
5699 +
"dependencies": {
5700 +
"has-flag": "^3.0.0"
5701 +
},
5702 +
"engines": {
5703 +
"node": ">=4"
5704 +
}
5705 +
},
5555 5706
"node_modules/nofilter": {
5556 5707
"version": "3.1.0",
5557 5708
"resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz",
5558 5709
"integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==",
5559 5710
"dev": true,
6182 6333
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
6183 6334
"integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
6184 6335
"dev": true,
6185 6336
"license": "ISC"
6186 6337
},
6338 +
"node_modules/pstree.remy": {
6339 +
"version": "1.1.8",
6340 +
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
6341 +
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
6342 +
"dev": true,
6343 +
"license": "MIT"
6344 +
},
6187 6345
"node_modules/pump": {
6188 6346
"version": "3.0.3",
6189 6347
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
6190 6348
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
6191 6349
"license": "MIT",
6210 6368
}
6211 6369
],
6212 6370
"license": "MIT"
6213 6371
},
6214 6372
"node_modules/qs": {
6215 -
"version": "6.14.0",
6216 -
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
6217 -
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
6373 +
"version": "6.15.1",
6374 +
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
6375 +
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
6218 6376
"license": "BSD-3-Clause",
6219 6377
"dependencies": {
6220 6378
"side-channel": "^1.1.0"
6221 6379
},
6222 6380
"engines": {
6823 6981
"decompress-response": "^6.0.0",
6824 6982
"once": "^1.3.1",
6825 6983
"simple-concat": "^1.0.0"
6826 6984
}
6827 6985
},
6986 +
"node_modules/simple-update-notifier": {
6987 +
"version": "2.0.0",
6988 +
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
6989 +
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
6990 +
"dev": true,
6991 +
"license": "MIT",
6992 +
"dependencies": {
6993 +
"semver": "^7.5.3"
6994 +
},
6995 +
"engines": {
6996 +
"node": ">=10"
6997 +
}
6998 +
},
6828 6999
"node_modules/slash": {
6829 7000
"version": "5.1.0",
6830 7001
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
6831 7002
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
6832 7003
"dev": true,
7271 7442
"license": "MIT",
7272 7443
"engines": {
7273 7444
"node": ">=0.6"
7274 7445
}
7275 7446
},
7447 +
"node_modules/touch": {
7448 +
"version": "3.1.1",
7449 +
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
7450 +
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
7451 +
"dev": true,
7452 +
"license": "ISC",
7453 +
"bin": {
7454 +
"nodetouch": "bin/nodetouch.js"
7455 +
}
7456 +
},
7276 7457
"node_modules/tr46": {
7277 7458
"version": "0.0.3",
7278 7459
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
7279 7460
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
7280 7461
"dev": true,
7533 7714
"tsserver": "bin/tsserver"
7534 7715
},
7535 7716
"engines": {
7536 7717
"node": ">=14.17"
7537 7718
}
7719 +
},
7720 +
"node_modules/undefsafe": {
7721 +
"version": "2.0.5",
7722 +
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
7723 +
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
7724 +
"dev": true,
7725 +
"license": "MIT"
7538 7726
},
7539 7727
"node_modules/undici-types": {
7540 7728
"version": "7.18.2",
7541 7729
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
7542 7730
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
7945 8133
"graphmatch": "^1.1.0"
7946 8134
}
7947 8135
}
7948 8136
}
7949 8137
}
api/package.json
1 1
{
2 2
"name": "orbit-api",
3 3
"version": "0.0.0",
4 4
"description": "",
5 5
"license": "ISC",
8 8
"directories": {
9 9
"test": "tests"
10 10
},
11 11
"scripts": {
12 12
"build": "tsc && tsc-alias",
13 -
"start": "ts-node -r tsconfig-paths/register src/index.ts",
13 +
"start": "nodemon src/index.ts",
14 14
"test": "npm run test:setup && dotenv -e .env.test -o -- npx ava",
15 15
"test:setup": "npm run test:setup:generate && npm run test:setup:migrate",
16 16
"test:setup:generate": "dotenv -e .env.test -o -- npx prisma generate",
17 17
"test:setup:migrate": "dotenv -e .env.test -o -- npx prisma db push",
18 18
"test:watch": "npm run test:setup && NODE_ENV=test npx ava --watch",
24 24
"@prisma/client": "^7.5.0",
25 25
"@types/cors": "^2.8.19",
26 26
"@types/jsonwebtoken": "^9.0.10",
27 27
"bcryptjs": "^3.0.3",
28 28
"better-sqlite3": "^12.5.0",
29 +
"body-parser": "^2.2.2",
29 30
"cors": "^2.8.6",
30 31
"date-fns": "^4.1.0",
31 32
"express": "^5.2.1",
32 33
"jsonwebtoken": "^9.0.3",
33 34
"morgan": "^1.10.1",
40 41
"@types/morgan": "^1.9.10",
41 42
"@types/node": "^25.5.2",
42 43
"ava": "^6.4.1",
43 44
"cross-var": "^1.1.0",
44 45
"dotenv-cli": "^11.0.0",
46 +
"nodemon": "^3.1.14",
45 47
"prisma": "^7.5.0",
46 48
"supertest": "^7.1.4",
47 49
"ts-node": "^10.9.2",
48 50
"tsc-alias": "^1.8.16",
49 51
"tsconfig-paths": "^4.2.0",
50 52
"tsx": "^4.21.0",
51 53
"typescript": "^6.0.2"
52 54
}
53 55
}
api/src/controllers/users.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import {
3 3
createUser,
4 +
findUserById,
5 +
findUserByEmail,
6 +
updateUser,
4 7
generateToken
5 8
} from '@app/models/user';
6 9
7 10
import {
8 11
validateRequiredFields
9 12
} from '@app/utils/validators'
10 13
11 14
export const UsersController = {
12 15
create: async (req: Request, res: Response, next: NextFunction) => {
13 16
try {
14 -
const { email, password, first_name: firstName, last_name: lastName } = req.body;
17 +
const { email, password, firstName, lastName } = req.body;
15 18
16 19
// Validate required fields
17 20
const requiredValidation = validateRequiredFields(req.body, [
18 -
'email', 'password', 'first_name'
21 +
'email', 'password', 'firstName'
19 22
]);
20 23
21 24
if (!requiredValidation.valid) {
22 25
return res.status(400).json({
23 26
error: "Campos faltantes",
30 33
);
31 34
32 35
const jwtToken = generateToken(user.id, user.email);
33 36
34 37
res.status(201).json({ token: jwtToken });
38 +
} catch (err) {
39 +
next(err);
40 +
}
41 +
},
42 +
43 +
update: async (req: Request, res: Response, next: NextFunction) => {
44 +
try {
45 +
const { id } = req.params
46 +
const { email, password, firstName, lastName } = req.body;
47 +
48 +
const user = await findUserById(Number(id));
49 +
50 +
if (!user) {
51 +
return res.status(404).json({
52 +
error: "Usuário não encontrado"
53 +
})
54 +
}
55 +
56 +
if (email || password || firstName || lastName) {
57 +
const response = await updateUser(user, {
58 +
email, firstName, lastName
59 +
})
60 +
}
61 +
62 +
const updated = await findUserById(Number(id))
63 +
64 +
// TODO: Drop encrypted password & token here
65 +
return res.status(200).json({
66 +
user: updated
67 +
})
35 68
} catch (err) {
36 69
next(err);
37 70
}
38 71
}
39 72
};
api/src/create-app.ts → api/src/createApp.ts
1 1
import express from 'express';
2 2
import cors from 'cors';
3 +
import bodyParser from 'body-parser';
3 4
import morgan from 'morgan';
4 5
import userRouter from '@app/routes/users';
5 6
import authRouter from '@app/routes/auth';
6 7
import { errorHandler } from '@app/middleware/errorHandler';
7 8
import { PrismaClient } from '@prisma/client';
14 15
origin: process.env.FRONTEND_URL || '*',
15 16
credentials: true
16 17
}));
17 18
18 19
app.use(express.json());
20 +
app.use(bodyParser.json());
19 21
20 22
// Only use morgan in non-test environments
21 23
if (process.env.NODE_ENV !== 'test') {
22 24
app.use(morgan('combined'));
23 25
}
31 33
app.use("/auth", authRouter);
32 34
33 35
app.use(errorHandler); // always last
34 36
35 37
return app;
36 -
} 38 +
}
api/src/index.ts
1 -
import { createApp } from '@app/create-app';
1 +
import { createApp } from '@app/createApp';
2 2
3 3
const port = process.env.PORT || 3000;
4 4
5 5
const app = createApp();
6 6
7 7
app.listen(port, () => {
8 8
console.log(`Server booted on port ${port}`)
9 9
})
10 10
api/src/middleware/auth.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import { verifyToken } from '@app/lib/jwt';
3 3
4 4
export interface AuthenticatedRequest extends Request {
5 5
userId?: number;
28 28
if (err.name === 'TokenExpiredError') {
29 29
return res.status(401).json({ error: 'Unauthorized: Token expired' });
30 30
}
31 31
next(err);
32 32
}
33 -
}; 33 +
};
api/src/middleware/errorHandler.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import { Prisma } from '@prisma/client' // Bleh
3 3
import { isDomainError } from "@app/lib/errors/base";
4 4
5 5
export const errorHandler = (
6 6
err: unknown,
7 7
_req: Request,
8 8
res: Response,
9 9
_next: NextFunction
10 10
) => {
11 -
// TODO: Rig some improved, centralized error logging here
12 -
console.error(`[ErrorHandler]: ${err}`);
11 +
if (process.env.NODE_ENV === "development") {
12 +
// TODO: Rig some improved, centralized error logging here
13 +
console.error(`[ErrorHandler]: ${err}`);
14 +
}
13 15
14 16
if (isDomainError(err)) {
15 17
return res.status(err.status)
16 18
.json({ error: err.message });
17 19
}
25 27
.json({ error: "Unique constraint violation" });
26 28
}
27 29
28 30
res.status(500).json({ error: "Internal server error" });
29 31
}
api/src/models/user.ts
1 1
import bcrypt from "bcryptjs";
2 2
import { prisma } from '../lib/prisma'
3 3
import {
4 4
generateToken as createJWT,
5 5
generatePasswordResetToken,
14 14
validateRequiredFields
15 15
} from "@app/utils/validators";
16 16
17 17
import { InvalidEmailError, ShortPasswordError } from '@app/lib/errors/UserErrors'
18 18
19 +
type UserOptions = {
20 +
email: string;
21 +
firstName: string;
22 +
lastName: string;
23 +
};
24 +
19 25
const createUser = async (email: string, firstName: string, lastName: string, password: string) => {
20 26
// Validate email format
21 27
if (!isValidEmail(email)) {
22 28
throw new InvalidEmailError()
23 29
}
24 30
25 31
// Validate password strength
26 32
if (!isValidPassword(password)) {
27 33
throw new ShortPasswordError()
28 34
}
29 -
35 +
30 36
const encryptedPassword = bcrypt.hashSync(password, 10);
31 37
32 38
try {
33 39
const user = await prisma.user.create({
34 40
data: {
39 45
}
40 46
});
41 47
42 48
return user;
43 49
} catch (err: any) {
44 -
console.log(err)
45 -
throw new Error("Something was off when creating user")
50 +
throw err;
46 51
}
47 52
}
48 53
49 54
const generateToken = (userId: number, email: string) => {
50 55
return createJWT(userId, email);
51 56
}
57 +
58 +
const findUserById = async (id: number) => {
59 +
return await prisma.user.findUnique({
60 +
where: { id }
61 +
});
62 +
};
52 63
53 64
const findUserByEmail = async (email: string) => {
54 65
return await prisma.user.findUnique({
55 66
where: { email }
56 67
});
132 143
});
133 144
134 145
return { success: true };
135 146
};
136 147
148 +
const updateUser = async (user: any, data: UserOptions) => {
149 +
return await prisma.user.update({
150 +
where: { id: user.id },
151 +
data
152 +
});
153 +
}
154 +
137 155
export {
138 156
createUser,
157 +
findUserById,
139 158
generateToken,
140 159
findUserByEmail,
141 160
authenticateUser,
142 161
initiatePasswordReset,
143 -
resetPassword
162 +
resetPassword,
163 +
updateUser
144 164
}
api/src/routes/users.ts
1 1
import { Router } from "express";
2 2
import { UsersController } from '@app/controllers/users'
3 3
4 4
const router = Router();
5 5
6 6
router.post('/', UsersController.create);
7 +
router.put('/:id', UsersController.update);
7 8
8 9
export default router;
api/tests/database-auth.test.ts
1 1
import test from "ava";
2 2
import { PrismaClient } from '@prisma/client';
3 3
import { PrismaPg } from '@prisma/adapter-pg';
4 4
import bcrypt from 'bcryptjs';
5 5
import 'dotenv/config';
22 22
console.error('Error cleaning test DB:', err);
23 23
}
24 24
});
25 25
26 26
test.after.always(async () => {
27 +
const deleteUsers = prisma.user.deleteMany();
28 +
29 +
await prisma.$transaction([
30 +
deleteUsers
31 +
])
32 +
27 33
await prisma?.$disconnect();
28 34
});
29 35
30 36
// Basic database connectivity test
31 37
test("database connection works", async (t) => {
250 256
t.deepEqual(updatedUser.createdAt, user.createdAt);
251 257
t.true(updatedUser.updatedAt >= beforeUpdate);
252 258
t.true(updatedUser.updatedAt <= afterUpdate);
253 259
t.true(updatedUser.updatedAt > user.updatedAt);
254 260
});
/dev/null → api/tests/users/create.test.ts
1 +
import test from "ava";
2 +
import { PrismaClient } from '@prisma/client';
3 +
import { PrismaPg } from '@prisma/adapter-pg';
4 +
import request from 'supertest';
5 +
import jwt from 'jsonwebtoken';
6 +
import 'dotenv/config';
7 +
import { Express } from 'express';
8 +
import { createApp } from '../../src/createApp';
9 +
10 +
let prisma: PrismaClient;
11 +
let app: Express;
12 +
13 +
test.before(() => {
14 +
process.env.NODE_ENV = 'test';
15 +
process.env.JWT_SECRET = 'test-secret-key';
16 +
17 +
const connectionString = process.env.DATABASE_URL || 'postgres://user:password@orbit_db:5432/orbit_test';
18 +
const adapter = new PrismaPg({ connectionString });
19 +
prisma = new PrismaClient({ adapter });
20 +
21 +
// Create Express app with test Prisma client
22 +
app = createApp(prisma);
23 +
});
24 +
25 +
test.beforeEach(async () => {
26 +
// Clean database before each test
27 +
try {
28 +
await prisma.user.deleteMany({});
29 +
} catch (err) {
30 +
console.error('Error cleaning test DB:', err);
31 +
}
32 +
});
33 +
34 +
test.after.always(async () => {
35 +
await prisma?.$disconnect();
36 +
});
37 +
38 +
test("POST /users creates a new user and returns JWT token", async (t) => {
39 +
const userData = {
40 +
email: "falamansa@example.com",
41 +
firstName: "Fala",
42 +
lastName: "Mansa",
43 +
password: "IscreviSeuNomeNaAreia"
44 +
};
45 +
46 +
const response = await request(app)
47 +
.post('/users')
48 +
.send(userData)
49 +
.expect('Content-Type', /json/)
50 +
.expect(201);
51 +
52 +
// Should return a JWT token
53 +
t.truthy(response.body.token);
54 +
55 +
// Verify the token is valid
56 +
const decoded = jwt.verify(response.body.token, process.env.JWT_SECRET!) as any;
57 +
t.is(decoded.email, userData.email);
58 +
t.truthy(decoded.userId);
59 +
60 +
// Verify user was created in database
61 +
const dbUser = await prisma.user.findUnique({
62 +
where: { email: userData.email }
63 +
});
64 +
t.truthy(dbUser);
65 +
t.is(dbUser!.email, userData.email);
66 +
t.is(dbUser!.firstName, userData.firstName);
67 +
t.is(dbUser!.lastName, userData.lastName);
68 +
});
69 +
70 +
test("POST /users returns 400 for missing required fields", async (t) => {
71 +
const response = await request(app)
72 +
.post('/users')
73 +
.send({
74 +
email: "invalid@example.com"
75 +
// Missing firstName and password
76 +
})
77 +
.expect('Content-Type', /json/)
78 +
.expect(400);
79 +
80 +
t.truthy(response.body.error);
81 +
t.is(response.body.error, "Campos faltantes"); // TODO: i18n?
82 +
t.truthy(response.body.missing);
83 +
t.true(response.body.missing.includes('password'));
84 +
t.true(response.body.missing.includes('firstName'));
85 +
});
86 +
87 +
test("POST /users returns 409 for duplicate email", async (t) => {
88 +
const email = "duplicate@example.com";
89 +
90 +
// Create first user
91 +
await prisma.user.create({
92 +
data: {
93 +
email,
94 +
firstName: "First",
95 +
lastName: "User",
96 +
encryptedPassword: "$2a$10$test"
97 +
}
98 +
});
99 +
100 +
// Try to create second user with same email
101 +
const response = await request(app)
102 +
.post('/users')
103 +
.send({
104 +
email,
105 +
firstName: "Second",
106 +
lastName: "User",
107 +
password: "hunter2"
108 +
})
109 +
.expect('Content-Type', /json/)
110 +
.expect(409);
111 +
112 +
t.truthy(response.body.error);
113 +
});
114 +
115 +
test("createApp successfully integrates with Prisma and Express", async (t) => {
116 +
// Test that the app factory works correctly
117 +
const testApp = createApp(prisma);
118 +
119 +
t.truthy(testApp);
120 +
t.is(typeof testApp.use, 'function');
121 +
t.is(typeof testApp.listen, 'function');
122 +
123 +
// Verify the app has the prisma client in locals
124 +
t.is(testApp.locals.prisma, prisma);
125 +
});
/dev/null → api/tests/users/delete.test.ts
1 +
import test from "ava";
2 +
import { PrismaClient } from '@prisma/client';
3 +
import { PrismaPg } from '@prisma/adapter-pg';
4 +
import request from 'supertest';
5 +
import bcrypt from 'bcryptjs';
6 +
import 'dotenv/config';
7 +
import { Express } from 'express';
8 +
import { createApp } from '../../src/createApp';
9 +
10 +
let prisma: PrismaClient;
11 +
let app: Express;
12 +
13 +
test.before(() => {
14 +
process.env.NODE_ENV = 'test';
15 +
16 +
const connectionString = process.env.DATABASE_URL || 'postgres://user:password@orbit_db:5432/orbit_test';
17 +
const adapter = new PrismaPg({ connectionString });
18 +
prisma = new PrismaClient({ adapter });
19 +
20 +
// Create Express app with test Prisma client
21 +
app = createApp(prisma);
22 +
});
23 +
24 +
test.beforeEach(async () => {
25 +
// Clean database before each test
26 +
try {
27 +
await prisma.user.deleteMany({});
28 +
} catch (err) {
29 +
console.error('Error cleaning test DB:', err);
30 +
}
31 +
});
32 +
33 +
test.after.always(async () => {
34 +
await prisma?.$disconnect();
35 +
});
36 +
37 +
//test("DELETE /users/:id deletes a user", async (t) => {
38 +
// t.pass()
39 +
// // Create a user first
40 +
// const user = await prisma.user.create({
41 +
// data: {
42 +
// email: "delete@example.com",
43 +
// firstName: "Delete",
44 +
// lastName: "Me",
45 +
// encryptedPassword: bcrypt.hashSync("password", 10)
46 +
// }
47 +
// });
48 +
//
49 +
// await request(app)
50 +
// .delete(`/users/${user.id}`)
51 +
// .expect(204);
52 +
//
53 +
// // Verify user was deleted
54 +
// const dbUser = await prisma.user.findUnique({
55 +
// where: { id: user.id }
56 +
// });
57 +
// t.is(dbUser, null);
58 +
//});
/dev/null → api/tests/users/fetch.test.ts
1 +
import test from "ava";
2 +
import { PrismaClient } from '@prisma/client';
3 +
import { PrismaPg } from '@prisma/adapter-pg';
4 +
import request from 'supertest';
5 +
import bcrypt from 'bcryptjs';
6 +
import 'dotenv/config';
7 +
import { Express } from 'express';
8 +
import { createApp } from '../../src/createApp';
9 +
10 +
let prisma: PrismaClient;
11 +
let app: Express;
12 +
13 +
test.before(() => {
14 +
process.env.NODE_ENV = 'test';
15 +
16 +
const connectionString = process.env.DATABASE_URL || 'postgres://user:password@orbit_db:5432/orbit_test';
17 +
const adapter = new PrismaPg({ connectionString });
18 +
prisma = new PrismaClient({ adapter });
19 +
20 +
// Create Express app with test Prisma client
21 +
app = createApp(prisma);
22 +
});
23 +
24 +
test.beforeEach(async () => {
25 +
// Clean database before each test
26 +
try {
27 +
await prisma.user.deleteMany({});
28 +
} catch (err) {
29 +
console.error('Error cleaning test DB:', err);
30 +
}
31 +
});
32 +
33 +
test.after.always(async () => {
34 +
await prisma?.$disconnect();
35 +
});
36 +
37 +
//test("GET /users/:id retrieves a user", async (t) => {
38 +
// t.pass()
39 +
// // Create a user first
40 +
// const user = await prisma.user.create({
41 +
// data: {
42 +
// email: "get@example.com",
43 +
// firstName: "Get",
44 +
// lastName: "User",
45 +
// encryptedPassword: bcrypt.hashSync("password", 10)
46 +
// }
47 +
// });
48 +
//
49 +
// const response = await request(app)
50 +
// .get(`/users/${user.id}`)
51 +
// .expect('Content-Type', /json/)
52 +
// .expect(200);
53 +
//
54 +
// t.is(response.body.id, user.id);
55 +
// t.is(response.body.email, user.email);
56 +
// t.is(response.body.firstName, user.firstName);
57 +
// t.is(response.body.lastName, user.lastName);
58 +
// t.falsy(response.body.encryptedPassword); // Should not expose password
59 +
//});
60 +
//
61 +
//test("GET /users/:id returns 404 for non-existent user", async (t) => {
62 +
// const response = await request(app)
63 +
// .get('/users/999999')
64 +
// .expect('Content-Type', /json/)
65 +
// .expect(404);
66 +
//
67 +
// t.truthy(response.body.error);
68 +
//});
/dev/null → api/tests/users/update.test.ts
1 +
import test from "ava";
2 +
import { PrismaClient } from '@prisma/client';
3 +
import { PrismaPg } from '@prisma/adapter-pg';
4 +
import request from 'supertest';
5 +
import bcrypt from 'bcryptjs';
6 +
import 'dotenv/config';
7 +
import { Express } from 'express';
8 +
import { createApp } from '../../src/createApp';
9 +
10 +
let prisma: PrismaClient;
11 +
let app: Express;
12 +
13 +
test.before(() => {
14 +
process.env.NODE_ENV = 'test';
15 +
16 +
const connectionString = process.env.DATABASE_URL || 'postgres://user:password@orbit_db:5432/orbit_test';
17 +
const adapter = new PrismaPg({ connectionString });
18 +
prisma = new PrismaClient({ adapter });
19 +
20 +
// Create Express app with test Prisma client
21 +
app = createApp(prisma);
22 +
});
23 +
24 +
test.beforeEach(async () => {
25 +
// Clean database before each test
26 +
try {
27 +
await prisma.user.deleteMany({});
28 +
} catch (err) {
29 +
console.error('Error cleaning test DB:', err);
30 +
}
31 +
});
32 +
33 +
test.after.always(async () => {
34 +
await prisma?.$disconnect();
35 +
});
36 +
37 +
test("PUT /users/:id updates user profile", async (t) => {
38 +
// Create a user first
39 +
const user = await prisma.user.create({
40 +
data: {
41 +
email: "update@example.com",
42 +
firstName: "Original",
43 +
lastName: "Name",
44 +
encryptedPassword: bcrypt.hashSync("password", 10)
45 +
}
46 +
});
47 +
48 +
const updateData = {
49 +
firstName: "Updated",
50 +
lastName: "Profile"
51 +
};
52 +
53 +
// TODO: Gotta check why its failing. Most likely due to the
54 +
// cors/credentials: true line
55 +
const response = await request(app)
56 +
.put(`/users/${user.id}`)
57 +
.send(updateData)
58 +
.expect('Content-Type', /json/)
59 +
.expect(200);
60 +
61 +
t.is(response.body.user.firstName, updateData.firstName);
62 +
t.is(response.body.user.lastName, updateData.lastName);
63 +
t.is(response.body.user.email, user.email); // Should remain unchanged
64 +
65 +
// Verify in database
66 +
const dbUser = await prisma.user.findUnique({
67 +
where: { id: user.id }
68 +
});
69 +
70 +
t.is(dbUser!.firstName, updateData.firstName);
71 +
t.is(dbUser!.lastName, updateData.lastName);
72 +
});
73 +
74 +
test("PUT /users/:id returns 404 for non-existent user", async (t) => {
75 +
const response = await request(app)
76 +
.put('/users/999999')
77 +
.send({
78 +
firstName: "Updated",
79 +
lastName: "Name"
80 +
})
81 +
.expect('Content-Type', /json/)
82 +
.expect(404);
83 +
84 +
t.truthy(response.body.error);
85 +
});
/dev/null → api/tsconfig.test.json
1 +
{
2 +
"extends": "./tsconfig.json",
3 +
"compilerOptions": {
4 +
"module": "ESNext",
5 +
"moduleResolution": "node",
6 +
"allowImportingTsExtensions": true,
7 +
"resolveJsonModule": true,
8 +
"noEmit": true
9 +
},
10 +
"ts-node": {
11 +
"esm": true,
12 +
"experimentalSpecifierResolution": "node",
13 +
"transpileOnly": true,
14 +
"require": ["tsconfig-paths/register"]
15 +
}
16 +
}frontend/app/(tabs)/settings.tsx
1 -
import { StyleSheet } from 'react-native';
1 +
import { TouchableOpacity, StyleSheet, FlatList, View } from 'react-native';
2 +
import { useState } from 'react';
2 3
import ParallaxScrollView from '@/components/misc/parallax-scroll-view';
4 +
import { Link } from 'expo-router'
3 5
4 6
import { ThemedText } from '@/components/misc/themed-text';
5 7
import { ThemedView } from '@/components/misc/themed-view';
6 8
import { IconSymbol } from '@/components/ui/icon-symbol';
7 9
import { Fonts } from '@/constants/theme';
10 +
import { useThemeColor } from '@/hooks/use-theme-color';
8 11
9 12
export default function Settings() {
13 +
const [options] = useState([
14 +
{ id: 'profile', name: 'Perfil', path: '/profile'}
15 +
])
16 +
17 +
const textColor = useThemeColor({}, 'text');
18 +
const tintColor = useThemeColor({}, 'tint');
19 +
20 +
const renderItem = ({item}) => (
21 +
<Link key={item.id} href={item.path} asChild style={[styles.item, { color: tintColor }]}>
22 +
<ThemedText>
23 +
{item.name}
24 +
</ThemedText>
25 +
</Link>
26 +
)
27 +
10 28
return (
11 29
<ParallaxScrollView
12 30
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
13 31
headerImage={
14 32
<IconSymbol
15 33
size={310}
16 -
color="#808080"
34 +
color="pink"
17 35
name="chevron.left.forwardslash.chevron.right"
18 36
style={styles.headerImage}
19 37
/>
20 38
}>
21 39
<ThemedView style={styles.titleContainer}>
22 40
<ThemedText
23 41
type="title"
24 42
style={{
25 43
fontFamily: Fonts.rounded,
26 44
}}>
27 -
Explore
45 +
Configurações
28 46
</ThemedText>
29 47
</ThemedView>
30 -
<ThemedText>
31 -
This app includes example code to help you get started.
32 -
</ThemedText>
48 +
49 +
{(options.map((opt) => (
50 +
renderItem({ item: opt })
51 +
)))}
33 52
</ParallaxScrollView>
34 53
)
35 54
}
36 55
37 56
39 58
headerImage: {
40 59
color: '#808080',
41 60
bottom: -90,
42 61
left: -35,
43 62
position: 'absolute',
63 +
},
64 +
item: {
65 +
paddingTop: 16,
66 +
paddingBottom: 16,
67 +
},
68 +
itemName: {
69 +
fontSize: 16,
70 +
fontWeight: 'bold',
71 +
},
72 +
separator: {
73 +
height: 1,
74 +
backgroundColor: '#ccc'
44 75
},
45 76
titleContainer: {
46 77
flexDirection: 'row',
47 78
gap: 8,
48 79
},
49 80
});
/dev/null → frontend/app/profile.tsx
1 +
import { useState, useEffect } from 'react';
2 +
import {
3 +
ThemedView
4 +
} from '@/components/ui/themed-view'
5 +
6 +
import {
7 +
ThemedText
8 +
} from '@/components/ui/themed-text'
9 +
10 +
import {
11 +
View,
12 +
Text,
13 +
StyleSheet,
14 +
ScrollView,
15 +
KeyboardAvoidingView,
16 +
Platform,
17 +
Alert,
18 +
} from 'react-native';
19 +
import { Link, useLocalSearchParams, router } from 'expo-router';
20 +
import { SafeAreaView } from 'react-native-safe-area-context';
21 +
import { useThemeColor } from '@/hooks/use-theme-color';
22 +
import { Button, Input } from '@/components/ui';
23 +
24 +
export default function Profile() {
25 +
const [firstName, setFirstName] = useState();
26 +
const [lastName, setLastName] = useState();
27 +
const [password, setPassword] = useState();
28 +
const [confirmPassword, setConfirmPassword] = useState();
29 +
const [email, setEmail] = useState();
30 +
const [loading, setLoading] = useState(false);
31 +
const [errors, setErrors] = useState<{
32 +
password?: string;
33 +
confirmPassword?: string;
34 +
}>({});
35 +
36 +
const backgroundColor = useThemeColor({}, 'background');
37 +
const textColor = useThemeColor({}, 'text');
38 +
39 +
const handleUpdate = () => {}
40 +
41 +
return (
42 +
<SafeAreaView style={[styles.container, { backgroundColor }]}>
43 +
<KeyboardAvoidingView
44 +
behavior='height'
45 +
style={styles.keyboardView}
46 +
>
47 +
<ScrollView
48 +
contentContainerStyle={styles.scrollContainer}
49 +
showsVerticalScrollIndicator={false}
50 +
>
51 +
<View style={styles.header}>
52 +
<Text style={[styles.title, { color: textColor }]}>Edite o seu perfil</Text>
53 +
<Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
54 +
Altere suas configurações
55 +
</Text>
56 +
</View>
57 +
58 +
<View style={styles.form}>
59 +
<Input
60 +
label="Primeiro Nome"
61 +
type="text"
62 +
value={firstName}
63 +
onChangeText={setFirstName}
64 +
placeholder="Qual seu nome?"
65 +
error={errors.firstName}
66 +
/>
67 +
68 +
<Input
69 +
label="Sobrenome"
70 +
type="text"
71 +
value={lastName}
72 +
onChangeText={setLastName}
73 +
placeholder="Qual seu sobrenome?"
74 +
error={errors.lastName}
75 +
/>
76 +
77 +
<Input
78 +
label="Nova senha"
79 +
type="password"
80 +
value={password}
81 +
onChangeText={setPassword}
82 +
placeholder="Digite uma nova senha"
83 +
error={errors.password}
84 +
showPasswordToggle
85 +
/>
86 +
87 +
<Input
88 +
label="Confirmar nova senha"
89 +
type="password"
90 +
value={confirmPassword}
91 +
onChangeText={setConfirmPassword}
92 +
placeholder="Confirme a nova senha"
93 +
error={errors.confirmPassword}
94 +
showPasswordToggle
95 +
/>
96 +
97 +
<Button
98 +
title="Trocar senha"
99 +
onPress={handleUpdate}
100 +
loading={loading}
101 +
style={styles.resetButton}
102 +
/>
103 +
</View>
104 +
105 +
</ScrollView>
106 +
</KeyboardAvoidingView>
107 +
</SafeAreaView>
108 +
)
109 +
}
110 +
111 +
const styles = StyleSheet.create({
112 +
container: {
113 +
flex: 1,
114 +
},
115 +
keyboardView: {
116 +
flex: 1,
117 +
},
118 +
scrollContainer: {
119 +
flexGrow: 1,
120 +
paddingHorizontal: 24,
121 +
justifyContent: 'start',
122 +
minHeight: '100%',
123 +
},
124 +
header: {
125 +
alignItems: 'left',
126 +
marginBottom: 40,
127 +
},
128 +
title: {
129 +
fontSize: 28,
130 +
fontWeight: 'bold',
131 +
marginBottom: 8,
132 +
},
133 +
subtitle: {
134 +
fontSize: 16,
135 +
textAlign: 'left',
136 +
},
137 +
form: {
138 +
marginBottom: 32,
139 +
},
140 +
resetButton: {
141 +
marginTop: 8,
142 +
},
143 +
link: {
144 +
textAlign: 'center',
145 +
fontSize: 14,
146 +
fontWeight: '500',
147 +
},
148 +
footer: {
149 +
flexDirection: 'row',
150 +
justifyContent: 'center',
151 +
alignItems: 'center',
152 +
marginTop: 'auto',
153 +
paddingBottom: 20,
154 +
},
155 +
footerText: {
156 +
fontSize: 14,
157 +
},
158 +
});
/dev/null → frontend/public/tamagui.generated.css
Binary file