Merge branch 'pl-old-version'

Pedro Lucas Porcellis porcellis@eletrotupi.com 2 months ago b13456d98a5b20510f062a4d4fb619887728bd8f
20 file(s) changed
  • api/ava.config.mjs +8 -17
  • api/nodemon.json +6 -0
  • api/package-lock.json +195 -7
  • api/package.json +3 -1
  • api/src/controllers/users.ts +35 -2
  • api/src/createApp.ts +3 -0
  • api/src/index.ts +1 -1
  • api/src/middleware/auth.ts +1 -0
  • api/src/middleware/errorHandler.ts +4 -2
  • api/src/models/user.ts +24 -4
  • api/src/routes/users.ts +1 -0
  • api/tests/database-auth.test.ts +6 -0
  • api/tests/users/create.test.ts +125 -0
  • api/tests/users/delete.test.ts +58 -0
  • api/tests/users/fetch.test.ts +68 -0
  • api/tests/users/update.test.ts +85 -0
  • api/tsconfig.test.json +15 -0
  • frontend/app/(tabs)/settings.tsx +37 -6
  • frontend/app/profile.tsx +158 -0
  • frontend/public/tamagui.generated.css +0 -0
api/ava.config.mjs
@@ -1,27 +1,19 @@
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 + };
api/nodemon.json
@@ -0,0 +1,6 @@
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
@@ -16,6 +16,7 @@ "@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",
@@ -32,6 +33,7 @@ "@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",
@@ -2693,9 +2695,9 @@ "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",
@@ -2704,7 +2706,7 @@ "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 },
@@ -4515,6 +4517,16 @@ "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",
@@ -5552,6 +5564,145 @@ "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",
@@ -6184,6 +6335,13 @@ "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",
@@ -6212,9 +6370,9 @@ ],
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"
@@ -6825,6 +6983,19 @@ "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",
@@ -7273,6 +7444,16 @@ "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",
@@ -7535,6 +7716,13 @@ },
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",
api/package.json
@@ -10,7 +10,7 @@ "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",
@@ -26,6 +26,7 @@ "@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",
@@ -42,6 +43,7 @@ "@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",
api/src/controllers/users.ts
@@ -1,6 +1,9 @@
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
@@ -11,11 +14,11 @@
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) {
@@ -32,6 +35,36 @@
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 }
api/src/createApp.ts
@@ -1,5 +1,6 @@
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';
@@ -16,6 +17,7 @@ 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') {
@@ -33,4 +35,4 @@
33 35 app.use(errorHandler); // always last
34 36
35 37 return app;
36 - }
38 + }
api/src/index.ts
@@ -1,4 +1,4 @@
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
api/src/middleware/auth.ts
@@ -30,4 +30,4 @@ return res.status(401).json({ error: 'Unauthorized: Token expired' });
30 30 }
31 31 next(err);
32 32 }
33 - };
33 + };
api/src/middleware/errorHandler.ts
@@ -8,8 +8,10 @@ _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)
api/src/models/user.ts
@@ -16,6 +16,12 @@ } 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)) {
@@ -26,7 +32,7 @@ // 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 {
@@ -41,14 +47,19 @@ });
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({
@@ -134,11 +145,20 @@
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
@@ -4,5 +4,6 @@
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
@@ -24,6 +24,12 @@ }
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
api/tests/users/create.test.ts
@@ -0,0 +1,125 @@
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 + });
api/tests/users/delete.test.ts
@@ -0,0 +1,58 @@
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 + //});
api/tests/users/fetch.test.ts
@@ -0,0 +1,68 @@
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 + //});
api/tests/users/update.test.ts
@@ -0,0 +1,85 @@
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 + });
api/tsconfig.test.json
@@ -0,0 +1,16 @@
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,19 +1,37 @@
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 />
@@ -24,12 +42,13 @@ 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 }
@@ -41,6 +60,18 @@ 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',
frontend/app/profile.tsx
@@ -0,0 +1,158 @@
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 + });
frontend/public/tamagui.generated.css
Binary file brotha