FEAT: Adicionado níveis de acesso e alterações do mesmo pelo painel de admin
Some checks are pending
Deploy Dev / deploy (push) Waiting to run
Some checks are pending
Deploy Dev / deploy (push) Waiting to run
This commit is contained in:
parent
f7a3f80efe
commit
da737b62fc
0
backend-dev.err.log
Normal file
0
backend-dev.err.log
Normal file
26
backend-dev.log
Normal file
26
backend-dev.log
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
> omnichannel-backend@1.0.0 dev
|
||||||
|
> cross-env NODE_ENV=development nest start --watch
|
||||||
|
|
||||||
|
[2J[3J[H[[90m17:25:31[0m] Starting compilation in watch mode...
|
||||||
|
|
||||||
|
[[90m17:25:39[0m] Found 0 errors. Watching for file changes.
|
||||||
|
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[NestFactory] [39m[32mStarting Nest application...[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[InstanceLoader] [39m[32mDatabaseModule dependencies initialized[39m[38;5;3m +27ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[InstanceLoader] [39m[32mAppModule dependencies initialized[39m[38;5;3m +11ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[InstanceLoader] [39m[32mAdminModule dependencies initialized[39m[38;5;3m +4ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[InstanceLoader] [39m[32mAuthModule dependencies initialized[39m[38;5;3m +1ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[RoutesResolver] [39m[32mAppController {/}:[39m[38;5;3m +17ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/health, GET} route[39m[38;5;3m +17ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[RoutesResolver] [39m[32mAuthController {/auth}:[39m[38;5;3m +2ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/auth/config, GET} route[39m[38;5;3m +3ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/auth/login, POST} route[39m[38;5;3m +4ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/auth/oauth/microsoft/start, GET} route[39m[38;5;3m +2ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/auth/oauth/microsoft/callback, GET} route[39m[38;5;3m +3ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[RoutesResolver] [39m[32mAdminAccessController {/admin/access}:[39m[38;5;3m +2ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/admin/access/options, GET} route[39m[38;5;3m +4ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/admin/access/users, GET} route[39m[38;5;3m +2ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/admin/access/users/:id, PUT} route[39m[38;5;3m +4ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[NestApplication] [39m[32mNest application successfully started[39m[38;5;3m +9ms[39m
|
||||||
|
[32m[Nest] 42064 - [39m14/05/2026, 17:25:44 [32m LOG[39m [38;5;3m[Bootstrap] [39m[32mBackend ouvindo na porta 3001[39m
|
||||||
57
backend-start.err.log
Normal file
57
backend-start.err.log
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
[31m[Nest] 4744 - [39m14/05/2026, 17:25:07 [31m ERROR[39m [38;5;3m[ExceptionsHandler] [39mAggregateError [ECONNREFUSED]:
|
||||||
|
at [90mC:\Users\rafael.lopes\Projetos\DevelopmentSothis\omnichannel\backend\[39mnode_modules\[4mpg-pool[24m\index.js:45:11
|
||||||
|
[90m at process.processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
||||||
|
at async Promise.all (index 0)
|
||||||
|
at async AdminAccessService.getOptions [90m(C:\Users\rafael.lopes\Projetos\DevelopmentSothis\omnichannel\backend\[39mdist\modules\admin\admin-access.service.js:21:35[90m)[39m
|
||||||
|
at async [90mC:\Users\rafael.lopes\Projetos\DevelopmentSothis\omnichannel\backend\[39mnode_modules\[4m@nestjs\core[24m\router\router-execution-context.js:46:28
|
||||||
|
at async [90mC:\Users\rafael.lopes\Projetos\DevelopmentSothis\omnichannel\backend\[39mnode_modules\[4m@nestjs\core[24m\router\router-proxy.js:9:17 {
|
||||||
|
code: [32m'ECONNREFUSED'[39m,
|
||||||
|
[errors]: [
|
||||||
|
Error: connect ECONNREFUSED ::1:5432
|
||||||
|
[90m at createConnectionError (node:net:1678:14)[39m
|
||||||
|
[90m at afterConnectMultiple (node:net:1708:16)[39m {
|
||||||
|
errno: [33m-4078[39m,
|
||||||
|
code: [32m'ECONNREFUSED'[39m,
|
||||||
|
syscall: [32m'connect'[39m,
|
||||||
|
address: [32m'::1'[39m,
|
||||||
|
port: [33m5432[39m
|
||||||
|
},
|
||||||
|
Error: connect ECONNREFUSED 127.0.0.1:5432
|
||||||
|
[90m at createConnectionError (node:net:1678:14)[39m
|
||||||
|
[90m at afterConnectMultiple (node:net:1708:16)[39m {
|
||||||
|
errno: [33m-4078[39m,
|
||||||
|
code: [32m'ECONNREFUSED'[39m,
|
||||||
|
syscall: [32m'connect'[39m,
|
||||||
|
address: [32m'127.0.0.1'[39m,
|
||||||
|
port: [33m5432[39m
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
[31m[Nest] 4744 - [39m14/05/2026, 17:25:07 [31m ERROR[39m [38;5;3m[ExceptionsHandler] [39mAggregateError [ECONNREFUSED]:
|
||||||
|
at [90mC:\Users\rafael.lopes\Projetos\DevelopmentSothis\omnichannel\backend\[39mnode_modules\[4mpg-pool[24m\index.js:45:11
|
||||||
|
[90m at process.processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
||||||
|
at async AdminAccessService.listUsers [90m(C:\Users\rafael.lopes\Projetos\DevelopmentSothis\omnichannel\backend\[39mdist\modules\admin\admin-access.service.js:31:24[90m)[39m
|
||||||
|
at async [90mC:\Users\rafael.lopes\Projetos\DevelopmentSothis\omnichannel\backend\[39mnode_modules\[4m@nestjs\core[24m\router\router-execution-context.js:46:28
|
||||||
|
at async [90mC:\Users\rafael.lopes\Projetos\DevelopmentSothis\omnichannel\backend\[39mnode_modules\[4m@nestjs\core[24m\router\router-proxy.js:9:17 {
|
||||||
|
code: [32m'ECONNREFUSED'[39m,
|
||||||
|
[errors]: [
|
||||||
|
Error: connect ECONNREFUSED ::1:5432
|
||||||
|
[90m at createConnectionError (node:net:1678:14)[39m
|
||||||
|
[90m at afterConnectMultiple (node:net:1708:16)[39m {
|
||||||
|
errno: [33m-4078[39m,
|
||||||
|
code: [32m'ECONNREFUSED'[39m,
|
||||||
|
syscall: [32m'connect'[39m,
|
||||||
|
address: [32m'::1'[39m,
|
||||||
|
port: [33m5432[39m
|
||||||
|
},
|
||||||
|
Error: connect ECONNREFUSED 127.0.0.1:5432
|
||||||
|
[90m at createConnectionError (node:net:1678:14)[39m
|
||||||
|
[90m at afterConnectMultiple (node:net:1708:16)[39m {
|
||||||
|
errno: [33m-4078[39m,
|
||||||
|
code: [32m'ECONNREFUSED'[39m,
|
||||||
|
syscall: [32m'connect'[39m,
|
||||||
|
address: [32m'127.0.0.1'[39m,
|
||||||
|
port: [33m5432[39m
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
22
backend-start.log
Normal file
22
backend-start.log
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
> omnichannel-backend@1.0.0 start
|
||||||
|
> cross-env NODE_ENV=production node dist/main.js
|
||||||
|
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[NestFactory] [39m[32mStarting Nest application...[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[InstanceLoader] [39m[32mDatabaseModule dependencies initialized[39m[38;5;3m +11ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[InstanceLoader] [39m[32mAppModule dependencies initialized[39m[38;5;3m +3ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[InstanceLoader] [39m[32mAdminModule dependencies initialized[39m[38;5;3m +1ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[InstanceLoader] [39m[32mAuthModule dependencies initialized[39m[38;5;3m +1ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[RoutesResolver] [39m[32mAppController {/}:[39m[38;5;3m +3ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/health, GET} route[39m[38;5;3m +2ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[RoutesResolver] [39m[32mAuthController {/auth}:[39m[38;5;3m +1ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/auth/config, GET} route[39m[38;5;3m +0ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/auth/login, POST} route[39m[38;5;3m +1ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/auth/oauth/microsoft/start, GET} route[39m[38;5;3m +2ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/auth/oauth/microsoft/callback, GET} route[39m[38;5;3m +2ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[RoutesResolver] [39m[32mAdminAccessController {/admin/access}:[39m[38;5;3m +0ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/admin/access/options, GET} route[39m[38;5;3m +1ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/admin/access/users, GET} route[39m[38;5;3m +0ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[RouterExplorer] [39m[32mMapped {/admin/access/users/:id, PUT} route[39m[38;5;3m +1ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[NestApplication] [39m[32mNest application successfully started[39m[38;5;3m +2ms[39m
|
||||||
|
[32m[Nest] 4744 - [39m14/05/2026, 17:24:40 [32m LOG[39m [38;5;3m[Bootstrap] [39m[32mBackend ouvindo na porta 3001[39m
|
||||||
161
package-lock.json
generated
161
package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"ldapts": "^8.1.7",
|
"ldapts": "^8.1.7",
|
||||||
|
"pg": "^8.20.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"winston": "^3.19.0",
|
"winston": "^3.19.0",
|
||||||
@ -23,6 +24,7 @@
|
|||||||
"@nestjs/cli": "^11.0.21",
|
"@nestjs/cli": "^11.0.21",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^25.6.2",
|
"@types/node": "^25.6.2",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
@ -903,6 +905,18 @@
|
|||||||
"undici-types": "~7.19.0"
|
"undici-types": "~7.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pg": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"pg-protocol": "*",
|
||||||
|
"pg-types": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/triple-beam": {
|
"node_modules/@types/triple-beam": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||||
@ -3392,6 +3406,96 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pg": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"pg-connection-string": "^2.12.0",
|
||||||
|
"pg-pool": "^3.13.0",
|
||||||
|
"pg-protocol": "^1.13.0",
|
||||||
|
"pg-types": "2.2.0",
|
||||||
|
"pgpass": "1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"pg-cloudflare": "^1.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"pg-native": ">=3.0.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"pg-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-cloudflare": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/pg-connection-string": {
|
||||||
|
"version": "2.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
|
||||||
|
"integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pg-int8": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-pool": {
|
||||||
|
"version": "3.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
|
||||||
|
"integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"pg": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pg-protocol": {
|
||||||
|
"version": "1.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
|
||||||
|
"integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pg-types": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pg-int8": "1.0.1",
|
||||||
|
"postgres-array": "~2.0.0",
|
||||||
|
"postgres-bytea": "~1.0.0",
|
||||||
|
"postgres-date": "~1.0.4",
|
||||||
|
"postgres-interval": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pgpass": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"split2": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@ -3422,6 +3526,45 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postgres-array": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-bytea": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-date": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postgres-interval": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xtend": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@ -3877,6 +4020,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split2": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stack-trace": {
|
"node_modules/stack-trace": {
|
||||||
"version": "0.0.10",
|
"version": "0.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||||
@ -4601,6 +4753,15 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/xtend": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs-parser": {
|
"node_modules/yargs-parser": {
|
||||||
"version": "21.1.1",
|
"version": "21.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"ldapts": "^8.1.7",
|
"ldapts": "^8.1.7",
|
||||||
|
"pg": "^8.20.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"winston": "^3.19.0",
|
"winston": "^3.19.0",
|
||||||
@ -24,6 +25,7 @@
|
|||||||
"@nestjs/cli": "^11.0.21",
|
"@nestjs/cli": "^11.0.21",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^25.6.2",
|
"@types/node": "^25.6.2",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
|
import { DatabaseModule } from './infra/database/database.module';
|
||||||
|
import { AdminModule } from './modules/admin/admin.module';
|
||||||
import { AuthModule } from './modules/auth/auth.module';
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuthModule],
|
imports: [DatabaseModule, AuthModule, AdminModule],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
9
src/infra/database/database.module.ts
Normal file
9
src/infra/database/database.module.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { DatabaseService } from './database.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [DatabaseService],
|
||||||
|
exports: [DatabaseService],
|
||||||
|
})
|
||||||
|
export class DatabaseModule {}
|
||||||
37
src/infra/database/database.service.ts
Normal file
37
src/infra/database/database.service.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { Pool, PoolClient, QueryResultRow } from 'pg';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DatabaseService implements OnModuleDestroy {
|
||||||
|
private readonly pool = new Pool({
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: Number(process.env.DB_PORT || 5432),
|
||||||
|
user: process.env.DB_USER || process.env.POSTGRES_USER,
|
||||||
|
password: process.env.DB_PASSWORD || process.env.POSTGRES_PASSWORD,
|
||||||
|
database: process.env.DB_NAME || process.env.POSTGRES_DB,
|
||||||
|
});
|
||||||
|
|
||||||
|
query<T extends QueryResultRow = QueryResultRow>(text: string, params?: unknown[]) {
|
||||||
|
return this.pool.query<T>(text, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction<T>(handler: (client: PoolClient) => Promise<T>) {
|
||||||
|
return this.pool.connect().then(async (client) => {
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
const result = await handler(client);
|
||||||
|
await client.query('COMMIT');
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/modules/admin/admin-access.controller.ts
Normal file
25
src/modules/admin/admin-access.controller.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
|
||||||
|
import { AdminAccessService } from './admin-access.service';
|
||||||
|
|
||||||
|
@Controller('admin/access')
|
||||||
|
export class AdminAccessController {
|
||||||
|
constructor(private readonly adminAccessService: AdminAccessService) {}
|
||||||
|
|
||||||
|
@Get('options')
|
||||||
|
getOptions() {
|
||||||
|
return this.adminAccessService.getOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('users')
|
||||||
|
listUsers() {
|
||||||
|
return this.adminAccessService.listUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('users/:id')
|
||||||
|
updateUserAccess(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: { perfilId?: number | null; areaId?: number | null },
|
||||||
|
) {
|
||||||
|
return this.adminAccessService.updateUserAccess(Number(id), body);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/modules/admin/admin-access.service.ts
Normal file
103
src/modules/admin/admin-access.service.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DatabaseService } from '../../infra/database/database.service';
|
||||||
|
|
||||||
|
interface AccessUpdateInput {
|
||||||
|
perfilId?: number | null;
|
||||||
|
areaId?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminAccessService {
|
||||||
|
constructor(private readonly database: DatabaseService) {}
|
||||||
|
|
||||||
|
async getOptions() {
|
||||||
|
const [profiles, areas] = await Promise.all([
|
||||||
|
this.database.query<{ id: number; nome: string }>(
|
||||||
|
'SELECT id, nome FROM perfis_acesso ORDER BY nome',
|
||||||
|
),
|
||||||
|
this.database.query<{ id: number; nome: string }>(
|
||||||
|
'SELECT id, nome FROM areas WHERE ativo = TRUE ORDER BY nome',
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
profiles: profiles.rows,
|
||||||
|
areas: areas.rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async listUsers() {
|
||||||
|
const result = await this.database.query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
u.id,
|
||||||
|
u.nome,
|
||||||
|
u.email,
|
||||||
|
u.ativo,
|
||||||
|
COALESCE(
|
||||||
|
JSON_AGG(DISTINCT JSONB_BUILD_OBJECT('id', p.id, 'nome', p.nome))
|
||||||
|
FILTER (WHERE p.id IS NOT NULL),
|
||||||
|
'[]'
|
||||||
|
) AS perfis,
|
||||||
|
COALESCE(
|
||||||
|
JSON_AGG(DISTINCT JSONB_BUILD_OBJECT('id', a.id, 'nome', a.nome, 'principal', ua.principal))
|
||||||
|
FILTER (WHERE a.id IS NOT NULL AND ua.ativo = TRUE),
|
||||||
|
'[]'
|
||||||
|
) AS areas
|
||||||
|
FROM usuarios u
|
||||||
|
LEFT JOIN usuarios_perfis up ON up.usuario_id = u.id
|
||||||
|
LEFT JOIN perfis_acesso p ON p.id = up.perfil_id
|
||||||
|
LEFT JOIN usuarios_areas ua ON ua.usuario_id = u.id
|
||||||
|
LEFT JOIN areas a ON a.id = ua.area_id
|
||||||
|
GROUP BY u.id
|
||||||
|
ORDER BY u.nome
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows.map((user: any) => {
|
||||||
|
const perfis = Array.isArray(user.perfis) ? user.perfis : [];
|
||||||
|
const areas = Array.isArray(user.areas) ? user.areas : [];
|
||||||
|
const primaryArea = areas.find((area) => area.principal) || areas[0] || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
nome: user.nome,
|
||||||
|
email: user.email,
|
||||||
|
ativo: user.ativo,
|
||||||
|
perfis,
|
||||||
|
areas,
|
||||||
|
perfilPrincipal: perfis[0] || null,
|
||||||
|
areaPrincipal: primaryArea,
|
||||||
|
accessStatus: perfis.length && areas.length ? 'assigned' : 'unassigned',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUserAccess(usuarioId: number, input: AccessUpdateInput) {
|
||||||
|
await this.database.transaction(async (client) => {
|
||||||
|
await client.query('DELETE FROM usuarios_perfis WHERE usuario_id = $1', [usuarioId]);
|
||||||
|
await client.query('DELETE FROM usuarios_areas WHERE usuario_id = $1', [usuarioId]);
|
||||||
|
|
||||||
|
if (input.perfilId) {
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO usuarios_perfis (usuario_id, perfil_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
|
||||||
|
[usuarioId, input.perfilId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.areaId) {
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO usuarios_areas (usuario_id, area_id, principal, ativo)
|
||||||
|
VALUES ($1, $2, TRUE, TRUE)
|
||||||
|
ON CONFLICT (usuario_id, area_id)
|
||||||
|
DO UPDATE SET principal = TRUE, ativo = TRUE, updated_at = NOW()
|
||||||
|
`,
|
||||||
|
[usuarioId, input.areaId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.listUsers().then((users) => users.find((user) => user.id === usuarioId));
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/modules/admin/admin.module.ts
Normal file
9
src/modules/admin/admin.module.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AdminAccessController } from './admin-access.controller';
|
||||||
|
import { AdminAccessService } from './admin-access.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AdminAccessController],
|
||||||
|
providers: [AdminAccessService],
|
||||||
|
})
|
||||||
|
export class AdminModule {}
|
||||||
@ -20,6 +20,11 @@ export class AuthTokenService {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
provider: user.provider,
|
provider: user.provider,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
perfis: user.perfis || [],
|
||||||
|
profiles: user.profiles || user.perfis || [],
|
||||||
|
areas: user.areas || [],
|
||||||
|
areaPrincipal: user.areaPrincipal || null,
|
||||||
|
accessStatus: user.accessStatus || 'unassigned',
|
||||||
},
|
},
|
||||||
config.jwtSecret,
|
config.jwtSecret,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export class AuthController {
|
|||||||
|
|
||||||
redirectUrl.searchParams.set('token', authResult.token);
|
redirectUrl.searchParams.set('token', authResult.token);
|
||||||
redirectUrl.searchParams.set('provider', authResult.user.provider);
|
redirectUrl.searchParams.set('provider', authResult.user.provider);
|
||||||
|
redirectUrl.searchParams.set('user', JSON.stringify(authResult.user));
|
||||||
|
|
||||||
return response.redirect(redirectUrl.toString());
|
return response.redirect(redirectUrl.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { AuthConfigService } from './auth.config';
|
|||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from './auth.controller';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthTokenService } from './auth-token.service';
|
import { AuthTokenService } from './auth-token.service';
|
||||||
|
import { UserAccessService } from './user-access.service';
|
||||||
import { LdapAuthProvider } from './providers/ldap-auth.provider';
|
import { LdapAuthProvider } from './providers/ldap-auth.provider';
|
||||||
import { MicrosoftOAuthProvider } from './providers/microsoft-oauth.provider';
|
import { MicrosoftOAuthProvider } from './providers/microsoft-oauth.provider';
|
||||||
import { OAuthStateService } from './providers/oauth-state.service';
|
import { OAuthStateService } from './providers/oauth-state.service';
|
||||||
@ -13,6 +14,7 @@ import { OAuthStateService } from './providers/oauth-state.service';
|
|||||||
AuthConfigService,
|
AuthConfigService,
|
||||||
AuthService,
|
AuthService,
|
||||||
AuthTokenService,
|
AuthTokenService,
|
||||||
|
UserAccessService,
|
||||||
LdapAuthProvider,
|
LdapAuthProvider,
|
||||||
MicrosoftOAuthProvider,
|
MicrosoftOAuthProvider,
|
||||||
OAuthStateService,
|
OAuthStateService,
|
||||||
|
|||||||
@ -9,6 +9,12 @@ export interface AuthenticatedUser {
|
|||||||
email: string | null;
|
email: string | null;
|
||||||
username: string;
|
username: string;
|
||||||
provider: 'ldap' | 'microsoft';
|
provider: 'ldap' | 'microsoft';
|
||||||
|
databaseId?: number;
|
||||||
|
perfis?: string[];
|
||||||
|
profiles?: string[];
|
||||||
|
areas?: string[];
|
||||||
|
areaPrincipal?: string | null;
|
||||||
|
accessStatus?: 'assigned' | 'unassigned';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResult {
|
export interface AuthResult {
|
||||||
|
|||||||
@ -3,12 +3,14 @@ import { Client } from 'ldapts';
|
|||||||
import { AuthConfigService } from '../auth.config';
|
import { AuthConfigService } from '../auth.config';
|
||||||
import { AuthTokenService } from '../auth-token.service';
|
import { AuthTokenService } from '../auth-token.service';
|
||||||
import { AuthResult, LoginData } from '../auth.types';
|
import { AuthResult, LoginData } from '../auth.types';
|
||||||
|
import { UserAccessService } from '../user-access.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LdapAuthProvider {
|
export class LdapAuthProvider {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly authConfig: AuthConfigService,
|
private readonly authConfig: AuthConfigService,
|
||||||
private readonly authToken: AuthTokenService,
|
private readonly authToken: AuthTokenService,
|
||||||
|
private readonly userAccess: UserAccessService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async authenticate({ username, password }: LoginData): Promise<AuthResult> {
|
async authenticate({ username, password }: LoginData): Promise<AuthResult> {
|
||||||
@ -41,7 +43,7 @@ export class LdapAuthProvider {
|
|||||||
await client.bind(userPrincipal, password);
|
await client.bind(userPrincipal, password);
|
||||||
|
|
||||||
const directoryUser = await this.searchUser(client, username);
|
const directoryUser = await this.searchUser(client, username);
|
||||||
const user = {
|
const providerUser = {
|
||||||
id: directoryUser?.email || userPrincipal,
|
id: directoryUser?.email || userPrincipal,
|
||||||
name: directoryUser?.name || username,
|
name: directoryUser?.name || username,
|
||||||
email:
|
email:
|
||||||
@ -50,6 +52,7 @@ export class LdapAuthProvider {
|
|||||||
username: directoryUser?.username || username,
|
username: directoryUser?.username || username,
|
||||||
provider: 'ldap' as const,
|
provider: 'ldap' as const,
|
||||||
};
|
};
|
||||||
|
const user = await this.userAccess.syncAuthenticatedUser(providerUser);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: this.authToken.issueToken(user),
|
token: this.authToken.issueToken(user),
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
import { AuthConfigService } from '../auth.config';
|
import { AuthConfigService } from '../auth.config';
|
||||||
import { AuthTokenService } from '../auth-token.service';
|
import { AuthTokenService } from '../auth-token.service';
|
||||||
import { AuthResult } from '../auth.types';
|
import { AuthResult } from '../auth.types';
|
||||||
|
import { UserAccessService } from '../user-access.service';
|
||||||
import { OAuthStateService } from './oauth-state.service';
|
import { OAuthStateService } from './oauth-state.service';
|
||||||
|
|
||||||
const MICROSOFT_SCOPE = 'openid profile email User.Read';
|
const MICROSOFT_SCOPE = 'openid profile email User.Read';
|
||||||
@ -17,6 +18,7 @@ export class MicrosoftOAuthProvider {
|
|||||||
private readonly authConfig: AuthConfigService,
|
private readonly authConfig: AuthConfigService,
|
||||||
private readonly authToken: AuthTokenService,
|
private readonly authToken: AuthTokenService,
|
||||||
private readonly oauthState: OAuthStateService,
|
private readonly oauthState: OAuthStateService,
|
||||||
|
private readonly userAccess: UserAccessService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getAuthorizeUrl() {
|
getAuthorizeUrl() {
|
||||||
@ -43,13 +45,14 @@ export class MicrosoftOAuthProvider {
|
|||||||
const tokenResponse = await this.exchangeCode(query.code);
|
const tokenResponse = await this.exchangeCode(query.code);
|
||||||
const microsoftUser = await this.getMicrosoftUser(tokenResponse.access_token);
|
const microsoftUser = await this.getMicrosoftUser(tokenResponse.access_token);
|
||||||
const email = microsoftUser.mail || microsoftUser.userPrincipalName;
|
const email = microsoftUser.mail || microsoftUser.userPrincipalName;
|
||||||
const user = {
|
const providerUser = {
|
||||||
id: microsoftUser.id || email,
|
id: microsoftUser.id || email,
|
||||||
name: microsoftUser.displayName || email,
|
name: microsoftUser.displayName || email,
|
||||||
email,
|
email,
|
||||||
username: microsoftUser.userPrincipalName || email,
|
username: microsoftUser.userPrincipalName || email,
|
||||||
provider: 'microsoft' as const,
|
provider: 'microsoft' as const,
|
||||||
};
|
};
|
||||||
|
const user = await this.userAccess.syncAuthenticatedUser(providerUser);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: this.authToken.issueToken(user),
|
token: this.authToken.issueToken(user),
|
||||||
|
|||||||
107
src/modules/auth/user-access.service.ts
Normal file
107
src/modules/auth/user-access.service.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PoolClient } from 'pg';
|
||||||
|
import { DatabaseService } from '../../infra/database/database.service';
|
||||||
|
import { AuthenticatedUser } from './auth.types';
|
||||||
|
|
||||||
|
interface UserAccessRow {
|
||||||
|
id: number;
|
||||||
|
perfis: string[] | null;
|
||||||
|
areas: string[] | null;
|
||||||
|
area_principal: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserAccessService {
|
||||||
|
constructor(private readonly database: DatabaseService) {}
|
||||||
|
|
||||||
|
syncAuthenticatedUser(user: AuthenticatedUser): Promise<AuthenticatedUser> {
|
||||||
|
return this.database.transaction(async (client) => {
|
||||||
|
const usuarioId = await this.upsertUser(client, user);
|
||||||
|
await this.upsertProvider(client, usuarioId, user);
|
||||||
|
const access = await this.getUserAccess(client, usuarioId);
|
||||||
|
|
||||||
|
const perfis = access.perfis || [];
|
||||||
|
const areas = access.areas || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
id: String(usuarioId),
|
||||||
|
databaseId: usuarioId,
|
||||||
|
perfis,
|
||||||
|
profiles: perfis,
|
||||||
|
areas,
|
||||||
|
areaPrincipal: access.area_principal,
|
||||||
|
accessStatus: perfis.length && areas.length ? 'assigned' : 'unassigned',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async upsertUser(client: PoolClient, user: AuthenticatedUser) {
|
||||||
|
const email = user.email || null;
|
||||||
|
const fallbackEmail = `${user.provider}:${user.username}`;
|
||||||
|
const lookupEmail = email || fallbackEmail;
|
||||||
|
|
||||||
|
const result = await client.query<{ id: number }>(
|
||||||
|
`
|
||||||
|
INSERT INTO usuarios (nome, email, ativo, updated_at)
|
||||||
|
VALUES ($1, $2, TRUE, NOW())
|
||||||
|
ON CONFLICT (email)
|
||||||
|
DO UPDATE SET
|
||||||
|
nome = EXCLUDED.nome,
|
||||||
|
ativo = TRUE,
|
||||||
|
updated_at = NOW()
|
||||||
|
RETURNING id
|
||||||
|
`,
|
||||||
|
[user.name || user.username, lookupEmail],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async upsertProvider(client: PoolClient, usuarioId: number, user: AuthenticatedUser) {
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO usuarios_provedores (usuario_id, provedor, provedor_user_id)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (provedor, provedor_user_id)
|
||||||
|
DO UPDATE SET usuario_id = EXCLUDED.usuario_id
|
||||||
|
`,
|
||||||
|
[usuarioId, user.provider, user.username || user.email || user.id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUserAccess(client: PoolClient, usuarioId: number) {
|
||||||
|
const result = await client.query<UserAccessRow>(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
u.id,
|
||||||
|
COALESCE(
|
||||||
|
ARRAY_AGG(DISTINCT p.nome) FILTER (WHERE p.nome IS NOT NULL),
|
||||||
|
ARRAY[]::VARCHAR[]
|
||||||
|
) AS perfis,
|
||||||
|
COALESCE(
|
||||||
|
ARRAY_AGG(DISTINCT a.nome) FILTER (WHERE a.nome IS NOT NULL AND ua.ativo = TRUE),
|
||||||
|
ARRAY[]::VARCHAR[]
|
||||||
|
) AS areas,
|
||||||
|
MAX(a.nome) FILTER (WHERE ua.principal = TRUE AND ua.ativo = TRUE) AS area_principal
|
||||||
|
FROM usuarios u
|
||||||
|
LEFT JOIN usuarios_perfis up ON up.usuario_id = u.id
|
||||||
|
LEFT JOIN perfis_acesso p ON p.id = up.perfil_id
|
||||||
|
LEFT JOIN usuarios_areas ua ON ua.usuario_id = u.id
|
||||||
|
LEFT JOIN areas a ON a.id = ua.area_id
|
||||||
|
WHERE u.id = $1
|
||||||
|
GROUP BY u.id
|
||||||
|
`,
|
||||||
|
[usuarioId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
result.rows[0] || {
|
||||||
|
id: usuarioId,
|
||||||
|
perfis: [],
|
||||||
|
areas: [],
|
||||||
|
area_principal: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user