Compare commits

...

33 Commits

Author SHA1 Message Date
Lakhan Samani
2f29bbcee4 fix: oauth callback update user 2022-02-02 12:30:11 +05:30
Lakhan Samani
63a8c82535 chore: update package-lock.json 2022-02-02 11:41:36 +05:30
Lakhan Samani
1f81e45e79 fix: user dashboard created_at + add signup_methods & roles 2022-02-02 11:39:08 +05:30
Lakhan Samani
40dcf67de9 fix: remove logs 2022-01-31 14:53:17 +05:30
Lakhan Samani
32d8b7c038 Merge branch 'main' of https://github.com/authorizerdev/authorizer 2022-01-31 14:30:54 +05:30
Lakhan Samani
2a91f3e7d8 Merge pull request #113 from anik-ghosh-au7/fix/dashboard
Fix/dashboard
2022-01-31 14:30:32 +05:30
Lakhan Samani
36d9861517 Fix getting host 2022-01-31 14:30:13 +05:30
Anik Ghosh
d577a21a9a user dashboard pagination bug removed 2022-01-31 14:26:19 +05:30
Anik Ghosh
115607cb6b Merge branch 'main' of https://github.com/authorizerdev/authorizer into fix/dashboard 2022-01-31 13:29:38 +05:30
Anik Ghosh
adb969ec04 add delete user modal 2022-01-31 13:23:38 +05:30
Lakhan Samani
4e48320cf1 fix: bug with authorizer url 2022-01-31 11:35:24 +05:30
Anik Ghosh
cfe035e96b loader added to user dashboard 2022-01-31 11:33:35 +05:30
Lakhan Samani
34a91f3195 Add log for host url 2022-01-30 23:44:37 +05:30
Lakhan Samani
ea14cc1743 Merge pull request #111 from anik-ghosh-au7/feat/manage-users
Feat/manage users
2022-01-30 23:43:21 +05:30
Anik Ghosh
9d7f5fd9db array input type update 2022-01-30 17:43:38 +05:30
Anik Ghosh
0520056e43 sync with main branch 2022-01-30 10:56:56 +05:30
Anik Ghosh
1821e27692 Merge branch 'main' of https://github.com/authorizerdev/authorizer into feat/manage-users 2022-01-30 10:52:35 +05:30
Anik Ghosh
388530a69c edit user details modal added 2022-01-30 10:39:35 +05:30
Lakhan Samani
1e32d790b3 Remove unsed code 2022-01-30 00:08:51 +05:30
Anik Ghosh
681ffc65f1 user list added 2022-01-29 20:53:53 +05:30
Lakhan Samani
6331ec7b7a Resolves #110 2022-01-29 17:03:21 +05:30
Lakhan Samani
25c9ce03bd fix: default logo 2022-01-27 09:55:05 +05:30
Lakhan Samani
ac416bfc7b fix(dashboard): mutation 2022-01-25 13:06:52 +05:30
Lakhan Samani
0049e1380b Merge pull request #108 from authorizerdev/feat/pagination
feat: add pagination for users & verification_requests
2022-01-25 10:58:42 +05:30
Lakhan Samani
9bd185a9c6 feat: add pagination for users & verification_requests 2022-01-25 10:57:40 +05:30
Lakhan Samani
82bc38923c Feat/manage env vars (#107)
* ui to manage env variables added

* ui to manage env variables updated

* ui to manage env variables updated

* ui to manage env variables updated

* ui to manage env variables updated

* ui updates

* ui updates

* ui updates

* ui updates

* env vars input field updated

* env vars input field updated

Co-authored-by: Anik Ghosh <tech.anikghosh@gmail.com>
2022-01-25 09:34:35 +05:30
Lakhan Samani
8df8010b22 Fix SMTP fields for update env mutation 2022-01-24 21:16:29 +05:30
Lakhan Samani
b42cc1549a fix: env to return custom access token script 2022-01-24 10:22:55 +05:30
Lakhan Samani
4bc9059b0f fix: allow using cookie and header in case of validating jwt 2022-01-24 09:56:12 +05:30
Lakhan Samani
87b1cac979 feat(server): add is_valid_jwt query 2022-01-24 00:32:06 +05:30
Lakhan Samani
7f18a3f634 Implement refresh token logic with fingerprint + rotation 2022-01-23 01:24:41 +05:30
Lakhan Samani
0511e737ae fix(server): add update roles env validation 2022-01-22 11:29:03 +05:30
Lakhan Samani
003d88fb6c Merge pull request #106 from authorizerdev/fix/organize_dbs
fix: organize dbs
2022-01-21 13:37:37 +05:30
124 changed files with 7814 additions and 1043 deletions

View File

@@ -1,16 +1,2 @@
ENV=production
DATABASE_URL=data.db DATABASE_URL=data.db
DATABASE_TYPE=sqlite DATABASE_TYPE=sqlite
ADMIN_SECRET=admin
JWT_SECRET=random_string
SENDER_EMAIL=info@authorizer.dev
SMTP_USERNAME=username
SMTP_PASSWORD=password
SMTP_HOST=smtp.mailtrap.io
SMTP_PORT=2525
JWT_TYPE=HS256
ROLES=user
DEFAULT_ROLES=user
PROTECTED_ROLES=admin
JWT_ROLE_CLAIM=role
CUSTOM_ACCESS_TOKEN_SCRIPT=function(user,tokenPayload){var data = tokenPayload;data.extra = {'x-extra-id': user.id};return data;}

View File

@@ -24,7 +24,9 @@ FROM alpine:latest
WORKDIR /root/ WORKDIR /root/
RUN mkdir app dashboard RUN mkdir app dashboard
COPY --from=node-builder /authorizer/app/build app/build COPY --from=node-builder /authorizer/app/build app/build
COPY --from=node-builder /authorizer/app/favicon_io app/favicon_io
COPY --from=node-builder /authorizer/dashboard/build dashboard/build COPY --from=node-builder /authorizer/dashboard/build dashboard/build
COPY --from=node-builder /authorizer/dashboard/favicon_io dashboard/favicon_io
COPY --from=go-builder /authorizer/build build COPY --from=go-builder /authorizer/build build
COPY templates templates COPY templates templates
EXPOSE 8080 EXPOSE 8080

View File

@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://authorizer.dev"> <a href="https://authorizer.dev">
<img alt="Logo" src="https://github.com/authorizerdev/authorizer/blob/main/assets/logo.png" width="60" /> <img alt="Logo" src="https://authorizer.dev/images/logo.png" width="60" />
</a> </a>
</p> </p>
<h1 align="center"> <h1 align="center">
@@ -129,18 +129,13 @@ Required environment variables are pre-configured in `.env` file. But based on t
> Note: For mac users, you might have to give binary the permission to execute. Here is the command you can use to grant permission `xattr -d com.apple.quarantine build/server` > Note: For mac users, you might have to give binary the permission to execute. Here is the command you can use to grant permission `xattr -d com.apple.quarantine build/server`
## Install instance on Heroku Deploy production ready Authorizer instance using one click deployment options available below
Deploy Authorizer using [heroku](https://github.com/authorizerdev/authorizer-heroku) and quickly play with it in 30seconds | **Infra provider** | **One-click link** | **Additional information** |
<br/><br/> | :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------: |
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku) | Railway.app | <a href="https://railway.app/new/template?template=https://github.com/authorizerdev/authorizer-railway&amp;plugins=postgresql,redis"><img src="https://railway.app/button.svg" style="height: 44px" alt="Deploy on Railway"></a> | [docs](https://docs.authorizer.dev/deployment/railway) |
| Heroku | <a href="https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku"><img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy to Heroku" style="height: 44px;"></a> | [docs](https://docs.authorizer.dev/deployment/heroku) |
# Install instance on railway | Render | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/authorizerdev/authorizer-render) | [docs](https://docs.authorizer.dev/deployment/render) |
Deploy production ready Authorizer instance using [railway.app](https://github.com/authorizerdev/authorizer-railway) with postgres and redis for free and build with it in 30seconds
<br/>
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template?template=https%3A%2F%2Fgithub.com%2Fauthorizerdev%2Fauthorizer-railway&plugins=postgresql%2Credis&envs=ENV%2CDATABASE_TYPE%2CADMIN_SECRET%2CCOOKIE_NAME%2CJWT_ROLE_CLAIM%2CJWT_TYPE%2CJWT_SECRET%2CFACEBOOK_CLIENT_ID%2CFACEBOOK_CLIENT_SECRET%2CGOOGLE_CLIENT_ID%2CGOOGLE_CLIENT_SECRET%2CGITHUB_CLIENT_ID%2CGITHUB_CLIENT_SECRET%2CALLOWED_ORIGINS%2CROLES%2CPROTECTED_ROLES%2CDEFAULT_ROLES&optionalEnvs=FACEBOOK_CLIENT_ID%2CFACEBOOK_CLIENT_SECRET%2CGOOGLE_CLIENT_ID%2CGOOGLE_CLIENT_SECRET%2CGITHUB_CLIENT_ID%2CGITHUB_CLIENT_SECRET%2CALLOWED_ORIGINS%2CROLES%2CPROTECTED_ROLES%2CDEFAULT_ROLES&ENVDesc=Deployment+environment&DATABASE_TYPEDesc=With+railway+we+are+deploying+postgres+db&ADMIN_SECRETDesc=Secret+to+access+the+admin+apis&COOKIE_NAMEDesc=Name+of+http+only+cookie+that+will+be+used+as+session&FACEBOOK_CLIENT_IDDesc=Facebook+client+ID+for+facebook+login&FACEBOOK_CLIENT_SECRETDesc=Facebook+client+secret+for+facebook+login&GOOGLE_CLIENT_IDDesc=Google+client+ID+for+google+login&GOOGLE_CLIENT_SECRETDesc=Google+client+secret+for+google+login&GITHUB_CLIENT_IDDesc=Github+client+ID+for+github+login&GITHUB_CLIENT_SECRETDesc=Github+client+secret+for+github+login&ALLOWED_ORIGINSDesc=Whitelist+the+URL+for+which+this+instance+of+authorizer+is+allowed&ROLESDesc=Comma+separated+list+of+roles+that+platform+supports.+Default+role+is+user&PROTECTED_ROLESDesc=Comma+separated+list+of+protected+roles+for+which+sign-up+is+disabled&DEFAULT_ROLESDesc=Default+role+that+should+be+assigned+to+user.+It+should+be+one+from+the+list+of+%60ROLES%60+env.+Default+role+is+user&JWT_ROLE_CLAIMDesc=JWT+key+to+be+used+to+validate+the+role+field.&JWT_TYPEDesc=JWT+encryption+type&JWT_SECRETDesc=Random+string+that+will+be+used+for+encrypting+the+JWT+token&ENVDefault=PRODUCTION&DATABASE_TYPEDefault=postgres&COOKIE_NAMEDefault=authorizer&JWT_TYPEDefault=HS256&JWT_ROLE_CLAIMDefault=role)
### Things to consider ### Things to consider

11
TODO.md
View File

@@ -1,5 +1,16 @@
# Task List # Task List
## Implement better way of handling jwt tokens
Check: https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/#server-side-rendering-ssr
- [x] Set finger print in response cookie (https://github.com/hasura/jwt-guide/blob/60a7a86146d604fc48a799fffdee712be1c52cd0/lib/setFingerprintCookieAndSignJwt.ts#L8)
- [x] Save refresh token in session store
- [x] refresh token should be made more secure with the help of secure token rotation. Every time new token is requested new refresh token should be generated
- [x] Return jwt in response
- [x] To get session send finger print and refresh token [if they are valid -> a new access token is generated and sent to user]
- [x] Refresh token should be long living token (refresh token + finger print hash should be verified)
## Open ID compatible claims and schema ## Open ID compatible claims and schema
- [x] Rename `schema.graphqls` and re generate schema - [x] Rename `schema.graphqls` and re generate schema

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
app/favicon_io/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

884
app/package-lock.json generated
View File

@@ -1,23 +1,847 @@
{ {
"name": "app", "name": "app",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {
"": {
"name": "app",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@authorizerdev/authorizer-react": "latest",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-is": "^17.0.2",
"react-router-dom": "^5.2.0",
"typescript": "^4.3.5"
},
"devDependencies": {
"@types/react-router-dom": "^5.1.8"
}
},
"node_modules/@authorizerdev/authorizer-js": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.2.1.tgz",
"integrity": "sha512-5lQlh+nc5xTsPongfTyCSX24A1WESu/BjhmZwUNuScEOGady0qPoDHE3RBf46dpi5v05wbHCDN1IFEalX5zssQ==",
"dependencies": {
"node-fetch": "^2.6.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@authorizerdev/authorizer-react": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.4.3.tgz",
"integrity": "sha512-o/wWe9zZ3ARYdjbDfhGfvOxe1YQrE1YQ+UN9pcq85YSDkbfBkOfcnJ4YxlxWdL0Obd/ErDIeQ3vskyrfvRf3sA==",
"dependencies": {
"@authorizerdev/authorizer-js": "^0.2.1",
"final-form": "^4.20.2",
"react-final-form": "^6.5.3",
"styled-components": "^5.3.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/@babel/code-frame": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
"integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
"dependencies": {
"@babel/highlight": "^7.16.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/generator": {
"version": "7.16.8",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz",
"integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==",
"dependencies": {
"@babel/types": "^7.16.8",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-annotate-as-pure": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz",
"integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==",
"dependencies": {
"@babel/types": "^7.16.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz",
"integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==",
"dependencies": {
"@babel/types": "^7.16.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-function-name": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz",
"integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==",
"dependencies": {
"@babel/helper-get-function-arity": "^7.16.7",
"@babel/template": "^7.16.7",
"@babel/types": "^7.16.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-get-function-arity": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz",
"integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==",
"dependencies": {
"@babel/types": "^7.16.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-hoist-variables": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz",
"integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==",
"dependencies": {
"@babel/types": "^7.16.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz",
"integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==",
"dependencies": {
"@babel/types": "^7.16.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-split-export-declaration": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz",
"integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==",
"dependencies": {
"@babel/types": "^7.16.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.16.10",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
"integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.16.7",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.16.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz",
"integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A==",
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
"integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==",
"dependencies": {
"@babel/code-frame": "^7.16.7",
"@babel/parser": "^7.16.7",
"@babel/types": "^7.16.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.16.10",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.10.tgz",
"integrity": "sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw==",
"dependencies": {
"@babel/code-frame": "^7.16.7",
"@babel/generator": "^7.16.8",
"@babel/helper-environment-visitor": "^7.16.7",
"@babel/helper-function-name": "^7.16.7",
"@babel/helper-hoist-variables": "^7.16.7",
"@babel/helper-split-export-declaration": "^7.16.7",
"@babel/parser": "^7.16.10",
"@babel/types": "^7.16.8",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.16.8",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz",
"integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.16.7",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"dependencies": {
"@emotion/memoize": "0.7.4"
}
},
"node_modules/@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
},
"node_modules/@emotion/stylis": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
},
"node_modules/@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"node_modules/@types/history": {
"version": "4.7.9",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==",
"dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ=="
},
"node_modules/@types/react": {
"version": "17.0.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz",
"integrity": "sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "17.0.9",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz",
"integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-router": {
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz",
"integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==",
"dev": true,
"dependencies": {
"@types/history": "*",
"@types/react": "*"
}
},
"node_modules/@types/react-router-dom": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz",
"integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==",
"dev": true,
"dependencies": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/babel-plugin-styled-components": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz",
"integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.16.0",
"@babel/helper-module-imports": "^7.16.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"lodash": "^4.17.11"
},
"peerDependencies": {
"styled-components": ">= 2"
}
},
"node_modules/babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
},
"node_modules/camelize": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
},
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=",
"engines": {
"node": ">=4"
}
},
"node_modules/css-to-react-native": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
"integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
"dependencies": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^4.0.2"
}
},
"node_modules/csstype": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
},
"node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/esbuild": {
"version": "0.12.17",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.17.tgz",
"integrity": "sha512-GshKJyVYUnlSXIZj/NheC2O0Kblh42CS7P1wJyTbbIHevTG4jYMS9NNw8EOd8dDWD0dzydYHS01MpZoUcQXB4g==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
}
},
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/final-form": {
"version": "4.20.6",
"resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.6.tgz",
"integrity": "sha512-fCdwIj49KOaFfDRlXB57Eo+GghIMZQWrA9TakQI3C9uQxHwaFHXqZSNRlUdfnQmNNeySwGOaGPZCvjy58hyv4w==",
"dependencies": {
"@babel/runtime": "^7.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/final-form"
}
},
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"engines": {
"node": ">=4"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"engines": {
"node": ">=4"
}
},
"node_modules/history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/hoist-non-react-statics/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"dependencies": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
},
"peerDependencies": {
"prop-types": "^15.0.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"scheduler": "^0.20.2"
},
"peerDependencies": {
"react": "17.0.2"
}
},
"node_modules/react-final-form": {
"version": "6.5.7",
"resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.7.tgz",
"integrity": "sha512-o7tvJXB+McGiXOILqIC8lnOcX4aLhIBiF/Xi9Qet35b7XOS8R7KL8HLRKTfnZWQJm6MCE15v1U0SFive0NcxyA==",
"dependencies": {
"@babel/runtime": "^7.15.4"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/final-form"
},
"peerDependencies": {
"final-form": "4.20.4",
"react": "^16.8.0 || ^17.0.0"
}
},
"node_modules/react-final-form/node_modules/@babel/runtime": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
"integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"node_modules/react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/react-router-dom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/react-router/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"node_modules/resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"node_modules/scheduler": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"node_modules/shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/styled-components": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz",
"integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==",
"dependencies": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/traverse": "^7.4.5",
"@emotion/is-prop-valid": "^0.8.8",
"@emotion/stylis": "^0.8.4",
"@emotion/unitless": "^0.7.4",
"babel-plugin-styled-components": ">= 1.12.0",
"css-to-react-native": "^3.0.0",
"hoist-non-react-statics": "^3.0.0",
"shallowequal": "^1.1.0",
"supports-color": "^5.5.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/styled-components"
},
"peerDependencies": {
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0",
"react-is": ">= 16.8.0"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
"engines": {
"node": ">=4"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"node_modules/typescript": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
}
},
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": { "@authorizerdev/authorizer-js": {
"version": "0.2.0", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.2.1.tgz",
"integrity": "sha512-T2gurEYEP1T56j9IwDIWP1PsELoWcU7TBl5G0UsMqFQhKo7T6p2hM634Z7PS1yLegU3aihuvHGI0C0n4xSo0VQ==", "integrity": "sha512-5lQlh+nc5xTsPongfTyCSX24A1WESu/BjhmZwUNuScEOGady0qPoDHE3RBf46dpi5v05wbHCDN1IFEalX5zssQ==",
"requires": { "requires": {
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
} }
}, },
"@authorizerdev/authorizer-react": { "@authorizerdev/authorizer-react": {
"version": "0.4.0", "version": "0.4.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.4.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.4.3.tgz",
"integrity": "sha512-ydE7xrP3cqeTtU923bK0+OIB1fnL0VHnbThcNa41n89XUPV3VBhZ23gxMg90nqon8JFE5g2TNz7+/qBCOQ7aZw==", "integrity": "sha512-o/wWe9zZ3ARYdjbDfhGfvOxe1YQrE1YQ+UN9pcq85YSDkbfBkOfcnJ4YxlxWdL0Obd/ErDIeQ3vskyrfvRf3sA==",
"requires": { "requires": {
"@authorizerdev/authorizer-js": "^0.2.0", "@authorizerdev/authorizer-js": "^0.2.1",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"
@@ -32,11 +856,11 @@
} }
}, },
"@babel/generator": { "@babel/generator": {
"version": "7.16.7", "version": "7.16.8",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.7.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz",
"integrity": "sha512-/ST3Sg8MLGY5HVYmrjOgL60ENux/HfO/CsUh7y4MalThufhE/Ff/6EibFDHi4jiDCaWfJKoqbE6oTh21c5hrRg==", "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==",
"requires": { "requires": {
"@babel/types": "^7.16.7", "@babel/types": "^7.16.8",
"jsesc": "^2.5.1", "jsesc": "^2.5.1",
"source-map": "^0.5.0" "source-map": "^0.5.0"
} }
@@ -105,9 +929,9 @@
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==" "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
}, },
"@babel/highlight": { "@babel/highlight": {
"version": "7.16.7", "version": "7.16.10",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz",
"integrity": "sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==", "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==",
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.16.7", "@babel/helper-validator-identifier": "^7.16.7",
"chalk": "^2.0.0", "chalk": "^2.0.0",
@@ -115,9 +939,9 @@
} }
}, },
"@babel/parser": { "@babel/parser": {
"version": "7.16.7", "version": "7.16.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz",
"integrity": "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA==" "integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A=="
}, },
"@babel/runtime": { "@babel/runtime": {
"version": "7.14.8", "version": "7.14.8",
@@ -138,26 +962,26 @@
} }
}, },
"@babel/traverse": { "@babel/traverse": {
"version": "7.16.7", "version": "7.16.10",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.7.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.10.tgz",
"integrity": "sha512-8KWJPIb8c2VvY8AJrydh6+fVRo2ODx1wYBU2398xJVq0JomuLBZmVQzLPBblJgHIGYG4znCpUZUZ0Pt2vdmVYQ==", "integrity": "sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw==",
"requires": { "requires": {
"@babel/code-frame": "^7.16.7", "@babel/code-frame": "^7.16.7",
"@babel/generator": "^7.16.7", "@babel/generator": "^7.16.8",
"@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-environment-visitor": "^7.16.7",
"@babel/helper-function-name": "^7.16.7", "@babel/helper-function-name": "^7.16.7",
"@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-hoist-variables": "^7.16.7",
"@babel/helper-split-export-declaration": "^7.16.7", "@babel/helper-split-export-declaration": "^7.16.7",
"@babel/parser": "^7.16.7", "@babel/parser": "^7.16.10",
"@babel/types": "^7.16.7", "@babel/types": "^7.16.8",
"debug": "^4.1.0", "debug": "^4.1.0",
"globals": "^11.1.0" "globals": "^11.1.0"
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.16.7", "version": "7.16.8",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.7.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz",
"integrity": "sha512-E8HuV7FO9qLpx6OtoGfUQ2cjIYnbFwvZWYBS+87EwtdMvmUPJSwykpovFB+8insbpF0uJcpr8KMUi64XZntZcg==", "integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==",
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.16.7", "@babel/helper-validator-identifier": "^7.16.7",
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
@@ -420,9 +1244,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node-fetch": { "node-fetch": {
"version": "2.6.6", "version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": { "requires": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
} }

View File

@@ -15,7 +15,7 @@ export default function Dashboard() {
return ( return (
<div> <div>
<h1>Hey 👋,</h1> <h1>Hey 👋,</h1>
<p>Thank you for joining authorizer demo app.</p> <p>Thank you for using authorizer.</p>
<p> <p>
Your email address is{' '} Your email address is{' '}
<a href={`mailto:${user?.email}`} style={{ color: '#3B82F6' }}> <a href={`mailto:${user?.email}`} style={{ color: '#3B82F6' }}>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -17,9 +17,11 @@
"@types/react": "^17.0.38", "@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.2", "@types/react-router-dom": "^5.3.2",
"dayjs": "^1.10.7",
"esbuild": "^0.14.9", "esbuild": "^0.14.9",
"framer-motion": "^5.5.5", "framer-motion": "^5.5.5",
"graphql": "^16.2.0", "graphql": "^16.2.0",
"lodash": "^4.17.21",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",

View File

@@ -12,6 +12,7 @@ const queryClient = createClient({
credentials: 'include', credentials: 'include',
}; };
}, },
requestPolicy: 'network-only',
}); });
const theme = extendTheme({ const theme = extendTheme({

View File

@@ -0,0 +1,112 @@
import React from 'react';
import {
Button,
Center,
Flex,
MenuItem,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
useDisclosure,
Text,
useToast,
} from '@chakra-ui/react';
import { useClient } from 'urql';
import { FaRegTrashAlt } from 'react-icons/fa';
import { DeleteUser } from '../graphql/mutation';
import { capitalizeFirstLetter } from '../utils';
interface userDataTypes {
id: string;
email: string;
}
const DeleteUserModal = ({
user,
updateUserList,
}: {
user: userDataTypes;
updateUserList: Function;
}) => {
const client = useClient();
const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
const [userData, setUserData] = React.useState<userDataTypes>({
id: '',
email: '',
});
React.useEffect(() => {
setUserData(user);
}, []);
const deleteHandler = async () => {
const res = await client
.mutation(DeleteUser, { params: { email: userData.email } })
.toPromise();
if (res.error) {
toast({
title: capitalizeFirstLetter(res.error.message),
isClosable: true,
status: 'error',
position: 'bottom-right',
});
return;
} else if (res.data?._delete_user) {
toast({
title: capitalizeFirstLetter(res.data?._delete_user.message),
isClosable: true,
status: 'success',
position: 'bottom-right',
});
}
onClose();
updateUserList();
};
return (
<>
<MenuItem onClick={onOpen}>Delete User</MenuItem>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Delete User</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text fontSize="md">Are you sure?</Text>
<Flex
padding="5%"
marginTop="5%"
marginBottom="2%"
border="1px solid #ff7875"
borderRadius="5px"
flexDirection="column"
>
<Text fontSize="sm">
User <b>{user.email}</b> will be deleted permanently!
</Text>
</Flex>
</ModalBody>
<ModalFooter>
<Button
leftIcon={<FaRegTrashAlt />}
colorScheme="red"
variant="solid"
onClick={deleteHandler}
isDisabled={false}
>
<Center h="100%" pt="5%">
Delete
</Center>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};
export default DeleteUserModal;

View File

@@ -0,0 +1,250 @@
import React from 'react';
import {
Button,
Center,
Flex,
MenuItem,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Stack,
useDisclosure,
Text,
useToast,
} from '@chakra-ui/react';
import { useClient } from 'urql';
import { FaSave } from 'react-icons/fa';
import InputField from './InputField';
import {
ArrayInputType,
DateInputType,
SelectInputType,
TextInputType,
} from '../constants';
import { getObjectDiff } from '../utils';
import { UpdateUser } from '../graphql/mutation';
const GenderTypes = {
Undisclosed: null,
Male: 'Male',
Female: 'Female',
};
interface userDataTypes {
id: string;
email: string;
given_name: string;
family_name: string;
middle_name: string;
nickname: string;
gender: string;
birthdate: string;
phone_number: string;
picture: string;
roles: [string] | [];
}
const EditUserModal = ({
user,
updateUserList,
}: {
user: userDataTypes;
updateUserList: Function;
}) => {
const client = useClient();
const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
const [userData, setUserData] = React.useState<userDataTypes>({
id: '',
email: '',
given_name: '',
family_name: '',
middle_name: '',
nickname: '',
gender: '',
birthdate: '',
phone_number: '',
picture: '',
roles: [],
});
React.useEffect(() => {
setUserData(user);
}, []);
const saveHandler = async () => {
const diff = getObjectDiff(user, userData);
const updatedUserData = diff.reduce(
(acc: any, property: string) => ({
...acc,
// @ts-ignore
[property]: userData[property],
}),
{}
);
const res = await client
.mutation(UpdateUser, { params: { ...updatedUserData, id: userData.id } })
.toPromise();
if (res.error) {
toast({
title: 'User data update failed',
isClosable: true,
status: 'error',
position: 'bottom-right',
});
} else if (res.data?._update_user?.id) {
toast({
title: 'User data update successful',
isClosable: true,
status: 'success',
position: 'bottom-right',
});
}
onClose();
updateUserList();
};
return (
<>
<MenuItem onClick={onOpen}>Edit User Details</MenuItem>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Edit User Details</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Stack>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Given Name:</Text>
</Flex>
<Center w="70%">
<InputField
variables={userData}
setVariables={setUserData}
inputType={TextInputType.GIVEN_NAME}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Middle Name:</Text>
</Flex>
<Center w="70%">
<InputField
variables={userData}
setVariables={setUserData}
inputType={TextInputType.MIDDLE_NAME}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Family Name:</Text>
</Flex>
<Center w="70%">
<InputField
variables={userData}
setVariables={setUserData}
inputType={TextInputType.FAMILY_NAME}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Birth Date:</Text>
</Flex>
<Center w="70%">
<InputField
variables={userData}
setVariables={setUserData}
inputType={DateInputType.BIRTHDATE}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Nickname:</Text>
</Flex>
<Center w="70%">
<InputField
variables={userData}
setVariables={setUserData}
inputType={TextInputType.NICKNAME}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Gender:</Text>
</Flex>
<Center w="70%">
<InputField
variables={userData}
setVariables={setUserData}
inputType={SelectInputType.GENDER}
value={userData.gender}
options={GenderTypes}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Phone Number:</Text>
</Flex>
<Center w="70%">
<InputField
variables={userData}
setVariables={setUserData}
inputType={TextInputType.PHONE_NUMBER}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Picture:</Text>
</Flex>
<Center w="70%">
<InputField
variables={userData}
setVariables={setUserData}
inputType={TextInputType.PICTURE}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Roles:</Text>
</Flex>
<Center w="70%">
<InputField
variables={userData}
setVariables={setUserData}
inputType={ArrayInputType.USER_ROLES}
/>
</Center>
</Flex>
</Stack>
</ModalBody>
<ModalFooter>
<Button
leftIcon={<FaSave />}
colorScheme="blue"
variant="solid"
onClick={saveHandler}
isDisabled={false}
>
<Center h="100%" pt="5%">
Save
</Center>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};
export default EditUserModal;

View File

@@ -0,0 +1,338 @@
import React from 'react';
import {
Box,
Flex,
Input,
Center,
InputGroup,
InputRightElement,
Tag,
TagLabel,
TagRightIcon,
Select,
Textarea,
Switch,
Code,
} from '@chakra-ui/react';
import {
FaRegClone,
FaRegEye,
FaRegEyeSlash,
FaPlus,
FaTimes,
} from 'react-icons/fa';
import {
ArrayInputOperations,
ArrayInputType,
SelectInputType,
HiddenInputType,
TextInputType,
TextAreaInputType,
SwitchInputType,
DateInputType,
} from '../constants';
import { copyTextToClipboard } from '../utils';
const InputField = ({
inputType,
variables,
setVariables,
fieldVisibility,
setFieldVisibility,
...downshiftProps
}: any) => {
const props = {
size: 'sm',
...downshiftProps,
};
const [inputFieldVisibility, setInputFieldVisibility] = React.useState<
Record<string, boolean>
>({
ROLES: false,
DEFAULT_ROLES: false,
PROTECTED_ROLES: false,
ALLOWED_ORIGINS: false,
roles: false,
});
const [inputData, setInputData] = React.useState<Record<string, string>>({
ROLES: '',
DEFAULT_ROLES: '',
PROTECTED_ROLES: '',
ALLOWED_ORIGINS: '',
roles: '',
});
const updateInputHandler = (
type: string,
operation: any,
role: string = ''
) => {
if (operation === ArrayInputOperations.APPEND) {
if (inputData[type] !== '') {
setVariables({
...variables,
[type]: [...variables[type], inputData[type]],
});
setInputData({ ...inputData, [type]: '' });
}
setInputFieldVisibility({ ...inputFieldVisibility, [type]: false });
}
if (operation === ArrayInputOperations.REMOVE) {
let updatedEnvVars = variables[type].filter(
(item: string) => item !== role
);
setVariables({
...variables,
[type]: updatedEnvVars,
});
}
};
if (Object.values(TextInputType).includes(inputType)) {
return (
<InputGroup size="sm">
<Input
{...props}
value={variables[inputType] ? variables[inputType] : ''}
onChange={(
event: Event & {
target: HTMLInputElement;
}
) =>
setVariables({
...variables,
[inputType]: event.target.value,
})
}
/>
<InputRightElement
children={<FaRegClone color="#bfbfbf" />}
cursor="pointer"
onClick={() => copyTextToClipboard(variables[inputType])}
/>
</InputGroup>
);
}
if (Object.values(HiddenInputType).includes(inputType)) {
return (
<InputGroup size="sm">
<Input
{...props}
value={variables[inputType]}
onChange={(
event: Event & {
target: HTMLInputElement;
}
) =>
setVariables({
...variables,
[inputType]: event.target.value,
})
}
type={!fieldVisibility[inputType] ? 'password' : 'text'}
/>
<InputRightElement
right="15px"
children={
<Flex>
{fieldVisibility[inputType] ? (
<Center
w="25px"
margin="0 1.5%"
cursor="pointer"
onClick={() =>
setFieldVisibility({
...fieldVisibility,
[inputType]: false,
})
}
>
<FaRegEyeSlash color="#bfbfbf" />
</Center>
) : (
<Center
w="25px"
margin="0 1.5%"
cursor="pointer"
onClick={() =>
setFieldVisibility({
...fieldVisibility,
[inputType]: true,
})
}
>
<FaRegEye color="#bfbfbf" />
</Center>
)}
<Center
w="25px"
margin="0 1.5%"
cursor="pointer"
onClick={() => copyTextToClipboard(variables[inputType])}
>
<FaRegClone color="#bfbfbf" />
</Center>
</Flex>
}
/>
</InputGroup>
);
}
if (Object.values(ArrayInputType).includes(inputType)) {
return (
<Flex
border="1px solid #e2e8f0"
w="100%"
paddingTop="0.5%"
overflowX="scroll"
overflowY="hidden"
justifyContent="start"
alignItems="center"
>
{variables[inputType].map((role: string, index: number) => (
<Box key={index} margin="0.5%" role="group">
<Tag
size="sm"
variant="outline"
colorScheme="gray"
minW="fit-content"
>
<TagLabel cursor="default">{role}</TagLabel>
<TagRightIcon
boxSize="12px"
as={FaTimes}
display="none"
cursor="pointer"
_groupHover={{ display: 'block' }}
onClick={() =>
updateInputHandler(
inputType,
ArrayInputOperations.REMOVE,
role
)
}
/>
</Tag>
</Box>
))}
{inputFieldVisibility[inputType] ? (
<Box ml="1%" mb="0.75%">
<Input
type="text"
size="xs"
minW="150px"
placeholder="add a new value"
value={inputData[inputType]}
onChange={(e: any) => {
setInputData({ ...inputData, [inputType]: e.target.value });
}}
onBlur={() =>
updateInputHandler(inputType, ArrayInputOperations.APPEND)
}
onKeyPress={(event) => {
if (event.key === 'Enter') {
updateInputHandler(inputType, ArrayInputOperations.APPEND);
}
}}
/>
</Box>
) : (
<Box
marginLeft="0.5%"
cursor="pointer"
onClick={() =>
setInputFieldVisibility({
...inputFieldVisibility,
[inputType]: true,
})
}
>
<Tag
size="sm"
variant="outline"
colorScheme="gray"
minW="fit-content"
>
<FaPlus />
</Tag>
</Box>
)}
</Flex>
);
}
if (Object.values(SelectInputType).includes(inputType)) {
if (inputType === SelectInputType.JWT_TYPE) {
return (
<Select size="sm" {...props}>
{[variables[inputType]].map((value: string) => (
<option value="value" key={value}>
{value}
</option>
))}
</Select>
);
}
const { options, ...rest } = props;
return (
<Select
size="sm"
{...rest}
value={variables[inputType] ? variables[inputType] : ''}
onChange={(e) =>
setVariables({ ...variables, [inputType]: e.target.value })
}
>
{Object.entries(options).map(([key, value]: any) => (
<option value={value} key={key}>
{key}
</option>
))}
</Select>
);
}
if (Object.values(TextAreaInputType).includes(inputType)) {
return (
<Textarea
{...props}
size="lg"
value={inputData[inputType]}
onChange={(e: any) => {
setInputData({ ...inputData, [inputType]: e.target.value });
}}
/>
);
}
if (Object.values(SwitchInputType).includes(inputType)) {
return (
<Flex w="25%" justifyContent="space-between">
<Code h="75%">Off</Code>
<Switch
size="md"
isChecked={variables[inputType]}
onChange={() => {
setVariables({
...variables,
[inputType]: !variables[inputType],
});
}}
/>
<Code h="75%">On</Code>
</Flex>
);
}
if (Object.values(DateInputType).includes(inputType)) {
return (
<Flex border="1px solid #e2e8f0" w="100%" h="33px" padding="1%">
<input
type="date"
style={{ width: '100%', paddingLeft: '2.5%' }}
value={variables[inputType] ? variables[inputType] : ''}
onChange={(e) =>
setVariables({ ...variables, [inputType]: e.target.value })
}
/>
</Flex>
);
}
return null;
};
export default InputField;

View File

@@ -1,7 +1,6 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { import {
IconButton, IconButton,
Avatar,
Box, Box,
CloseButton, CloseButton,
Flex, Flex,
@@ -21,9 +20,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { import {
FiHome, FiHome,
FiTrendingUp, FiCode,
FiCompass,
FiStar,
FiSettings, FiSettings,
FiMenu, FiMenu,
FiUser, FiUser,
@@ -90,6 +87,17 @@ export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
</NavItem> </NavItem>
</NavLink> </NavLink>
))} ))}
<Link
href="/playground"
target="_blank"
style={{
textDecoration: 'none',
}}
_focus={{ _boxShadow: 'none' }}
>
<NavItem icon={FiCode}>API Playground</NavItem>
</Link>
</Box> </Box>
); );
}; };
@@ -100,37 +108,31 @@ interface NavItemProps extends FlexProps {
} }
export const NavItem = ({ icon, children, ...rest }: NavItemProps) => { export const NavItem = ({ icon, children, ...rest }: NavItemProps) => {
return ( return (
<Link <Flex
href="#" align="center"
style={{ textDecoration: 'none' }} p="3"
_focus={{ boxShadow: 'none' }} mx="3"
borderRadius="md"
role="group"
cursor="pointer"
_hover={{
bg: 'blue.500',
color: 'white',
}}
{...rest}
> >
<Flex {icon && (
align="center" <Icon
p="3" mr="4"
mx="3" fontSize="16"
borderRadius="md" _groupHover={{
role="group" color: 'white',
cursor="pointer" }}
_hover={{ as={icon}
bg: 'blue.500', />
color: 'white', )}
}} {children}
{...rest} </Flex>
>
{icon && (
<Icon
mr="4"
fontSize="16"
_groupHover={{
color: 'white',
}}
as={icon}
/>
)}
{children}
</Flex>
</Link>
); );
}; };
@@ -161,6 +163,7 @@ export const MobileNav = ({ onOpen, ...rest }: MobileProps) => {
borderBottomWidth="1px" borderBottomWidth="1px"
borderBottomColor={useColorModeValue('gray.200', 'gray.700')} borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
justifyContent={{ base: 'space-between', md: 'flex-end' }} justifyContent={{ base: 'space-between', md: 'flex-end' }}
zIndex={99}
{...rest} {...rest}
> >
<IconButton <IconButton

View File

@@ -1 +1,68 @@
export const LOGO_URL = "https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png" export const LOGO_URL =
'https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png';
export const TextInputType = {
GOOGLE_CLIENT_ID: 'GOOGLE_CLIENT_ID',
GITHUB_CLIENT_ID: 'GITHUB_CLIENT_ID',
FACEBOOK_CLIENT_ID: 'FACEBOOK_CLIENT_ID',
JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM',
REDIS_URL: 'REDIS_URL',
SMTP_HOST: 'SMTP_HOST',
SMTP_PORT: 'SMTP_PORT',
SMTP_USERNAME: 'SMTP_USERNAME',
SENDER_EMAIL: 'SENDER_EMAIL',
ORGANIZATION_NAME: 'ORGANIZATION_NAME',
ORGANIZATION_LOGO: 'ORGANIZATION_LOGO',
DATABASE_NAME: 'DATABASE_NAME',
DATABASE_TYPE: 'DATABASE_TYPE',
DATABASE_URL: 'DATABASE_URL',
GIVEN_NAME: 'given_name',
MIDDLE_NAME: 'middle_name',
FAMILY_NAME: 'family_name',
NICKNAME: 'nickname',
PHONE_NUMBER: 'phone_number',
PICTURE: 'picture',
};
export const HiddenInputType = {
GOOGLE_CLIENT_SECRET: 'GOOGLE_CLIENT_SECRET',
GITHUB_CLIENT_SECRET: 'GITHUB_CLIENT_SECRET',
FACEBOOK_CLIENT_SECRET: 'FACEBOOK_CLIENT_SECRET',
JWT_SECRET: 'JWT_SECRET',
SMTP_PASSWORD: 'SMTP_PASSWORD',
ADMIN_SECRET: 'ADMIN_SECRET',
OLD_ADMIN_SECRET: 'OLD_ADMIN_SECRET',
};
export const ArrayInputType = {
ROLES: 'ROLES',
DEFAULT_ROLES: 'DEFAULT_ROLES',
PROTECTED_ROLES: 'PROTECTED_ROLES',
ALLOWED_ORIGINS: 'ALLOWED_ORIGINS',
USER_ROLES: 'roles',
};
export const SelectInputType = {
JWT_TYPE: 'JWT_TYPE',
GENDER: 'gender',
};
export const TextAreaInputType = {
CUSTOM_ACCESS_TOKEN_SCRIPT: 'CUSTOM_ACCESS_TOKEN_SCRIPT',
};
export const SwitchInputType = {
DISABLE_LOGIN_PAGE: 'DISABLE_LOGIN_PAGE',
DISABLE_MAGIC_LINK_LOGIN: 'DISABLE_MAGIC_LINK_LOGIN',
DISABLE_EMAIL_VERIFICATION: 'DISABLE_EMAIL_VERIFICATION',
DISABLE_BASIC_AUTHENTICATION: 'DISABLE_BASIC_AUTHENTICATION',
};
export const DateInputType = {
BIRTHDATE: 'birthdate',
};
export const ArrayInputOperations = {
APPEND: 'APPEND',
REMOVE: 'REMOVE',
};

View File

@@ -32,7 +32,7 @@ export const AuthContextProvider = ({ children }: { children: any }) => {
if (fetching) { if (fetching) {
return ( return (
<Center> <Center h="100%">
<Spinner /> <Spinner />
</Center> </Center>
); );

View File

@@ -21,3 +21,27 @@ export const AdminLogout = `
} }
} }
`; `;
export const UpdateEnvVariables = `
mutation updateEnvVariables($params: UpdateEnvInput!) {
_update_env(params: $params) {
message
}
}
`;
export const UpdateUser = `
mutation updateUser($params: UpdateUserInput!) {
_update_user(params: $params) {
id
}
}
`;
export const DeleteUser = `
mutation deleteUser($params: DeleteUserInput!) {
_delete_user(params: $params) {
message
}
}
`;

View File

@@ -5,3 +5,69 @@ export const AdminSessionQuery = `
} }
} }
`; `;
export const EnvVariablesQuery = `
query {
_env{
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET,
FACEBOOK_CLIENT_ID,
FACEBOOK_CLIENT_SECRET,
ROLES,
DEFAULT_ROLES,
PROTECTED_ROLES,
JWT_TYPE,
JWT_SECRET,
JWT_ROLE_CLAIM,
REDIS_URL,
SMTP_HOST,
SMTP_PORT,
SMTP_USERNAME,
SMTP_PASSWORD,
SENDER_EMAIL,
ALLOWED_ORIGINS,
ORGANIZATION_NAME,
ORGANIZATION_LOGO,
ADMIN_SECRET,
DISABLE_LOGIN_PAGE,
DISABLE_MAGIC_LINK_LOGIN,
DISABLE_EMAIL_VERIFICATION,
DISABLE_BASIC_AUTHENTICATION,
CUSTOM_ACCESS_TOKEN_SCRIPT,
DATABASE_NAME,
DATABASE_TYPE,
DATABASE_URL,
}
}
`;
export const UserDetailsQuery = `
query($params: PaginatedInput) {
_users(params: $params) {
pagination {
limit
page
offset
total
}
users {
id
email
email_verified
given_name
family_name
middle_name
nickname
gender
birthdate
phone_number
picture
signup_methods
roles
created_at
}
}
}
`;

View File

@@ -1,35 +1,773 @@
import { Box, Divider, Flex } from '@chakra-ui/react'; import React, { useEffect } from 'react';
import React from 'react'; import {
Box,
Divider,
Flex,
Stack,
Center,
Text,
Button,
Input,
InputGroup,
InputRightElement,
useToast,
} from '@chakra-ui/react';
import { useClient } from 'urql';
import {
FaGoogle,
FaGithub,
FaFacebookF,
FaSave,
FaRegEyeSlash,
FaRegEye,
} from 'react-icons/fa';
import _ from 'lodash';
import InputField from '../components/InputField';
import { EnvVariablesQuery } from '../graphql/queries';
import {
ArrayInputType,
SelectInputType,
HiddenInputType,
TextInputType,
TextAreaInputType,
SwitchInputType,
} from '../constants';
import { UpdateEnvVariables } from '../graphql/mutation';
import { getObjectDiff, capitalizeFirstLetter } from '../utils';
interface envVarTypes {
GOOGLE_CLIENT_ID: string;
GOOGLE_CLIENT_SECRET: string;
GITHUB_CLIENT_ID: string;
GITHUB_CLIENT_SECRET: string;
FACEBOOK_CLIENT_ID: string;
FACEBOOK_CLIENT_SECRET: string;
ROLES: [string] | [];
DEFAULT_ROLES: [string] | [];
PROTECTED_ROLES: [string] | [];
JWT_TYPE: string;
JWT_SECRET: string;
JWT_ROLE_CLAIM: string;
REDIS_URL: string;
SMTP_HOST: string;
SMTP_PORT: string;
SMTP_USERNAME: string;
SMTP_PASSWORD: string;
SENDER_EMAIL: string;
ALLOWED_ORIGINS: [string] | [];
ORGANIZATION_NAME: string;
ORGANIZATION_LOGO: string;
CUSTOM_ACCESS_TOKEN_SCRIPT: string;
ADMIN_SECRET: string;
DISABLE_LOGIN_PAGE: boolean;
DISABLE_MAGIC_LINK_LOGIN: boolean;
DISABLE_EMAIL_VERIFICATION: boolean;
DISABLE_BASIC_AUTHENTICATION: boolean;
OLD_ADMIN_SECRET: string;
DATABASE_NAME: string;
DATABASE_TYPE: string;
DATABASE_URL: string;
}
// Don't allow changing database from here as it can cause persistence issues
export default function Environment() { export default function Environment() {
const client = useClient();
const toast = useToast();
const [adminSecret, setAdminSecret] = React.useState<
Record<string, string | boolean>
>({
value: '',
disableInputField: true,
});
const [loading, setLoading] = React.useState<boolean>(true);
const [envVariables, setEnvVariables] = React.useState<envVarTypes>({
GOOGLE_CLIENT_ID: '',
GOOGLE_CLIENT_SECRET: '',
GITHUB_CLIENT_ID: '',
GITHUB_CLIENT_SECRET: '',
FACEBOOK_CLIENT_ID: '',
FACEBOOK_CLIENT_SECRET: '',
ROLES: [],
DEFAULT_ROLES: [],
PROTECTED_ROLES: [],
JWT_TYPE: '',
JWT_SECRET: '',
JWT_ROLE_CLAIM: '',
REDIS_URL: '',
SMTP_HOST: '',
SMTP_PORT: '',
SMTP_USERNAME: '',
SMTP_PASSWORD: '',
SENDER_EMAIL: '',
ALLOWED_ORIGINS: [],
ORGANIZATION_NAME: '',
ORGANIZATION_LOGO: '',
CUSTOM_ACCESS_TOKEN_SCRIPT: '',
ADMIN_SECRET: '',
DISABLE_LOGIN_PAGE: false,
DISABLE_MAGIC_LINK_LOGIN: false,
DISABLE_EMAIL_VERIFICATION: false,
DISABLE_BASIC_AUTHENTICATION: false,
OLD_ADMIN_SECRET: '',
DATABASE_NAME: '',
DATABASE_TYPE: '',
DATABASE_URL: '',
});
const [fieldVisibility, setFieldVisibility] = React.useState<
Record<string, boolean>
>({
GOOGLE_CLIENT_SECRET: false,
GITHUB_CLIENT_SECRET: false,
FACEBOOK_CLIENT_SECRET: false,
JWT_SECRET: false,
SMTP_PASSWORD: false,
ADMIN_SECRET: false,
OLD_ADMIN_SECRET: false,
});
useEffect(() => {
let isMounted = true;
async function getData() {
const {
data: { _env: envData },
} = await client.query(EnvVariablesQuery).toPromise();
if (isMounted) {
setLoading(false);
setEnvVariables({
...envData,
OLD_ADMIN_SECRET: envData.ADMIN_SECRET,
ADMIN_SECRET: '',
});
setAdminSecret({
value: '',
disableInputField: true,
});
}
}
getData();
return () => {
isMounted = false;
};
}, []);
const validateAdminSecretHandler = (event: any) => {
if (envVariables.OLD_ADMIN_SECRET === event.target.value) {
setAdminSecret({
...adminSecret,
value: event.target.value,
disableInputField: false,
});
} else {
setAdminSecret({
...adminSecret,
value: event.target.value,
disableInputField: true,
});
}
if (envVariables.ADMIN_SECRET !== '') {
setEnvVariables({ ...envVariables, ADMIN_SECRET: '' });
}
};
const saveHandler = async () => {
setLoading(true);
const {
data: { _env: envData },
} = await client.query(EnvVariablesQuery).toPromise();
const diff = getObjectDiff(envVariables, envData);
const updatedEnvVariables = diff.reduce(
(acc: any, property: string) => ({
...acc,
// @ts-ignore
[property]: envVariables[property],
}),
{}
);
if (
updatedEnvVariables[HiddenInputType.ADMIN_SECRET] === '' ||
updatedEnvVariables[HiddenInputType.OLD_ADMIN_SECRET] !==
envData.ADMIN_SECRET
) {
delete updatedEnvVariables.OLD_ADMIN_SECRET;
delete updatedEnvVariables.ADMIN_SECRET;
}
delete updatedEnvVariables.DATABASE_URL;
delete updatedEnvVariables.DATABASE_TYPE;
delete updatedEnvVariables.DATABASE_NAME;
const res = await client
.mutation(UpdateEnvVariables, { params: updatedEnvVariables })
.toPromise();
setLoading(false);
if (res.error) {
toast({
title: capitalizeFirstLetter(res.error.message),
isClosable: true,
status: 'error',
position: 'bottom-right',
});
return;
}
setAdminSecret({
value: '',
disableInputField: true,
});
toast({
title: `Successfully updated ${
Object.keys(updatedEnvVariables).length
} variables`,
isClosable: true,
status: 'success',
position: 'bottom-right',
});
};
return ( return (
<Box m="5" p="5" bg="white" rounded="md"> <Box m="5" py="5" px="10" bg="white" rounded="md">
<h1>Social Media Logins</h1> <Text fontSize="md" paddingTop="2%" fontWeight="bold">
<Divider />- Add horizontal input for clientID and secret for - Google - Social Media Logins
Github - Facebook </Text>
<h1>Roles</h1> <Stack spacing={6} padding="2% 0%">
<Divider />- Add tagged input for roles, default roles, and protected <Flex>
roles <Center
<h1>JWT Configurations</h1> w="50px"
<Divider />- Add input for JWT Type (keep this disabled for now with marginRight="1.5%"
notice saying, "More JWT types will be enabled in upcoming releases"),JWT border="1px solid #e2e8f0"
secret, JWT role claim borderRadius="5px"
<h1>Session Storage</h1> >
<Divider />- Add input for redis url <FaGoogle style={{ color: '#8c8c8c' }} />
<h1>Email Configurations</h1> </Center>
<Divider />- Add input for SMTP Host, PORT, Username, Password, From <Center w="45%" marginRight="1.5%">
Email, <InputField
<h1>White Listing</h1> variables={envVariables}
<Divider />- Add input for allowed origins setVariables={setEnvVariables}
<h1>Organization Information</h1> inputType={TextInputType.GOOGLE_CLIENT_ID}
<Divider />- Add input for organization name, and logo placeholder="Google Client ID"
<h1>Custom Scripts</h1> />
<Divider />- For now add text area input for CUSTOM_ACCESS_TOKEN_SCRIPT </Center>
<h1>Disable Features</h1> <Center w="45%">
<Divider /> <InputField
<h1>Danger</h1> variables={envVariables}
<Divider />- Include changing admin secret setVariables={setEnvVariables}
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.GOOGLE_CLIENT_SECRET}
placeholder="Google Secret"
/>
</Center>
</Flex>
<Flex>
<Center
w="50px"
marginRight="1.5%"
border="1px solid #e2e8f0"
borderRadius="5px"
>
<FaGithub style={{ color: '#8c8c8c' }} />
</Center>
<Center w="45%" marginRight="1.5%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.GITHUB_CLIENT_ID}
placeholder="Github Client ID"
/>
</Center>
<Center w="45%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.GITHUB_CLIENT_SECRET}
placeholder="Github Secret"
/>
</Center>
</Flex>
<Flex>
<Center
w="50px"
marginRight="1.5%"
border="1px solid #e2e8f0"
borderRadius="5px"
>
<FaFacebookF style={{ color: '#8c8c8c' }} />
</Center>
<Center w="45%" marginRight="1.5%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.FACEBOOK_CLIENT_ID}
placeholder="Facebook Client ID"
/>
</Center>
<Center w="45%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.FACEBOOK_CLIENT_SECRET}
placeholder="Facebook Secret"
/>
</Center>
</Flex>
</Stack>
<Divider marginTop="2%" marginBottom="2%" />
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
Roles
</Text>
<Stack spacing={6} padding="2% 0%">
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Roles:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={ArrayInputType.ROLES}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Default Roles:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={ArrayInputType.DEFAULT_ROLES}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Protected Roles:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={ArrayInputType.PROTECTED_ROLES}
/>
</Center>
</Flex>
</Stack>
<Divider marginTop="2%" marginBottom="2%" />
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
JWT (JSON Web Tokens) Configurations
</Text>
<Stack spacing={6} padding="2% 0%">
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">JWT Type:</Text>
</Flex>
<Center w="70%">
<Flex w="100%" justifyContent="space-between">
<Flex flex="2">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={SelectInputType.JWT_TYPE}
isDisabled={true}
defaultValue={SelectInputType.JWT_TYPE}
/>
</Flex>
<Flex flex="3" justifyContent="center" alignItems="center">
<Text fontSize="sm">
More JWT types will be enabled in upcoming releases.
</Text>
</Flex>
</Flex>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">JWT Secret</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.JWT_SECRET}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">JWT Role Claim:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.JWT_ROLE_CLAIM}
/>
</Center>
</Flex>
</Stack>
<Divider marginTop="2%" marginBottom="2%" />
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
Session Storage
</Text>
<Stack spacing={6} padding="2% 0%">
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Redis URL:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.REDIS_URL}
/>
</Center>
</Flex>
</Stack>
<Divider marginTop="2%" marginBottom="2%" />
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
Email Configurations
</Text>
<Stack spacing={6} padding="2% 0%">
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">SMTP Host:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.SMTP_HOST}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Port:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.SMTP_PORT}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Username:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.SMTP_USERNAME}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Password:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.SMTP_PASSWORD}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">From Email:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.SENDER_EMAIL}
/>
</Center>
</Flex>
</Stack>
<Divider marginTop="2%" marginBottom="2%" />
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
White Listing
</Text>
<Stack spacing={6} padding="2% 0%">
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Allowed Origins:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={ArrayInputType.ALLOWED_ORIGINS}
/>
</Center>
</Flex>
</Stack>
<Divider marginTop="2%" marginBottom="2%" />
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
Organization Information
</Text>
<Stack spacing={6} padding="2% 0%">
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Organization Name:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.ORGANIZATION_NAME}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Organization Logo:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.ORGANIZATION_LOGO}
/>
</Center>
</Flex>
</Stack>
<Divider marginTop="2%" marginBottom="2%" />
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
Custom Scripts
</Text>
<Stack spacing={6} padding="2% 0%">
<Flex>
<Center w="100%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextAreaInputType.CUSTOM_ACCESS_TOKEN_SCRIPT}
placeholder="Add script here"
minH="25vh"
/>
</Center>
</Flex>
</Stack>
<Divider marginTop="2%" marginBottom="2%" />
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
Disable Features
</Text>
<Stack spacing={6} padding="2% 0%">
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Disable Login Page:</Text>
</Flex>
<Flex justifyContent="start" w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={SwitchInputType.DISABLE_LOGIN_PAGE}
/>
</Flex>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Disable Email Verification:</Text>
</Flex>
<Flex justifyContent="start" w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={SwitchInputType.DISABLE_EMAIL_VERIFICATION}
/>
</Flex>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Disable Magic Login Link:</Text>
</Flex>
<Flex justifyContent="start" w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={SwitchInputType.DISABLE_MAGIC_LINK_LOGIN}
/>
</Flex>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Disable Basic Authentication:</Text>
</Flex>
<Flex justifyContent="start" w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={SwitchInputType.DISABLE_BASIC_AUTHENTICATION}
/>
</Flex>
</Flex>
</Stack>
<Divider marginTop="2%" marginBottom="2%" />
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
Danger
</Text>
<Stack
spacing={6}
padding="0 5%"
marginTop="3%"
border="1px solid #ff7875"
borderRadius="5px"
>
<Stack spacing={6} padding="3% 0">
<Text fontStyle="italic" fontSize="sm" color="gray.600">
Note: Database related environment variables cannot be updated from
dashboard :(
</Text>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">DataBase Name:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.DATABASE_NAME}
isDisabled={true}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">DataBase Type:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.DATABASE_TYPE}
isDisabled={true}
/>
</Center>
</Flex>
<Flex>
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">DataBase URL:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={TextInputType.DATABASE_URL}
isDisabled={true}
/>
</Center>
</Flex>
</Stack>
<Flex marginTop="3%">
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Old Admin Secret:</Text>
</Flex>
<Center w="70%">
<InputGroup size="sm">
<Input
size="sm"
placeholder="Enter Old Admin Secret"
value={adminSecret.value as string}
onChange={(event: any) => validateAdminSecretHandler(event)}
type={
!fieldVisibility[HiddenInputType.OLD_ADMIN_SECRET]
? 'password'
: 'text'
}
/>
<InputRightElement
right="5px"
children={
<Flex>
{fieldVisibility[HiddenInputType.OLD_ADMIN_SECRET] ? (
<Center
w="25px"
margin="0 1.5%"
cursor="pointer"
onClick={() =>
setFieldVisibility({
...fieldVisibility,
[HiddenInputType.OLD_ADMIN_SECRET]: false,
})
}
>
<FaRegEyeSlash color="#bfbfbf" />
</Center>
) : (
<Center
w="25px"
margin="0 1.5%"
cursor="pointer"
onClick={() =>
setFieldVisibility({
...fieldVisibility,
[HiddenInputType.OLD_ADMIN_SECRET]: true,
})
}
>
<FaRegEye color="#bfbfbf" />
</Center>
)}
</Flex>
}
/>
</InputGroup>
</Center>
</Flex>
<Flex paddingBottom="3%">
<Flex w="30%" justifyContent="start" alignItems="center">
<Text fontSize="sm">New Admin Secret:</Text>
</Flex>
<Center w="70%">
<InputField
variables={envVariables}
setVariables={setEnvVariables}
inputType={HiddenInputType.ADMIN_SECRET}
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
isDisabled={adminSecret.disableInputField}
placeholder="Enter New Admin Secret"
/>
</Center>
</Flex>
</Stack>
<Divider marginTop="5%" marginBottom="2%" />
<Stack spacing={6} padding="1% 0">
<Flex justifyContent="end" alignItems="center">
<Button
leftIcon={<FaSave />}
colorScheme="blue"
variant="solid"
onClick={saveHandler}
isDisabled={loading}
>
Save
</Button>
</Flex>
</Stack>
</Box> </Box>
); );
} }

View File

@@ -1,6 +1,400 @@
import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useClient } from 'urql';
import dayjs from 'dayjs';
import {
Box,
Flex,
IconButton,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Select,
Table,
Tag,
Tbody,
Td,
Text,
TableCaption,
Th,
Thead,
Tooltip,
Tr,
Button,
Center,
Menu,
MenuButton,
MenuList,
MenuItem,
useToast,
Spinner,
} from '@chakra-ui/react';
import {
FaAngleLeft,
FaAngleRight,
FaAngleDoubleLeft,
FaAngleDoubleRight,
FaExclamationCircle,
FaAngleDown,
} from 'react-icons/fa';
import { UserDetailsQuery } from '../graphql/queries';
import { UpdateUser } from '../graphql/mutation';
import EditUserModal from '../components/EditUserModal';
import DeleteUserModal from '../components/DeleteUserModal';
interface paginationPropTypes {
limit: number;
page: number;
offset: number;
total: number;
maxPages: number;
}
interface userDataTypes {
id: string;
email: string;
email_verified: boolean;
given_name: string;
family_name: string;
middle_name: string;
nickname: string;
gender: string;
birthdate: string;
phone_number: string;
picture: string;
signup_methods: string;
roles: [string];
created_at: number;
}
const getMaxPages = (pagination: paginationPropTypes) => {
const { limit, total } = pagination;
if (total > 1) {
return total % limit === 0
? total / limit
: parseInt(`${total / limit}`) + 1;
} else return 1;
};
const getLimits = (pagination: paginationPropTypes) => {
const { total } = pagination;
const limits = [5];
if (total > 10) {
for (let i = 10; i <= total && limits.length <= 10; i += 5) {
limits.push(i);
}
}
return limits;
};
export default function Users() { export default function Users() {
return <Box>Welcome to Users Page</Box>; const client = useClient();
const toast = useToast();
const [paginationProps, setPaginationProps] =
React.useState<paginationPropTypes>({
limit: 5,
page: 1,
offset: 0,
total: 0,
maxPages: 1,
});
const [userList, setUserList] = React.useState<userDataTypes[]>([]);
const [loading, setLoading] = React.useState<boolean>(false);
const updateUserList = async () => {
setLoading(true);
const { data } = await client
.query(UserDetailsQuery, {
params: {
pagination: {
limit: paginationProps.limit,
page: paginationProps.page,
},
},
})
.toPromise();
if (data?._users) {
const { pagination, users } = data._users;
const maxPages = getMaxPages(pagination);
if (users && users.length > 0) {
setPaginationProps({ ...paginationProps, ...pagination, maxPages });
setUserList(users);
} else {
if (paginationProps.page !== 1) {
setPaginationProps({
...paginationProps,
...pagination,
maxPages,
page: 1,
});
}
}
}
setLoading(false);
};
React.useEffect(() => {
updateUserList();
}, []);
React.useEffect(() => {
updateUserList();
}, [paginationProps.page, paginationProps.limit]);
const paginationHandler = (value: Record<string, number>) => {
setPaginationProps({ ...paginationProps, ...value });
};
const userVerificationHandler = async (user: userDataTypes) => {
const { id, email } = user;
const res = await client
.mutation(UpdateUser, {
params: {
id,
email,
email_verified: true,
},
})
.toPromise();
if (res.error) {
toast({
title: 'User verification failed',
isClosable: true,
status: 'error',
position: 'bottom-right',
});
} else if (res.data?._update_user?.id) {
toast({
title: 'User verification successful',
isClosable: true,
status: 'success',
position: 'bottom-right',
});
}
updateUserList();
};
return (
<Box m="5" py="5" px="10" bg="white" rounded="md">
<Flex margin="2% 0" justifyContent="space-between" alignItems="center">
<Text fontSize="md" fontWeight="bold">
Users
</Text>
</Flex>
{!loading ? (
userList.length > 0 ? (
<Table variant="simple">
<Thead>
<Tr>
<Th>Email</Th>
<Th>Created At</Th>
<Th>Signup Methods</Th>
<Th>Roles</Th>
<Th>Verified</Th>
<Th>Actions</Th>
</Tr>
</Thead>
<Tbody>
{userList.map((user: userDataTypes) => {
const { email_verified, created_at, ...rest }: any = user;
return (
<Tr key={user.id} style={{ fontSize: 14 }}>
<Td>{user.email}</Td>
<Td>
{dayjs(user.created_at * 1000).format('MMM DD, YYYY')}
</Td>
<Td>{user.signup_methods}</Td>
<Td>{user.roles.join(', ')}</Td>
<Td>
<Tag
size="sm"
variant="outline"
colorScheme={user.email_verified ? 'green' : 'yellow'}
>
{user.email_verified.toString()}
</Tag>
</Td>
<Td>
<Menu>
<MenuButton as={Button} variant="unstyled" size="sm">
<Flex
justifyContent="space-between"
alignItems="center"
>
<Text fontSize="sm" fontWeight="light">
Menu
</Text>
<FaAngleDown style={{ marginLeft: 10 }} />
</Flex>
</MenuButton>
<MenuList>
{!user.email_verified && (
<MenuItem
onClick={() => userVerificationHandler(user)}
>
Verify User
</MenuItem>
)}
<EditUserModal
user={rest}
updateUserList={updateUserList}
/>
<DeleteUserModal
user={rest}
updateUserList={updateUserList}
/>
</MenuList>
</Menu>
</Td>
</Tr>
);
})}
</Tbody>
{(paginationProps.maxPages > 1 || paginationProps.total >= 5) && (
<TableCaption>
<Flex
justifyContent="space-between"
alignItems="center"
m="2% 0"
>
<Flex flex="1">
<Tooltip label="First Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: 1,
})
}
isDisabled={paginationProps.page <= 1}
mr={4}
icon={<FaAngleDoubleLeft />}
/>
</Tooltip>
<Tooltip label="Previous Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: paginationProps.page - 1,
})
}
isDisabled={paginationProps.page <= 1}
icon={<FaAngleLeft />}
/>
</Tooltip>
</Flex>
<Flex
flex="8"
justifyContent="space-evenly"
alignItems="center"
>
<Text mr={8}>
Page{' '}
<Text fontWeight="bold" as="span">
{paginationProps.page}
</Text>{' '}
of{' '}
<Text fontWeight="bold" as="span">
{paginationProps.maxPages}
</Text>
</Text>
<Flex alignItems="center">
<Text flexShrink="0">Go to page:</Text>{' '}
<NumberInput
ml={2}
mr={8}
w={28}
min={1}
max={paginationProps.maxPages}
onChange={(value) =>
paginationHandler({
page: parseInt(value),
})
}
value={paginationProps.page}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</Flex>
<Select
w={32}
value={paginationProps.limit}
onChange={(e) =>
paginationHandler({
page: 1,
limit: parseInt(e.target.value),
})
}
>
{getLimits(paginationProps).map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</Select>
</Flex>
<Flex flex="1">
<Tooltip label="Next Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: paginationProps.page + 1,
})
}
isDisabled={
paginationProps.page >= paginationProps.maxPages
}
icon={<FaAngleRight />}
/>
</Tooltip>
<Tooltip label="Last Page">
<IconButton
aria-label="icon button"
onClick={() =>
paginationHandler({
page: paginationProps.maxPages,
})
}
isDisabled={
paginationProps.page >= paginationProps.maxPages
}
ml={4}
icon={<FaAngleDoubleRight />}
/>
</Tooltip>
</Flex>
</Flex>
</TableCaption>
)}
</Table>
) : (
<Flex
flexDirection="column"
minH="25vh"
justifyContent="center"
alignItems="center"
>
<Center w="50px" marginRight="1.5%">
<FaExclamationCircle style={{ color: '#f0f0f0', fontSize: 70 }} />
</Center>
<Text
fontSize="2xl"
paddingRight="1%"
fontWeight="bold"
color="#d9d9d9"
>
No Data
</Text>
</Flex>
)
) : (
<Center minH="25vh">
<Spinner />
</Center>
)}
</Box>
);
} }

View File

@@ -26,6 +26,7 @@ export const AppRoutes = () => {
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="users" element={<Users />} /> <Route path="users" element={<Users />} />
<Route path="environment" element={<Environment />} /> <Route path="environment" element={<Environment />} />
<Route path="*" element={<Home />} />
</Route> </Route>
</Routes> </Routes>
</Suspense> </Suspense>
@@ -35,6 +36,7 @@ export const AppRoutes = () => {
<Suspense fallback={<></>}> <Suspense fallback={<></>}>
<Routes> <Routes>
<Route path="/" element={<Auth />} /> <Route path="/" element={<Auth />} />
<Route path="*" element={<Auth />} />
</Routes> </Routes>
</Suspense> </Suspense>
); );

View File

@@ -1,6 +1,66 @@
import _ from 'lodash';
export const hasAdminSecret = () => { export const hasAdminSecret = () => {
return (<any>window)['__authorizer__'].isOnboardingCompleted === true; return (<any>window)['__authorizer__'].isOnboardingCompleted === true;
}; };
export const capitalizeFirstLetter = (data: string): string => export const capitalizeFirstLetter = (data: string): string =>
data.charAt(0).toUpperCase() + data.slice(1); data.charAt(0).toUpperCase() + data.slice(1);
const fallbackCopyTextToClipboard = (text: string) => {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
const msg = successful ? 'successful' : 'unsuccessful';
console.log('Fallback: Copying text command was ' + msg);
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
}
document.body.removeChild(textArea);
};
export const copyTextToClipboard = (text: string) => {
if (!navigator.clipboard) {
fallbackCopyTextToClipboard(text);
return;
}
navigator.clipboard.writeText(text).then(
() => {
console.log('Async: Copying to clipboard was successful!');
},
(err) => {
console.error('Async: Could not copy text: ', err);
}
);
};
export const getObjectDiff = (obj1: any, obj2: any) => {
const diff = Object.keys(obj1).reduce((result, key) => {
if (!obj2.hasOwnProperty(key)) {
result.push(key);
} else if (
_.isEqual(obj1[key], obj2[key]) ||
(obj1[key] === null && obj2[key] === '') ||
(obj1[key] &&
Array.isArray(obj1[key]) &&
obj1[key].length === 0 &&
obj2[key] === null)
) {
const resultKeyIndex = result.indexOf(key);
result.splice(resultKeyIndex, 1);
}
return result;
}, Object.keys(obj2));
return diff;
};

View File

@@ -67,6 +67,7 @@
/* Advanced Options */ /* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */, "skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
"lib": ["esnext", "dom"]
} }
} }

View File

@@ -16,6 +16,7 @@ const (
// EnvKeyVersion key for build arg version // EnvKeyVersion key for build arg version
EnvKeyVersion = "VERSION" EnvKeyVersion = "VERSION"
// EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL // EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL
// TODO: remove support AUTHORIZER_URL env
EnvKeyAuthorizerURL = "AUTHORIZER_URL" EnvKeyAuthorizerURL = "AUTHORIZER_URL"
// EnvKeyPort key for env variable PORT // EnvKeyPort key for env variable PORT
EnvKeyPort = "PORT" EnvKeyPort = "PORT"

View File

@@ -0,0 +1,4 @@
package constants
// DefaultLimit is the default limit for pagination
var DefaultLimit = 10

View File

@@ -0,0 +1,46 @@
package cookie
import (
"net/url"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin"
)
// SetAdminCookie sets the admin cookie in the response
func SetAdminCookie(gc *gin.Context, token string) {
secure := true
httpOnly := true
hostname := utils.GetHost(gc)
host, _ := utils.GetHostParts(hostname)
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), token, 3600, "/", host, secure, httpOnly)
}
// GetAdminCookie gets the admin cookie from the request
func GetAdminCookie(gc *gin.Context) (string, error) {
cookie, err := gc.Request.Cookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName))
if err != nil {
return "", err
}
// cookie escapes special characters like $
// hence we need to unescape before comparing
decodedValue, err := url.QueryUnescape(cookie.Value)
if err != nil {
return "", err
}
return decodedValue, nil
}
// DeleteAdminCookie sets the response cookie to empty
func DeleteAdminCookie(gc *gin.Context) {
secure := true
httpOnly := true
hostname := utils.GetHost(gc)
host, _ := utils.GetHostParts(hostname)
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), "", -1, "/", host, secure, httpOnly)
}

102
server/cookie/cookie.go Normal file
View File

@@ -0,0 +1,102 @@
package cookie
import (
"net/http"
"net/url"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin"
)
// SetCookie sets the cookie in the response. It sets 4 cookies
// 1 COOKIE_NAME.access_token jwt token for the host (temp.abc.com)
// 2 COOKIE_NAME.access_token.domain jwt token for the domain (abc.com).
// 3 COOKIE_NAME.fingerprint fingerprint hash for the refresh token verification.
// 4 COOKIE_NAME.refresh_token refresh token
// Note all sites don't allow 2nd type of cookie
func SetCookie(gc *gin.Context, accessToken, refreshToken, fingerprintHash string) {
secure := true
httpOnly := true
hostname := utils.GetHost(gc)
host, _ := utils.GetHostParts(hostname)
domain := utils.GetDomainName(hostname)
if domain != "localhost" {
domain = "." + domain
}
year := 60 * 60 * 24 * 365
thirtyMin := 60 * 30
gc.SetSameSite(http.SameSiteNoneMode)
// set cookie for host
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+".access_token", accessToken, thirtyMin, "/", host, secure, httpOnly)
// in case of subdomain, set cookie for domain
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+".access_token.domain", accessToken, thirtyMin, "/", domain, secure, httpOnly)
// set finger print cookie (this should be accessed via cookie only)
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+".fingerprint", fingerprintHash, year, "/", host, secure, httpOnly)
// set refresh token cookie (this should be accessed via cookie only)
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+".refresh_token", refreshToken, year, "/", host, secure, httpOnly)
}
// GetAccessTokenCookie to get access token cookie from the request
func GetAccessTokenCookie(gc *gin.Context) (string, error) {
cookie, err := gc.Request.Cookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName) + ".access_token")
if err != nil {
cookie, err = gc.Request.Cookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName) + ".access_token.domain")
if err != nil {
return "", err
}
}
return cookie.Value, nil
}
// GetRefreshTokenCookie to get refresh token cookie
func GetRefreshTokenCookie(gc *gin.Context) (string, error) {
cookie, err := gc.Request.Cookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName) + ".refresh_token")
if err != nil {
return "", err
}
return cookie.Value, nil
}
// GetFingerPrintCookie to get fingerprint cookie
func GetFingerPrintCookie(gc *gin.Context) (string, error) {
cookie, err := gc.Request.Cookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName) + ".fingerprint")
if err != nil {
return "", err
}
// cookie escapes special characters like $
// hence we need to unescape before comparing
decodedValue, err := url.QueryUnescape(cookie.Value)
if err != nil {
return "", err
}
return decodedValue, nil
}
// DeleteCookie sets response cookies to expire
func DeleteCookie(gc *gin.Context) {
secure := true
httpOnly := true
hostname := utils.GetHost(gc)
host, _ := utils.GetHostParts(hostname)
domain := utils.GetDomainName(hostname)
if domain != "localhost" {
domain = "." + domain
}
gc.SetSameSite(http.SameSiteNoneMode)
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+".access_token", "", -1, "/", host, secure, httpOnly)
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+".access_token.domain", "", -1, "/", domain, secure, httpOnly)
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+".fingerprint", "", -1, "/", host, secure, httpOnly)
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+".refresh_token", "", -1, "/", host, secure, httpOnly)
}

View File

@@ -4,8 +4,8 @@ package models
type Env struct { type Env struct {
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"` ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
EnvData []byte `gorm:"type:text" json:"env" bson:"env"` EnvData string `gorm:"type:text" json:"env" bson:"env"`
Hash string `gorm:"type:hash" json:"hash" bson:"hash"` Hash string `gorm:"type:text" json:"hash" bson:"hash"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"` CreatedAt int64 `json:"created_at" bson:"created_at"`
} }

View File

@@ -5,9 +5,9 @@ type Session struct {
Key string `json:"_key,omitempty" bson:"_key,omitempty"` // for arangodb Key string `json:"_key,omitempty" bson:"_key,omitempty"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"` ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
UserID string `gorm:"type:char(36),index:" json:"user_id" bson:"user_id"` UserID string `gorm:"type:char(36),index:" json:"user_id" bson:"user_id"`
User User `json:"-" bson:"-"` User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" bson:"-"`
UserAgent string `json:"user_agent" bson:"user_agent"` UserAgent string `json:"user_agent" bson:"user_agent"`
IP string `json:"ip" bson:"ip"` IP string `json:"ip" bson:"ip"`
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"` CreatedAt int64 `json:"created_at" bson:"created_at"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
} }

View File

@@ -1,5 +1,11 @@
package models package models
import (
"strings"
"github.com/authorizerdev/authorizer/server/graph/model"
)
// User model for db // User model for db
type User struct { type User struct {
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
@@ -19,6 +25,30 @@ type User struct {
PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at"` PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at"`
Picture *string `gorm:"type:text" json:"picture" bson:"picture"` Picture *string `gorm:"type:text" json:"picture" bson:"picture"`
Roles string `json:"roles" bson:"roles"` Roles string `json:"roles" bson:"roles"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"` CreatedAt int64 `json:"created_at" bson:"created_at"`
}
func (user *User) AsAPIUser() *model.User {
isEmailVerified := user.EmailVerifiedAt != nil
isPhoneVerified := user.PhoneNumberVerifiedAt != nil
return &model.User{
ID: user.ID,
Email: user.Email,
EmailVerified: isEmailVerified,
SignupMethods: user.SignupMethods,
GivenName: user.GivenName,
FamilyName: user.FamilyName,
MiddleName: user.MiddleName,
Nickname: user.Nickname,
PreferredUsername: &user.Email,
Gender: user.Gender,
Birthdate: user.Birthdate,
PhoneNumber: user.PhoneNumber,
PhoneNumberVerified: &isPhoneVerified,
Picture: user.Picture,
Roles: strings.Split(user.Roles, ","),
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
}
} }

View File

@@ -1,5 +1,7 @@
package models package models
import "github.com/authorizerdev/authorizer/server/graph/model"
// VerificationRequest model for db // VerificationRequest model for db
type VerificationRequest struct { type VerificationRequest struct {
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
@@ -7,7 +9,19 @@ type VerificationRequest struct {
Token string `gorm:"type:text" json:"token" bson:"token"` Token string `gorm:"type:text" json:"token" bson:"token"`
Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"` Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"`
ExpiresAt int64 `json:"expires_at" bson:"expires_at"` ExpiresAt int64 `json:"expires_at" bson:"expires_at"`
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"` CreatedAt int64 `json:"created_at" bson:"created_at"`
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"` Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
} }
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
return &model.VerificationRequest{
ID: v.ID,
Token: &v.Token,
Identifier: &v.Identifier,
Expires: &v.ExpiresAt,
CreatedAt: &v.CreatedAt,
UpdatedAt: &v.UpdatedAt,
Email: &v.Email,
}
}

View File

@@ -1,6 +1,7 @@
package arangodb package arangodb
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"strings" "strings"
@@ -11,6 +12,7 @@ import (
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -66,32 +68,40 @@ func (p *provider) DeleteUser(user models.User) error {
} }
// ListUsers to get list of users from database // ListUsers to get list of users from database
func (p *provider) ListUsers() ([]models.User, error) { func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
var users []models.User var users []*model.User
query := fmt.Sprintf("FOR d in %s RETURN d", models.Collections.User) ctx := driver.WithQueryFullCount(context.Background())
cursor, err := p.db.Query(nil, query, nil) query := fmt.Sprintf("FOR d in %s SORT d.created_at DESC LIMIT %d, %d RETURN d", models.Collections.User, pagination.Offset, pagination.Limit)
cursor, err := p.db.Query(ctx, query, nil)
if err != nil { if err != nil {
return users, err return nil, err
} }
defer cursor.Close() defer cursor.Close()
paginationClone := pagination
paginationClone.Total = cursor.Statistics().FullCount()
for { for {
var user models.User var user models.User
meta, err := cursor.ReadDocument(nil, &user) meta, err := cursor.ReadDocument(nil, &user)
if driver.IsNoMoreDocuments(err) { if arangoDriver.IsNoMoreDocuments(err) {
break break
} else if err != nil { } else if err != nil {
return users, err return nil, err
} }
if meta.Key != "" { if meta.Key != "" {
users = append(users, user) users = append(users, user.AsAPIUser())
} }
} }
return users, nil return &model.Users{
Pagination: &paginationClone,
Users: users,
}, nil
} }
// GetUserByEmail to get user information from database using email address // GetUserByEmail to get user information from database using email address

View File

@@ -1,12 +1,14 @@
package arangodb package arangodb
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"time" "time"
"github.com/arangodb/go-driver" "github.com/arangodb/go-driver"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -93,17 +95,20 @@ func (p *provider) GetVerificationRequestByEmail(email string, identifier string
} }
// ListVerificationRequests to get list of verification requests from database // ListVerificationRequests to get list of verification requests from database
func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, error) { func (p *provider) ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error) {
var verificationRequests []models.VerificationRequest var verificationRequests []*model.VerificationRequest
ctx := driver.WithQueryFullCount(context.Background())
query := fmt.Sprintf("FOR d in %s SORT d.created_at DESC LIMIT %d, %d RETURN d", models.Collections.VerificationRequest, pagination.Offset, pagination.Limit)
query := fmt.Sprintf("FOR d in %s RETURN d", models.Collections.VerificationRequest) cursor, err := p.db.Query(ctx, query, nil)
cursor, err := p.db.Query(nil, query, nil)
if err != nil { if err != nil {
return verificationRequests, err return nil, err
} }
defer cursor.Close() defer cursor.Close()
paginationClone := pagination
paginationClone.Total = cursor.Statistics().FullCount()
for { for {
var verificationRequest models.VerificationRequest var verificationRequest models.VerificationRequest
meta, err := cursor.ReadDocument(nil, &verificationRequest) meta, err := cursor.ReadDocument(nil, &verificationRequest)
@@ -111,16 +116,19 @@ func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, err
if driver.IsNoMoreDocuments(err) { if driver.IsNoMoreDocuments(err) {
break break
} else if err != nil { } else if err != nil {
return verificationRequests, err return nil, err
} }
if meta.Key != "" { if meta.Key != "" {
verificationRequests = append(verificationRequests, verificationRequest) verificationRequests = append(verificationRequests, verificationRequest.AsAPIVerificationRequest())
} }
} }
return verificationRequests, nil return &model.VerificationRequests{
VerificationRequests: verificationRequests,
Pagination: &paginationClone,
}, nil
} }
// DeleteVerificationRequest to delete verification request from database // DeleteVerificationRequest to delete verification request from database

View File

@@ -8,6 +8,7 @@ import (
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/google/uuid" "github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
@@ -60,13 +61,29 @@ func (p *provider) DeleteUser(user models.User) error {
} }
// ListUsers to get list of users from database // ListUsers to get list of users from database
func (p *provider) ListUsers() ([]models.User, error) { func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
var users []models.User var users []*model.User
opts := options.Find()
opts.SetLimit(pagination.Limit)
opts.SetSkip(pagination.Offset)
opts.SetSort(bson.M{"created_at": -1})
paginationClone := pagination
// TODO add pagination total
userCollection := p.db.Collection(models.Collections.User, options.Collection()) userCollection := p.db.Collection(models.Collections.User, options.Collection())
cursor, err := userCollection.Find(nil, bson.M{}, options.Find()) count, err := userCollection.CountDocuments(nil, bson.M{}, options.Count())
if err != nil {
log.Println("error getting total users:", err)
return nil, err
}
paginationClone.Total = count
cursor, err := userCollection.Find(nil, bson.M{}, opts)
if err != nil { if err != nil {
log.Println("error getting users:", err) log.Println("error getting users:", err)
return users, err return nil, err
} }
defer cursor.Close(nil) defer cursor.Close(nil)
@@ -74,12 +91,15 @@ func (p *provider) ListUsers() ([]models.User, error) {
var user models.User var user models.User
err := cursor.Decode(&user) err := cursor.Decode(&user)
if err != nil { if err != nil {
return users, err return nil, err
} }
users = append(users, user) users = append(users, user.AsAPIUser())
} }
return users, nil return &model.Users{
Pagination: &paginationClone,
Users: users,
}, nil
} }
// GetUserByEmail to get user information from database using email address // GetUserByEmail to get user information from database using email address

View File

@@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/google/uuid" "github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
@@ -56,13 +57,24 @@ func (p *provider) GetVerificationRequestByEmail(email string, identifier string
} }
// ListVerificationRequests to get list of verification requests from database // ListVerificationRequests to get list of verification requests from database
func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, error) { func (p *provider) ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error) {
var verificationRequests []models.VerificationRequest var verificationRequests []*model.VerificationRequest
opts := options.Find()
opts.SetLimit(pagination.Limit)
opts.SetSkip(pagination.Offset)
opts.SetSort(bson.M{"created_at": -1})
verificationRequestCollection := p.db.Collection(models.Collections.VerificationRequest, options.Collection()) verificationRequestCollection := p.db.Collection(models.Collections.VerificationRequest, options.Collection())
cursor, err := verificationRequestCollection.Find(nil, bson.M{}, options.Find())
verificationRequestCollectionCount, err := verificationRequestCollection.CountDocuments(nil, bson.M{})
paginationClone := pagination
paginationClone.Total = verificationRequestCollectionCount
cursor, err := verificationRequestCollection.Find(nil, bson.M{}, opts)
if err != nil { if err != nil {
log.Println("error getting verification requests:", err) log.Println("error getting verification requests:", err)
return verificationRequests, err return nil, err
} }
defer cursor.Close(nil) defer cursor.Close(nil)
@@ -70,12 +82,15 @@ func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, err
var verificationRequest models.VerificationRequest var verificationRequest models.VerificationRequest
err := cursor.Decode(&verificationRequest) err := cursor.Decode(&verificationRequest)
if err != nil { if err != nil {
return verificationRequests, err return nil, err
} }
verificationRequests = append(verificationRequests, verificationRequest) verificationRequests = append(verificationRequests, verificationRequest.AsAPIVerificationRequest())
} }
return verificationRequests, nil return &model.VerificationRequests{
VerificationRequests: verificationRequests,
Pagination: &paginationClone,
}, nil
} }
// DeleteVerificationRequest to delete verification request from database // DeleteVerificationRequest to delete verification request from database

View File

@@ -1,6 +1,9 @@
package providers package providers
import "github.com/authorizerdev/authorizer/server/db/models" import (
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/graph/model"
)
type Provider interface { type Provider interface {
// AddUser to save user information in database // AddUser to save user information in database
@@ -10,7 +13,7 @@ type Provider interface {
// DeleteUser to delete user information from database // DeleteUser to delete user information from database
DeleteUser(user models.User) error DeleteUser(user models.User) error
// ListUsers to get list of users from database // ListUsers to get list of users from database
ListUsers() ([]models.User, error) ListUsers(pagination model.Pagination) (*model.Users, error)
// GetUserByEmail to get user information from database using email address // GetUserByEmail to get user information from database using email address
GetUserByEmail(email string) (models.User, error) GetUserByEmail(email string) (models.User, error)
// GetUserByID to get user information from database using user ID // GetUserByID to get user information from database using user ID
@@ -23,7 +26,7 @@ type Provider interface {
// GetVerificationRequestByEmail to get verification request by email from database // GetVerificationRequestByEmail to get verification request by email from database
GetVerificationRequestByEmail(email string, identifier string) (models.VerificationRequest, error) GetVerificationRequestByEmail(email string, identifier string) (models.VerificationRequest, error)
// ListVerificationRequests to get list of verification requests from database // ListVerificationRequests to get list of verification requests from database
ListVerificationRequests() ([]models.VerificationRequest, error) ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error)
// DeleteVerificationRequest to delete verification request from database // DeleteVerificationRequest to delete verification request from database
DeleteVerificationRequest(verificationRequest models.VerificationRequest) error DeleteVerificationRequest(verificationRequest models.VerificationRequest) error

View File

@@ -15,8 +15,10 @@ func (p *provider) AddEnv(env models.Env) (models.Env, error) {
} }
env.Key = env.ID env.Key = env.ID
result := p.db.Create(&env) env.CreatedAt = time.Now().Unix()
env.UpdatedAt = time.Now().Unix()
result := p.db.Create(&env)
if result.Error != nil { if result.Error != nil {
log.Println("error adding config:", result.Error) log.Println("error adding config:", result.Error)
return env, result.Error return env, result.Error

View File

@@ -2,6 +2,7 @@ package sql
import ( import (
"log" "log"
"time"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/google/uuid" "github.com/google/uuid"
@@ -15,6 +16,8 @@ func (p *provider) AddSession(session models.Session) error {
} }
session.Key = session.ID session.Key = session.ID
session.CreatedAt = time.Now().Unix()
session.UpdatedAt = time.Now().Unix()
res := p.db.Clauses( res := p.db.Clauses(
clause.OnConflict{ clause.OnConflict{
DoNothing: true, DoNothing: true,

View File

@@ -8,6 +8,7 @@ import (
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
@@ -22,6 +23,8 @@ func (p *provider) AddUser(user models.User) (models.User, error) {
user.Roles = strings.Join(envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",") user.Roles = strings.Join(envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
} }
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()
user.Key = user.ID user.Key = user.ID
result := p.db.Clauses( result := p.db.Clauses(
clause.OnConflict{ clause.OnConflict{
@@ -64,15 +67,32 @@ func (p *provider) DeleteUser(user models.User) error {
} }
// ListUsers to get list of users from database // ListUsers to get list of users from database
func (p *provider) ListUsers() ([]models.User, error) { func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
var users []models.User var users []models.User
result := p.db.Find(&users) result := p.db.Limit(int(pagination.Limit)).Offset(int(pagination.Offset)).Order("created_at DESC").Find(&users)
if result.Error != nil { if result.Error != nil {
log.Println("error getting users:", result.Error) log.Println("error getting users:", result.Error)
return users, result.Error return nil, result.Error
} }
return users, nil responseUsers := []*model.User{}
for _, user := range users {
responseUsers = append(responseUsers, user.AsAPIUser())
}
var total int64
totalRes := p.db.Model(&models.User{}).Count(&total)
if totalRes.Error != nil {
return nil, totalRes.Error
}
paginationClone := pagination
paginationClone.Total = total
return &model.Users{
Pagination: &paginationClone,
Users: responseUsers,
}, nil
} }
// GetUserByEmail to get user information from database using email address // GetUserByEmail to get user information from database using email address

View File

@@ -2,8 +2,10 @@ package sql
import ( import (
"log" "log"
"time"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
@@ -15,6 +17,8 @@ func (p *provider) AddVerificationRequest(verificationRequest models.Verificatio
} }
verificationRequest.Key = verificationRequest.ID verificationRequest.Key = verificationRequest.ID
verificationRequest.CreatedAt = time.Now().Unix()
verificationRequest.UpdatedAt = time.Now().Unix()
result := p.db.Clauses(clause.OnConflict{ result := p.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}}, Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}},
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at"}), DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at"}),
@@ -56,15 +60,33 @@ func (p *provider) GetVerificationRequestByEmail(email string, identifier string
} }
// ListVerificationRequests to get list of verification requests from database // ListVerificationRequests to get list of verification requests from database
func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, error) { func (p *provider) ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error) {
var verificationRequests []models.VerificationRequest var verificationRequests []models.VerificationRequest
result := p.db.Find(&verificationRequests) result := p.db.Limit(int(pagination.Limit)).Offset(int(pagination.Offset)).Order("created_at DESC").Find(&verificationRequests)
if result.Error != nil { if result.Error != nil {
log.Println("error getting verification requests:", result.Error) log.Println("error getting verification requests:", result.Error)
return verificationRequests, result.Error return nil, result.Error
} }
return verificationRequests, nil
responseVerificationRequests := []*model.VerificationRequest{}
for _, v := range verificationRequests {
responseVerificationRequests = append(responseVerificationRequests, v.AsAPIVerificationRequest())
}
var total int64
totalRes := p.db.Model(&models.VerificationRequest{}).Count(&total)
if totalRes.Error != nil {
return nil, totalRes.Error
}
paginationClone := pagination
paginationClone.Total = total
return &model.VerificationRequests{
VerificationRequests: responseVerificationRequests,
Pagination: &paginationClone,
}, nil
} }
// DeleteVerificationRequest to delete verification request from database // DeleteVerificationRequest to delete verification request from database

View File

@@ -6,10 +6,10 @@ import (
) )
// SendForgotPasswordMail to send forgot password email // SendForgotPasswordMail to send forgot password email
func SendForgotPasswordMail(toEmail, token, host string) error { func SendForgotPasswordMail(toEmail, token, hostname string) error {
resetPasswordUrl := envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyResetPasswordURL) resetPasswordUrl := envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyResetPasswordURL)
if resetPasswordUrl == "" { if resetPasswordUrl == "" {
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyResetPasswordURL, envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL)+"/app/reset-password") envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyResetPasswordURL, hostname+"/app/reset-password")
} }
// The receiver needs to be in slice as the receive supports multiple receiver // The receiver needs to be in slice as the receive supports multiple receiver

View File

@@ -6,7 +6,7 @@ import (
) )
// SendVerificationMail to send verification email // SendVerificationMail to send verification email
func SendVerificationMail(toEmail, token string) error { func SendVerificationMail(toEmail, token, hostname string) error {
// The receiver needs to be in slice as the receive supports multiple receiver // The receiver needs to be in slice as the receive supports multiple receiver
Receiver := []string{toEmail} Receiver := []string{toEmail}
@@ -99,7 +99,7 @@ func SendVerificationMail(toEmail, token string) error {
data := make(map[string]interface{}, 3) data := make(map[string]interface{}, 3)
data["org_logo"] = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo) data["org_logo"] = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
data["org_name"] = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName) data["org_name"] = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
data["verification_url"] = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/verify_email?token=" + token data["verification_url"] = hostname + "/verify_email?token=" + token
message = addEmailTemplate(message, data, "verify_email.tmpl") message = addEmailTemplate(message, data, "verify_email.tmpl")
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)

31
server/env/env.go vendored
View File

@@ -12,16 +12,6 @@ import (
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
// TODO move this to env store
var (
// ARG_DB_URL is the cli arg variable for the database url
ARG_DB_URL *string
// ARG_DB_TYPE is the cli arg variable for the database type
ARG_DB_TYPE *string
// ARG_ENV_FILE is the cli arg variable for the env file
ARG_ENV_FILE *string
)
// InitEnv to initialize EnvData and through error if required env are not present // InitEnv to initialize EnvData and through error if required env are not present
func InitEnv() { func InitEnv() {
// get clone of current store // get clone of current store
@@ -41,8 +31,6 @@ func InitEnv() {
} }
} }
// set authorizer url to empty string so that fresh url is obtained with every server start
envData.StringEnv[constants.EnvKeyAuthorizerURL] = ""
if envData.StringEnv[constants.EnvKeyAppURL] == "" { if envData.StringEnv[constants.EnvKeyAppURL] == "" {
envData.StringEnv[constants.EnvKeyAppURL] = os.Getenv(constants.EnvKeyAppURL) envData.StringEnv[constants.EnvKeyAppURL] = os.Getenv(constants.EnvKeyAppURL)
} }
@@ -51,8 +39,8 @@ func InitEnv() {
envData.StringEnv[constants.EnvKeyEnvPath] = `.env` envData.StringEnv[constants.EnvKeyEnvPath] = `.env`
} }
if ARG_ENV_FILE != nil && *ARG_ENV_FILE != "" { if envstore.ARG_ENV_FILE != nil && *envstore.ARG_ENV_FILE != "" {
envData.StringEnv[constants.EnvKeyEnvPath] = *ARG_ENV_FILE envData.StringEnv[constants.EnvKeyEnvPath] = *envstore.ARG_ENV_FILE
} }
err := godotenv.Load(envData.StringEnv[constants.EnvKeyEnvPath]) err := godotenv.Load(envData.StringEnv[constants.EnvKeyEnvPath])
@@ -74,8 +62,8 @@ func InitEnv() {
if envData.StringEnv[constants.EnvKeyDatabaseType] == "" { if envData.StringEnv[constants.EnvKeyDatabaseType] == "" {
envData.StringEnv[constants.EnvKeyDatabaseType] = os.Getenv("DATABASE_TYPE") envData.StringEnv[constants.EnvKeyDatabaseType] = os.Getenv("DATABASE_TYPE")
if ARG_DB_TYPE != nil && *ARG_DB_TYPE != "" { if envstore.ARG_DB_TYPE != nil && *envstore.ARG_DB_TYPE != "" {
envData.StringEnv[constants.EnvKeyDatabaseType] = *ARG_DB_TYPE envData.StringEnv[constants.EnvKeyDatabaseType] = *envstore.ARG_DB_TYPE
} }
if envData.StringEnv[constants.EnvKeyDatabaseType] == "" { if envData.StringEnv[constants.EnvKeyDatabaseType] == "" {
@@ -86,8 +74,8 @@ func InitEnv() {
if envData.StringEnv[constants.EnvKeyDatabaseURL] == "" { if envData.StringEnv[constants.EnvKeyDatabaseURL] == "" {
envData.StringEnv[constants.EnvKeyDatabaseURL] = os.Getenv("DATABASE_URL") envData.StringEnv[constants.EnvKeyDatabaseURL] = os.Getenv("DATABASE_URL")
if ARG_DB_URL != nil && *ARG_DB_URL != "" { if envstore.ARG_DB_URL != nil && *envstore.ARG_DB_URL != "" {
envData.StringEnv[constants.EnvKeyDatabaseURL] = *ARG_DB_URL envData.StringEnv[constants.EnvKeyDatabaseURL] = *envstore.ARG_DB_URL
} }
if envData.StringEnv[constants.EnvKeyDatabaseURL] == "" { if envData.StringEnv[constants.EnvKeyDatabaseURL] == "" {
@@ -256,10 +244,9 @@ func InitEnv() {
trimVal := strings.TrimSpace(val) trimVal := strings.TrimSpace(val)
if trimVal != "" { if trimVal != "" {
roles = append(roles, trimVal) roles = append(roles, trimVal)
} if utils.StringSliceContains(defaultRoleSplit, trimVal) {
defaultRoles = append(defaultRoles, trimVal)
if utils.StringSliceContains(defaultRoleSplit, trimVal) { }
defaultRoles = append(defaultRoles, trimVal)
} }
} }

View File

@@ -25,15 +25,19 @@ func PersistEnv() error {
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEncryptionKey, hash) envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEncryptionKey, hash)
encodedHash := utils.EncryptB64(hash) encodedHash := utils.EncryptB64(hash)
configData, err := json.Marshal(envstore.EnvInMemoryStoreObj.GetEnvStoreClone()) encryptedConfig, err := utils.EncryptEnvData(envstore.EnvInMemoryStoreObj.GetEnvStoreClone())
if err != nil { if err != nil {
return err return err
} }
// configData, err := json.Marshal()
// if err != nil {
// return err
// }
encryptedConfig, err := utils.EncryptAES(configData) // encryptedConfig, err := utils.EncryptAES(configData)
if err != nil { // if err != nil {
return err // return err
} // }
env = models.Env{ env = models.Env{
Hash: encodedHash, Hash: encodedHash,
@@ -51,7 +55,12 @@ func PersistEnv() error {
} }
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEncryptionKey, decryptedEncryptionKey) envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEncryptionKey, decryptedEncryptionKey)
decryptedConfigs, err := utils.DecryptAES(env.EnvData) b64DecryptedConfig, err := utils.DecryptB64(env.EnvData)
if err != nil {
return err
}
decryptedConfigs, err := utils.DecryptAES([]byte(b64DecryptedConfig))
if err != nil { if err != nil {
return err return err
} }

View File

@@ -6,6 +6,15 @@ import (
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
) )
var (
// ARG_DB_URL is the cli arg variable for the database url
ARG_DB_URL *string
// ARG_DB_TYPE is the cli arg variable for the database type
ARG_DB_TYPE *string
// ARG_ENV_FILE is the cli arg variable for the env file
ARG_ENV_FILE *string
)
// Store data structure // Store data structure
type Store struct { type Store struct {
StringEnv map[string]string `json:"string_env"` StringEnv map[string]string `json:"string_env"`
@@ -26,7 +35,7 @@ var EnvInMemoryStoreObj = &EnvInMemoryStore{
constants.EnvKeyAdminCookieName: "authorizer-admin", constants.EnvKeyAdminCookieName: "authorizer-admin",
constants.EnvKeyJwtRoleClaim: "role", constants.EnvKeyJwtRoleClaim: "role",
constants.EnvKeyOrganizationName: "Authorizer", constants.EnvKeyOrganizationName: "Authorizer",
constants.EnvKeyOrganizationLogo: "https://www.authorizer.io/images/logo.png", constants.EnvKeyOrganizationLogo: "https://www.authorizer.dev/images/logo.png",
}, },
BoolEnv: map[string]bool{ BoolEnv: map[string]bool{
constants.EnvKeyDisableBasicAuthentication: false, constants.EnvKeyDisableBasicAuthentication: false,

View File

@@ -6,7 +6,6 @@ require (
github.com/99designs/gqlgen v0.14.0 github.com/99designs/gqlgen v0.14.0
github.com/arangodb/go-driver v1.2.1 github.com/arangodb/go-driver v1.2.1
github.com/coreos/go-oidc/v3 v3.1.0 github.com/coreos/go-oidc/v3 v3.1.0
github.com/gin-contrib/location v0.0.2
github.com/gin-gonic/gin v1.7.2 github.com/gin-gonic/gin v1.7.2
github.com/go-playground/validator/v10 v10.8.0 // indirect github.com/go-playground/validator/v10 v10.8.0 // indirect
github.com/go-redis/redis/v8 v8.11.0 github.com/go-redis/redis/v8 v8.11.0

View File

@@ -82,11 +82,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gin-contrib/location v0.0.2 h1:QZKh1+K/LLR4KG/61eIO3b7MLuKi8tytQhV6texLgP4=
github.com/gin-contrib/location v0.0.2/go.mod h1:NGoidiRlf0BlA/VKSVp+g3cuSMeTmip/63PhEjRhUAc=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA= github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -100,7 +97,6 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.8.0 h1:1kAa0fCrnpv+QYdkdcRzrRM7AyYs5o8+jZdJCz9xj6k= github.com/go-playground/validator/v10 v10.8.0 h1:1kAa0fCrnpv+QYdkdcRzrRM7AyYs5o8+jZdJCz9xj6k=
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk= github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=

File diff suppressed because it is too large Load Diff

View File

@@ -23,9 +23,10 @@ type DeleteUserInput struct {
type Env struct { type Env struct {
AdminSecret *string `json:"ADMIN_SECRET"` AdminSecret *string `json:"ADMIN_SECRET"`
DatabaseType *string `json:"DATABASE_TYPE"`
DatabaseURL *string `json:"DATABASE_URL"`
DatabaseName *string `json:"DATABASE_NAME"` DatabaseName *string `json:"DATABASE_NAME"`
DatabaseURL *string `json:"DATABASE_URL"`
DatabaseType *string `json:"DATABASE_TYPE"`
CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"`
SMTPHost *string `json:"SMTP_HOST"` SMTPHost *string `json:"SMTP_HOST"`
SMTPPort *string `json:"SMTP_PORT"` SMTPPort *string `json:"SMTP_PORT"`
SMTPUsername *string `json:"SMTP_USERNAME"` SMTPUsername *string `json:"SMTP_USERNAME"`
@@ -34,7 +35,6 @@ type Env struct {
JwtType *string `json:"JWT_TYPE"` JwtType *string `json:"JWT_TYPE"`
JwtSecret *string `json:"JWT_SECRET"` JwtSecret *string `json:"JWT_SECRET"`
AllowedOrigins []string `json:"ALLOWED_ORIGINS"` AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
AuthorizerURL *string `json:"AUTHORIZER_URL"`
AppURL *string `json:"APP_URL"` AppURL *string `json:"APP_URL"`
RedisURL *string `json:"REDIS_URL"` RedisURL *string `json:"REDIS_URL"`
CookieName *string `json:"COOKIE_NAME"` CookieName *string `json:"COOKIE_NAME"`
@@ -66,6 +66,11 @@ type ForgotPasswordInput struct {
Email string `json:"email"` Email string `json:"email"`
} }
type IsValidJWTQueryInput struct {
Jwt *string `json:"jwt"`
Roles []string `json:"roles"`
}
type LoginInput struct { type LoginInput struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
@@ -87,6 +92,22 @@ type Meta struct {
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
} }
type PaginatedInput struct {
Pagination *PaginationInput `json:"pagination"`
}
type Pagination struct {
Limit int64 `json:"limit"`
Page int64 `json:"page"`
Offset int64 `json:"offset"`
Total int64 `json:"total"`
}
type PaginationInput struct {
Limit *int64 `json:"limit"`
Page *int64 `json:"page"`
}
type ResendVerifyEmailInput struct { type ResendVerifyEmailInput struct {
Email string `json:"email"` Email string `json:"email"`
Identifier string `json:"identifier"` Identifier string `json:"identifier"`
@@ -102,6 +123,10 @@ type Response struct {
Message string `json:"message"` Message string `json:"message"`
} }
type SessionQueryInput struct {
Roles []string `json:"roles"`
}
type SignUpInput struct { type SignUpInput struct {
Email string `json:"email"` Email string `json:"email"`
GivenName *string `json:"given_name"` GivenName *string `json:"given_name"`
@@ -119,18 +144,16 @@ type SignUpInput struct {
type UpdateEnvInput struct { type UpdateEnvInput struct {
AdminSecret *string `json:"ADMIN_SECRET"` AdminSecret *string `json:"ADMIN_SECRET"`
CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"`
OldAdminSecret *string `json:"OLD_ADMIN_SECRET"` OldAdminSecret *string `json:"OLD_ADMIN_SECRET"`
DatabaseType *string `json:"DATABASE_TYPE"`
DatabaseURL *string `json:"DATABASE_URL"`
DatabaseName *string `json:"DATABASE_NAME"`
SMTPHost *string `json:"SMTP_HOST"` SMTPHost *string `json:"SMTP_HOST"`
SMTPPort *string `json:"SMTP_PORT"` SMTPPort *string `json:"SMTP_PORT"`
SMTPUsername *string `json:"SMTP_USERNAME"`
SMTPPassword *string `json:"SMTP_PASSWORD"`
SenderEmail *string `json:"SENDER_EMAIL"` SenderEmail *string `json:"SENDER_EMAIL"`
SenderPassword *string `json:"SENDER_PASSWORD"`
JwtType *string `json:"JWT_TYPE"` JwtType *string `json:"JWT_TYPE"`
JwtSecret *string `json:"JWT_SECRET"` JwtSecret *string `json:"JWT_SECRET"`
AllowedOrigins []string `json:"ALLOWED_ORIGINS"` AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
AuthorizerURL *string `json:"AUTHORIZER_URL"`
AppURL *string `json:"APP_URL"` AppURL *string `json:"APP_URL"`
RedisURL *string `json:"REDIS_URL"` RedisURL *string `json:"REDIS_URL"`
CookieName *string `json:"COOKIE_NAME"` CookieName *string `json:"COOKIE_NAME"`
@@ -203,6 +226,16 @@ type User struct {
UpdatedAt *int64 `json:"updated_at"` UpdatedAt *int64 `json:"updated_at"`
} }
type Users struct {
Pagination *Pagination `json:"pagination"`
Users []*User `json:"users"`
}
type ValidJWTResponse struct {
Valid bool `json:"valid"`
Message string `json:"message"`
}
type VerificationRequest struct { type VerificationRequest struct {
ID string `json:"id"` ID string `json:"id"`
Identifier *string `json:"identifier"` Identifier *string `json:"identifier"`
@@ -213,6 +246,11 @@ type VerificationRequest struct {
UpdatedAt *int64 `json:"updated_at"` UpdatedAt *int64 `json:"updated_at"`
} }
type VerificationRequests struct {
Pagination *Pagination `json:"pagination"`
VerificationRequests []*VerificationRequest `json:"verification_requests"`
}
type VerifyEmailInput struct { type VerifyEmailInput struct {
Token string `json:"token"` Token string `json:"token"`
} }

View File

@@ -5,6 +5,13 @@ scalar Int64
scalar Map scalar Map
scalar Any scalar Any
type Pagination {
limit: Int64!
page: Int64!
offset: Int64!
total: Int64!
}
type Meta { type Meta {
version: String! version: String!
is_google_login_enabled: Boolean! is_google_login_enabled: Boolean!
@@ -36,6 +43,11 @@ type User {
updated_at: Int64 updated_at: Int64
} }
type Users {
pagination: Pagination!
users: [User!]!
}
type VerificationRequest { type VerificationRequest {
id: ID! id: ID!
identifier: String identifier: String
@@ -46,6 +58,11 @@ type VerificationRequest {
updated_at: Int64 updated_at: Int64
} }
type VerificationRequests {
pagination: Pagination!
verification_requests: [VerificationRequest!]!
}
type Error { type Error {
message: String! message: String!
reason: String! reason: String!
@@ -62,11 +79,17 @@ type Response {
message: String! message: String!
} }
type ValidJWTResponse {
valid: Boolean!
message: String!
}
type Env { type Env {
ADMIN_SECRET: String ADMIN_SECRET: String
DATABASE_TYPE: String
DATABASE_URL: String
DATABASE_NAME: String DATABASE_NAME: String
DATABASE_URL: String
DATABASE_TYPE: String
CUSTOM_ACCESS_TOKEN_SCRIPT: String
SMTP_HOST: String SMTP_HOST: String
SMTP_PORT: String SMTP_PORT: String
SMTP_USERNAME: String SMTP_USERNAME: String
@@ -75,7 +98,6 @@ type Env {
JWT_TYPE: String JWT_TYPE: String
JWT_SECRET: String JWT_SECRET: String
ALLOWED_ORIGINS: [String!] ALLOWED_ORIGINS: [String!]
AUTHORIZER_URL: String
APP_URL: String APP_URL: String
REDIS_URL: String REDIS_URL: String
COOKIE_NAME: String COOKIE_NAME: String
@@ -100,18 +122,16 @@ type Env {
input UpdateEnvInput { input UpdateEnvInput {
ADMIN_SECRET: String ADMIN_SECRET: String
CUSTOM_ACCESS_TOKEN_SCRIPT: String
OLD_ADMIN_SECRET: String OLD_ADMIN_SECRET: String
DATABASE_TYPE: String
DATABASE_URL: String
DATABASE_NAME: String
SMTP_HOST: String SMTP_HOST: String
SMTP_PORT: String SMTP_PORT: String
SMTP_USERNAME: String
SMTP_PASSWORD: String
SENDER_EMAIL: String SENDER_EMAIL: String
SENDER_PASSWORD: String
JWT_TYPE: String JWT_TYPE: String
JWT_SECRET: String JWT_SECRET: String
ALLOWED_ORIGINS: [String!] ALLOWED_ORIGINS: [String!]
AUTHORIZER_URL: String
APP_URL: String APP_URL: String
REDIS_URL: String REDIS_URL: String
COOKIE_NAME: String COOKIE_NAME: String
@@ -221,6 +241,24 @@ input MagicLinkLoginInput {
roles: [String!] roles: [String!]
} }
input SessionQueryInput {
roles: [String!]
}
input IsValidJWTQueryInput {
jwt: String
roles: [String!]
}
input PaginationInput {
limit: Int64
page: Int64
}
input PaginatedInput {
pagination: PaginationInput
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
@@ -242,11 +280,12 @@ type Mutation {
type Query { type Query {
meta: Meta! meta: Meta!
session(roles: [String!]): AuthResponse! session(params: SessionQueryInput): AuthResponse!
is_valid_jwt(params: IsValidJWTQueryInput): ValidJWTResponse!
profile: User! profile: User!
# admin only apis # admin only apis
_users: [User!]! _users(params: PaginatedInput): Users!
_verification_requests: [VerificationRequest!]! _verification_requests(params: PaginatedInput): VerificationRequests!
_admin_session: Response! _admin_session: Response!
_env: Env! _env: Env!
} }

View File

@@ -75,20 +75,24 @@ func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) {
return resolvers.MetaResolver(ctx) return resolvers.MetaResolver(ctx)
} }
func (r *queryResolver) Session(ctx context.Context, roles []string) (*model.AuthResponse, error) { func (r *queryResolver) Session(ctx context.Context, params *model.SessionQueryInput) (*model.AuthResponse, error) {
return resolvers.SessionResolver(ctx, roles) return resolvers.SessionResolver(ctx, params)
}
func (r *queryResolver) IsValidJwt(ctx context.Context, params *model.IsValidJWTQueryInput) (*model.ValidJWTResponse, error) {
return resolvers.IsValidJwtResolver(ctx, params)
} }
func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) { func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) {
return resolvers.ProfileResolver(ctx) return resolvers.ProfileResolver(ctx)
} }
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) { func (r *queryResolver) Users(ctx context.Context, params *model.PaginatedInput) (*model.Users, error) {
return resolvers.UsersResolver(ctx) return resolvers.UsersResolver(ctx, params)
} }
func (r *queryResolver) VerificationRequests(ctx context.Context) ([]*model.VerificationRequest, error) { func (r *queryResolver) VerificationRequests(ctx context.Context, params *model.PaginatedInput) (*model.VerificationRequests, error) {
return resolvers.VerificationRequestsResolver(ctx) return resolvers.VerificationRequestsResolver(ctx, params)
} }
func (r *queryResolver) AdminSession(ctx context.Context) (*model.Response, error) { func (r *queryResolver) AdminSession(ctx context.Context) (*model.Response, error) {

View File

@@ -22,14 +22,19 @@ type State struct {
// AppHandler is the handler for the /app route // AppHandler is the handler for the /app route
func AppHandler() gin.HandlerFunc { func AppHandler() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
hostname := utils.GetHost(c)
if envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableLoginPage) {
c.JSON(400, gin.H{"error": "login page is not enabled"})
return
}
state := c.Query("state") state := c.Query("state")
var stateObj State var stateObj State
if state == "" { if state == "" {
stateObj.AuthorizerURL = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) stateObj.AuthorizerURL = hostname
stateObj.RedirectURL = stateObj.AuthorizerURL + "/app" stateObj.RedirectURL = hostname + "/app"
} else { } else {
decodedState, err := utils.DecryptB64(state) decodedState, err := utils.DecryptB64(state)
if err != nil { if err != nil {
@@ -57,7 +62,7 @@ func AppHandler() gin.HandlerFunc {
} }
// validate host and domain of authorizer url // validate host and domain of authorizer url
if strings.TrimSuffix(stateObj.AuthorizerURL, "/") != envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) { if strings.TrimSuffix(stateObj.AuthorizerURL, "/") != hostname {
c.JSON(400, gin.H{"error": "invalid host url"}) c.JSON(400, gin.H{"error": "invalid host url"})
return return
} }

View File

@@ -11,11 +11,13 @@ import (
"time" "time"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/oauth" "github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -28,11 +30,11 @@ func OAuthCallbackHandler() gin.HandlerFunc {
provider := c.Param("oauth_provider") provider := c.Param("oauth_provider")
state := c.Request.FormValue("state") state := c.Request.FormValue("state")
sessionState := session.GetSocailLoginState(state) sessionState := sessionstore.GetSocailLoginState(state)
if sessionState == "" { if sessionState == "" {
c.JSON(400, gin.H{"error": "invalid oauth state"}) c.JSON(400, gin.H{"error": "invalid oauth state"})
} }
session.RemoveSocialLoginState(state) sessionstore.RemoveSocialLoginState(state)
// contains random token, redirect url, role // contains random token, redirect url, role
sessionSplit := strings.Split(state, "___") sessionSplit := strings.Split(state, "___")
@@ -94,8 +96,13 @@ func OAuthCallbackHandler() gin.HandlerFunc {
if !strings.Contains(signupMethod, provider) { if !strings.Contains(signupMethod, provider) {
signupMethod = signupMethod + "," + provider signupMethod = signupMethod + "," + provider
} }
user = existingUser
user.SignupMethods = signupMethod user.SignupMethods = signupMethod
user.Password = existingUser.Password
if user.EmailVerifiedAt == nil {
now := time.Now().Unix()
user.EmailVerifiedAt = &now
}
// There multiple scenarios with roles here in social login // There multiple scenarios with roles here in social login
// 1. user has access to protected roles + roles and trying to login // 1. user has access to protected roles + roles and trying to login
@@ -129,18 +136,17 @@ func OAuthCallbackHandler() gin.HandlerFunc {
} else { } else {
user.Roles = existingUser.Roles user.Roles = existingUser.Roles
} }
user.Key = existingUser.Key
user.ID = existingUser.ID
user, err = db.Provider.UpdateUser(user) user, err = db.Provider.UpdateUser(user)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
} }
user, _ = db.Provider.GetUserByEmail(user.Email) authToken, _ := token.CreateAuthToken(user, inputRoles)
userIdStr := fmt.Sprintf("%v", user.ID) sessionstore.SetUserSession(user.ID, authToken.FingerPrint, authToken.RefreshToken.Token)
refreshToken, _, _ := utils.CreateAuthToken(user, constants.TokenTypeRefreshToken, inputRoles) cookie.SetCookie(c, authToken.AccessToken.Token, authToken.RefreshToken.Token, authToken.FingerPrintHash)
accessToken, _, _ := utils.CreateAuthToken(user, constants.TokenTypeAccessToken, inputRoles)
utils.SetCookie(c, accessToken)
session.SetUserSession(userIdStr, accessToken, refreshToken)
utils.SaveSessionInDB(user.ID, c) utils.SaveSessionInDB(user.ID, c)
c.Redirect(http.StatusTemporaryRedirect, redirectURL) c.Redirect(http.StatusTemporaryRedirect, redirectURL)

View File

@@ -7,7 +7,7 @@ import (
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/oauth" "github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
@@ -16,7 +16,7 @@ import (
// OAuthLoginHandler set host in the oauth state that is useful for redirecting to oauth_callback // OAuthLoginHandler set host in the oauth state that is useful for redirecting to oauth_callback
func OAuthLoginHandler() gin.HandlerFunc { func OAuthLoginHandler() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// TODO validate redirect URL hostname := utils.GetHost(c)
redirectURL := c.Query("redirectURL") redirectURL := c.Query("redirectURL")
roles := c.Query("roles") roles := c.Query("roles")
@@ -54,9 +54,9 @@ func OAuthLoginHandler() gin.HandlerFunc {
isProviderConfigured = false isProviderConfigured = false
break break
} }
session.SetSocailLoginState(oauthStateString, constants.SignupMethodGoogle) sessionstore.SetSocailLoginState(oauthStateString, constants.SignupMethodGoogle)
// during the init of OAuthProvider authorizer url might be empty // during the init of OAuthProvider authorizer url might be empty
oauth.OAuthProviders.GoogleConfig.RedirectURL = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/google" oauth.OAuthProviders.GoogleConfig.RedirectURL = hostname + "/oauth_callback/google"
url := oauth.OAuthProviders.GoogleConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.GoogleConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case constants.SignupMethodGithub: case constants.SignupMethodGithub:
@@ -64,8 +64,8 @@ func OAuthLoginHandler() gin.HandlerFunc {
isProviderConfigured = false isProviderConfigured = false
break break
} }
session.SetSocailLoginState(oauthStateString, constants.SignupMethodGithub) sessionstore.SetSocailLoginState(oauthStateString, constants.SignupMethodGithub)
oauth.OAuthProviders.GithubConfig.RedirectURL = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/github" oauth.OAuthProviders.GithubConfig.RedirectURL = hostname + "/oauth_callback/github"
url := oauth.OAuthProviders.GithubConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.GithubConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case constants.SignupMethodFacebook: case constants.SignupMethodFacebook:
@@ -73,8 +73,8 @@ func OAuthLoginHandler() gin.HandlerFunc {
isProviderConfigured = false isProviderConfigured = false
break break
} }
session.SetSocailLoginState(oauthStateString, constants.SignupMethodFacebook) sessionstore.SetSocailLoginState(oauthStateString, constants.SignupMethodFacebook)
oauth.OAuthProviders.FacebookConfig.RedirectURL = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/facebook" oauth.OAuthProviders.FacebookConfig.RedirectURL = hostname + "/oauth_callback/facebook"
url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
default: default:

View File

@@ -5,9 +5,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -19,20 +20,20 @@ func VerifyEmailHandler() gin.HandlerFunc {
errorRes := gin.H{ errorRes := gin.H{
"message": "invalid token", "message": "invalid token",
} }
token := c.Query("token") tokenInQuery := c.Query("token")
if token == "" { if tokenInQuery == "" {
c.JSON(400, errorRes) c.JSON(400, errorRes)
return return
} }
verificationRequest, err := db.Provider.GetVerificationRequestByToken(token) verificationRequest, err := db.Provider.GetVerificationRequestByToken(tokenInQuery)
if err != nil { if err != nil {
c.JSON(400, errorRes) c.JSON(400, errorRes)
return return
} }
// verify if token exists in db // verify if token exists in db
claim, err := utils.VerifyVerificationToken(token) claim, err := token.VerifyVerificationToken(tokenInQuery)
if err != nil { if err != nil {
c.JSON(400, errorRes) c.JSON(400, errorRes)
return return
@@ -56,13 +57,17 @@ func VerifyEmailHandler() gin.HandlerFunc {
db.Provider.DeleteVerificationRequest(verificationRequest) db.Provider.DeleteVerificationRequest(verificationRequest)
roles := strings.Split(user.Roles, ",") roles := strings.Split(user.Roles, ",")
refreshToken, _, _ := utils.CreateAuthToken(user, constants.TokenTypeRefreshToken, roles) authToken, err := token.CreateAuthToken(user, roles)
if err != nil {
accessToken, _, _ := utils.CreateAuthToken(user, constants.TokenTypeAccessToken, roles) c.JSON(400, gin.H{
"message": err.Error(),
session.SetUserSession(user.ID, accessToken, refreshToken) })
return
}
sessionstore.SetUserSession(user.ID, authToken.FingerPrint, authToken.RefreshToken.Token)
cookie.SetCookie(c, authToken.AccessToken.Token, authToken.RefreshToken.Token, authToken.FingerPrintHash)
utils.SaveSessionInDB(user.ID, c) utils.SaveSessionInDB(user.ID, c)
utils.SetCookie(c, accessToken)
c.Redirect(http.StatusTemporaryRedirect, claim.RedirectURL) c.Redirect(http.StatusTemporaryRedirect, claim.RedirectURL)
} }
} }

View File

@@ -9,15 +9,15 @@ import (
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/oauth" "github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/routes" "github.com/authorizerdev/authorizer/server/routes"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
) )
var VERSION string var VERSION string
func main() { func main() {
env.ARG_DB_URL = flag.String("database_url", "", "Database connection string") envstore.ARG_DB_URL = flag.String("database_url", "", "Database connection string")
env.ARG_DB_TYPE = flag.String("database_type", "", "Database type, possible values are postgres,mysql,sqlite") envstore.ARG_DB_TYPE = flag.String("database_type", "", "Database type, possible values are postgres,mysql,sqlite")
env.ARG_ENV_FILE = flag.String("env_file", "", "Env file path") envstore.ARG_ENV_FILE = flag.String("env_file", "", "Env file path")
flag.Parse() flag.Parse()
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyVersion, VERSION) envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyVersion, VERSION)
@@ -26,7 +26,7 @@ func main() {
db.InitDB() db.InitDB()
env.PersistEnv() env.PersistEnv()
session.InitSession() sessionstore.InitSession()
oauth.InitOAuth() oauth.InitOAuth()
router := routes.InitRouter() router := routes.InitRouter()

View File

@@ -3,19 +3,12 @@ package middlewares
import ( import (
"context" "context"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/gin-contrib/location"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// GinContextToContextMiddleware is a middleware to add gin context in context // GinContextToContextMiddleware is a middleware to add gin context in context
func GinContextToContextMiddleware() gin.HandlerFunc { func GinContextToContextMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) == "" {
url := location.Get(c)
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyAuthorizerURL, url.Scheme+"://"+c.Request.Host)
}
ctx := context.WithValue(c.Request.Context(), "GinContextKey", c) ctx := context.WithValue(c.Request.Context(), "GinContextKey", c)
c.Request = c.Request.WithContext(ctx) c.Request = c.Request.WithContext(ctx)
c.Next() c.Next()

View File

@@ -43,7 +43,7 @@ func InitOAuth() {
OAuthProviders.GoogleConfig = &oauth2.Config{ OAuthProviders.GoogleConfig = &oauth2.Config{
ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientID), ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientID),
ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientSecret), ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientSecret),
RedirectURL: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/google", RedirectURL: "/oauth_callback/google",
Endpoint: OIDCProviders.GoogleOIDC.Endpoint(), Endpoint: OIDCProviders.GoogleOIDC.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
} }
@@ -52,7 +52,7 @@ func InitOAuth() {
OAuthProviders.GithubConfig = &oauth2.Config{ OAuthProviders.GithubConfig = &oauth2.Config{
ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID), ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID),
ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientSecret), ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientSecret),
RedirectURL: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/github", RedirectURL: "/oauth_callback/github",
Endpoint: githubOAuth2.Endpoint, Endpoint: githubOAuth2.Endpoint,
} }
} }
@@ -60,7 +60,7 @@ func InitOAuth() {
OAuthProviders.FacebookConfig = &oauth2.Config{ OAuthProviders.FacebookConfig = &oauth2.Config{
ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyFacebookClientID), ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyFacebookClientID),
ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyFacebookClientSecret), ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyFacebookClientSecret),
RedirectURL: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/facebook", RedirectURL: "/oauth_callback/facebook",
Endpoint: facebookOAuth2.Endpoint, Endpoint: facebookOAuth2.Endpoint,
Scopes: []string{"public_profile", "email"}, Scopes: []string{"public_profile", "email"},
} }

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
@@ -28,7 +29,7 @@ func AdminLoginResolver(ctx context.Context, params model.AdminLoginInput) (*mod
if err != nil { if err != nil {
return res, err return res, err
} }
utils.SetAdminCookie(gc, hashedKey) cookie.SetAdminCookie(gc, hashedKey)
res = &model.Response{ res = &model.Response{
Message: "admin logged in successfully", Message: "admin logged in successfully",

View File

@@ -4,7 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@@ -17,11 +19,11 @@ func AdminLogoutResolver(ctx context.Context) (*model.Response, error) {
return res, err return res, err
} }
if !utils.IsSuperAdmin(gc) { if !token.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized") return res, fmt.Errorf("unauthorized")
} }
utils.DeleteAdminCookie(gc) cookie.DeleteAdminCookie(gc)
res = &model.Response{ res = &model.Response{
Message: "admin logged out successfully", Message: "admin logged out successfully",

View File

@@ -5,8 +5,10 @@ import (
"fmt" "fmt"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@@ -19,7 +21,7 @@ func AdminSessionResolver(ctx context.Context) (*model.Response, error) {
return res, err return res, err
} }
if !utils.IsSuperAdmin(gc) { if !token.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized") return res, fmt.Errorf("unauthorized")
} }
@@ -27,7 +29,7 @@ func AdminSessionResolver(ctx context.Context) (*model.Response, error) {
if err != nil { if err != nil {
return res, err return res, err
} }
utils.SetAdminCookie(gc, hashedKey) cookie.SetAdminCookie(gc, hashedKey)
res = &model.Response{ res = &model.Response{
Message: "admin logged in successfully", Message: "admin logged in successfully",

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
@@ -71,7 +72,7 @@ func AdminSignupResolver(ctx context.Context, params model.AdminSignupInput) (*m
if err != nil { if err != nil {
return res, err return res, err
} }
utils.SetAdminCookie(gc, hashedKey) cookie.SetAdminCookie(gc, hashedKey)
res = &model.Response{ res = &model.Response{
Message: "admin signed up successfully", Message: "admin signed up successfully",

View File

@@ -7,7 +7,8 @@ import (
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@@ -19,7 +20,7 @@ func DeleteUserResolver(ctx context.Context, params model.DeleteUserInput) (*mod
return res, err return res, err
} }
if !utils.IsSuperAdmin(gc) { if !token.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized") return res, fmt.Errorf("unauthorized")
} }
@@ -28,7 +29,7 @@ func DeleteUserResolver(ctx context.Context, params model.DeleteUserInput) (*mod
return res, err return res, err
} }
session.DeleteAllUserSession(fmt.Sprintf("%x", user.ID)) sessionstore.DeleteAllUserSession(fmt.Sprintf("%x", user.ID))
err = db.Provider.DeleteUser(user) err = db.Provider.DeleteUser(user)
if err != nil { if err != nil {

View File

@@ -7,6 +7,7 @@ import (
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@@ -20,16 +21,17 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
return res, err return res, err
} }
if !utils.IsSuperAdmin(gc) { if !token.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized") return res, fmt.Errorf("unauthorized")
} }
// get clone of store // get clone of store
store := envstore.EnvInMemoryStoreObj.GetEnvStoreClone() store := envstore.EnvInMemoryStoreObj.GetEnvStoreClone()
adminSecret := store.StringEnv[constants.EnvKeyAdminSecret] adminSecret := store.StringEnv[constants.EnvKeyAdminSecret]
databaseType := store.StringEnv[constants.EnvKeyDatabaseType]
databaseURL := store.StringEnv[constants.EnvKeyDatabaseURL] databaseURL := store.StringEnv[constants.EnvKeyDatabaseURL]
databaseName := store.StringEnv[constants.EnvKeyDatabaseName] databaseName := store.StringEnv[constants.EnvKeyDatabaseName]
databaseType := store.StringEnv[constants.EnvKeyDatabaseType]
customAccessTokenScript := store.StringEnv[constants.EnvKeyCustomAccessTokenScript]
smtpHost := store.StringEnv[constants.EnvKeySmtpHost] smtpHost := store.StringEnv[constants.EnvKeySmtpHost]
smtpPort := store.StringEnv[constants.EnvKeySmtpPort] smtpPort := store.StringEnv[constants.EnvKeySmtpPort]
smtpUsername := store.StringEnv[constants.EnvKeySmtpUsername] smtpUsername := store.StringEnv[constants.EnvKeySmtpUsername]
@@ -39,7 +41,6 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
jwtSecret := store.StringEnv[constants.EnvKeyJwtSecret] jwtSecret := store.StringEnv[constants.EnvKeyJwtSecret]
jwtRoleClaim := store.StringEnv[constants.EnvKeyJwtRoleClaim] jwtRoleClaim := store.StringEnv[constants.EnvKeyJwtRoleClaim]
allowedOrigins := store.SliceEnv[constants.EnvKeyAllowedOrigins] allowedOrigins := store.SliceEnv[constants.EnvKeyAllowedOrigins]
authorizerURL := store.StringEnv[constants.EnvKeyAuthorizerURL]
appURL := store.StringEnv[constants.EnvKeyAppURL] appURL := store.StringEnv[constants.EnvKeyAppURL]
redisURL := store.StringEnv[constants.EnvKeyRedisURL] redisURL := store.StringEnv[constants.EnvKeyRedisURL]
cookieName := store.StringEnv[constants.EnvKeyCookieName] cookieName := store.StringEnv[constants.EnvKeyCookieName]
@@ -62,9 +63,10 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
res = &model.Env{ res = &model.Env{
AdminSecret: &adminSecret, AdminSecret: &adminSecret,
DatabaseType: &databaseType,
DatabaseURL: &databaseURL,
DatabaseName: &databaseName, DatabaseName: &databaseName,
DatabaseURL: &databaseURL,
DatabaseType: &databaseType,
CustomAccessTokenScript: &customAccessTokenScript,
SMTPHost: &smtpHost, SMTPHost: &smtpHost,
SMTPPort: &smtpPort, SMTPPort: &smtpPort,
SMTPPassword: &smtpPassword, SMTPPassword: &smtpPassword,
@@ -74,7 +76,6 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
JwtSecret: &jwtSecret, JwtSecret: &jwtSecret,
JwtRoleClaim: &jwtRoleClaim, JwtRoleClaim: &jwtRoleClaim,
AllowedOrigins: allowedOrigins, AllowedOrigins: allowedOrigins,
AuthorizerURL: &authorizerURL,
AppURL: &appURL, AppURL: &appURL,
RedisURL: &redisURL, RedisURL: &redisURL,
CookieName: &cookieName, CookieName: &cookieName,

View File

@@ -13,6 +13,7 @@ import (
"github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@@ -26,7 +27,6 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
if envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) { if envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) {
return res, fmt.Errorf(`basic authentication is disabled for this instance`) return res, fmt.Errorf(`basic authentication is disabled for this instance`)
} }
host := gc.Request.Host
params.Email = strings.ToLower(params.Email) params.Email = strings.ToLower(params.Email)
if !utils.IsValidEmail(params.Email) { if !utils.IsValidEmail(params.Email) {
@@ -38,12 +38,13 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
return res, fmt.Errorf(`user with this email not found`) return res, fmt.Errorf(`user with this email not found`)
} }
token, err := utils.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword) hostname := utils.GetHost(gc)
verificationToken, err := token.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword, hostname)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: token, Token: verificationToken,
Identifier: constants.VerificationTypeForgotPassword, Identifier: constants.VerificationTypeForgotPassword,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
@@ -51,7 +52,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency
go func() { go func() {
email.SendForgotPasswordMail(params.Email, token, host) email.SendForgotPasswordMail(params.Email, verificationToken, hostname)
}() }()
res = &model.Response{ res = &model.Response{

View File

@@ -0,0 +1,52 @@
package resolvers
import (
"context"
"errors"
"fmt"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
tokenHelper "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
)
// IsValidJwtResolver resolver to return if given jwt is valid
func IsValidJwtResolver(ctx context.Context, params *model.IsValidJWTQueryInput) (*model.ValidJWTResponse, error) {
gc, err := utils.GinContextFromContext(ctx)
token, err := token.GetAccessToken(gc)
if token == "" || err != nil {
if params != nil && *params.Jwt != "" {
token = *params.Jwt
} else {
return nil, errors.New("no jwt provided via cookie / header / params")
}
}
claims, err := tokenHelper.VerifyJWTToken(token)
if err != nil {
return nil, err
}
claimRoleInterface := claims[envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtRoleClaim)].([]interface{})
claimRoles := []string{}
for _, v := range claimRoleInterface {
claimRoles = append(claimRoles, v.(string))
}
if params != nil && params.Roles != nil && len(params.Roles) > 0 {
for _, v := range params.Roles {
if !utils.StringSliceContains(claimRoles, v) {
return nil, fmt.Errorf(`unauthorized`)
}
}
}
return &model.ValidJWTResponse{
Valid: true,
Message: "Valid JWT",
}, nil
}

View File

@@ -7,10 +7,12 @@ import (
"strings" "strings"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -56,21 +58,21 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
roles = params.Roles roles = params.Roles
} }
refreshToken, _, _ := utils.CreateAuthToken(user, constants.TokenTypeRefreshToken, roles)
accessToken, expiresAt, _ := utils.CreateAuthToken(user, constants.TokenTypeAccessToken, roles) authToken, err := token.CreateAuthToken(user, roles)
if err != nil {
session.SetUserSession(user.ID, accessToken, refreshToken) return res, err
}
sessionstore.SetUserSession(user.ID, authToken.FingerPrint, authToken.RefreshToken.Token)
cookie.SetCookie(gc, authToken.AccessToken.Token, authToken.RefreshToken.Token, authToken.FingerPrintHash)
utils.SaveSessionInDB(user.ID, gc) utils.SaveSessionInDB(user.ID, gc)
res = &model.AuthResponse{ res = &model.AuthResponse{
Message: `Logged in successfully`, Message: `Logged in successfully`,
AccessToken: &accessToken, AccessToken: &authToken.AccessToken.Token,
ExpiresAt: &expiresAt, ExpiresAt: &authToken.AccessToken.ExpiresAt,
User: utils.GetResponseUserData(user), User: user.AsAPIUser(),
} }
utils.SetCookie(gc, accessToken)
return res, nil return res, nil
} }

View File

@@ -2,10 +2,11 @@ package resolvers
import ( import (
"context" "context"
"fmt"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@@ -17,22 +18,38 @@ func LogoutResolver(ctx context.Context) (*model.Response, error) {
return res, err return res, err
} }
token, err := utils.GetAuthToken(gc) // get refresh token
refreshToken, err := token.GetRefreshToken(gc)
if err != nil { if err != nil {
return res, err return res, err
} }
claim, err := utils.VerifyAuthToken(token) // get fingerprint hash
fingerprintHash, err := token.GetFingerPrint(gc)
if err != nil { if err != nil {
return res, err return res, err
} }
userId := fmt.Sprintf("%v", claim["id"]) decryptedFingerPrint, err := utils.DecryptAES([]byte(fingerprintHash))
session.DeleteUserSession(userId, token) if err != nil {
return res, err
}
fingerPrint := string(decryptedFingerPrint)
// verify refresh token and fingerprint
claims, err := token.VerifyJWTToken(refreshToken)
if err != nil {
return res, err
}
userID := claims["id"].(string)
sessionstore.DeleteUserSession(userID, fingerPrint)
cookie.DeleteCookie(gc)
res = &model.Response{ res = &model.Response{
Message: "Logged out successfully", Message: "Logged out successfully",
} }
utils.DeleteCookie(gc)
return res, nil return res, nil
} }

View File

@@ -13,12 +13,17 @@ import (
"github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
// MagicLinkLoginResolver is a resolver for magic link login mutation // MagicLinkLoginResolver is a resolver for magic link login mutation
func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInput) (*model.Response, error) { func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInput) (*model.Response, error) {
var res *model.Response var res *model.Response
gc, err := utils.GinContextFromContext(ctx)
if err != nil {
return res, err
}
if envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) { if envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) {
return res, fmt.Errorf(`magic link login is disabled for this instance`) return res, fmt.Errorf(`magic link login is disabled for this instance`)
@@ -101,15 +106,16 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
} }
} }
hostname := utils.GetHost(gc)
if !envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { if !envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
// insert verification request // insert verification request
verificationType := constants.VerificationTypeMagicLinkLogin verificationType := constants.VerificationTypeMagicLinkLogin
token, err := utils.CreateVerificationToken(params.Email, verificationType) verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: token, Token: verificationToken,
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
@@ -117,7 +123,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency
go func() { go func() {
email.SendVerificationMail(params.Email, token) email.SendVerificationMail(params.Email, verificationToken, hostname)
}() }()
} }

View File

@@ -6,7 +6,7 @@ import (
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@@ -18,30 +18,17 @@ func ProfileResolver(ctx context.Context) (*model.User, error) {
return res, err return res, err
} }
token, err := utils.GetAuthToken(gc) claims, err := token.ValidateAccessToken(gc)
if err != nil { if err != nil {
return res, err return res, err
} }
claim, err := utils.VerifyAuthToken(token) userID := fmt.Sprintf("%v", claims["id"])
user, err := db.Provider.GetUserByID(userID)
if err != nil { if err != nil {
return res, err return res, err
} }
userID := fmt.Sprintf("%v", claim["id"]) return user.AsAPIUser(), nil
email := fmt.Sprintf("%v", claim["email"])
sessionToken := session.GetUserSession(userID, token)
if sessionToken == "" {
return res, fmt.Errorf(`unauthorized`)
}
user, err := db.Provider.GetUserByEmail(email)
if err != nil {
return res, err
}
res = utils.GetResponseUserData(user)
return res, nil
} }

View File

@@ -11,12 +11,17 @@ import (
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
// ResendVerifyEmailResolver is a resolver for resend verify email mutation // ResendVerifyEmailResolver is a resolver for resend verify email mutation
func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error) { func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error) {
var res *model.Response var res *model.Response
gc, err := utils.GinContextFromContext(ctx)
if err != nil {
return res, err
}
params.Email = strings.ToLower(params.Email) params.Email = strings.ToLower(params.Email)
if !utils.IsValidEmail(params.Email) { if !utils.IsValidEmail(params.Email) {
@@ -38,12 +43,13 @@ func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEma
log.Println("error deleting verification request:", err) log.Println("error deleting verification request:", err)
} }
token, err := utils.CreateVerificationToken(params.Email, params.Identifier) hostname := utils.GetHost(gc)
verificationToken, err := token.CreateVerificationToken(params.Email, params.Identifier, hostname)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: token, Token: verificationToken,
Identifier: params.Identifier, Identifier: params.Identifier,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
@@ -51,7 +57,7 @@ func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEma
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency
go func() { go func() {
email.SendVerificationMail(params.Email, token) email.SendVerificationMail(params.Email, verificationToken, hostname)
}() }()
res = &model.Response{ res = &model.Response{

View File

@@ -10,6 +10,7 @@ import (
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@@ -30,7 +31,7 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput)
} }
// verify if token exists in db // verify if token exists in db
claim, err := utils.VerifyVerificationToken(params.Token) claim, err := token.VerifyVerificationToken(params.Token)
if err != nil { if err != nil {
return res, fmt.Errorf(`invalid token`) return res, fmt.Errorf(`invalid token`)
} }

View File

@@ -3,80 +3,92 @@ package resolvers
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
// SessionResolver is a resolver for session query // SessionResolver is a resolver for session query
func SessionResolver(ctx context.Context, roles []string) (*model.AuthResponse, error) { func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*model.AuthResponse, error) {
var res *model.AuthResponse var res *model.AuthResponse
gc, err := utils.GinContextFromContext(ctx) gc, err := utils.GinContextFromContext(ctx)
if err != nil { if err != nil {
return res, err return res, err
} }
token, err := utils.GetAuthToken(gc)
// get refresh token
refreshToken, err := token.GetRefreshToken(gc)
if err != nil { if err != nil {
return res, err return res, err
} }
claim, accessTokenErr := utils.VerifyAuthToken(token) // get fingerprint hash
expiresAt := claim["exp"].(int64) fingerprintHash, err := token.GetFingerPrint(gc)
email := fmt.Sprintf("%v", claim["email"])
user, err := db.Provider.GetUserByEmail(email)
if err != nil { if err != nil {
return res, err return res, err
} }
userIdStr := fmt.Sprintf("%v", user.ID) decryptedFingerPrint, err := utils.DecryptAES([]byte(fingerprintHash))
if err != nil {
return res, err
}
sessionToken := session.GetUserSession(userIdStr, token) fingerPrint := string(decryptedFingerPrint)
if sessionToken == "" { // verify refresh token and fingerprint
claims, err := token.VerifyJWTToken(refreshToken)
if err != nil {
return res, err
}
userID := claims["id"].(string)
persistedRefresh := sessionstore.GetUserSession(userID, fingerPrint)
if refreshToken != persistedRefresh {
return res, fmt.Errorf(`unauthorized`) return res, fmt.Errorf(`unauthorized`)
} }
expiresTimeObj := time.Unix(expiresAt, 0) user, err := db.Provider.GetUserByID(userID)
currentTimeObj := time.Now() if err != nil {
return res, err
claimRoleInterface := claim[envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtRoleClaim)].([]interface{})
claimRoles := make([]string, len(claimRoleInterface))
for i, v := range claimRoleInterface {
claimRoles[i] = v.(string)
} }
if len(roles) > 0 { // refresh token has "roles" as claim
for _, v := range roles { claimRoleInterface := claims["roles"].([]interface{})
claimRoles := []string{}
for _, v := range claimRoleInterface {
claimRoles = append(claimRoles, v.(string))
}
if params != nil && params.Roles != nil && len(params.Roles) > 0 {
for _, v := range params.Roles {
if !utils.StringSliceContains(claimRoles, v) { if !utils.StringSliceContains(claimRoles, v) {
return res, fmt.Errorf(`unauthorized`) return res, fmt.Errorf(`unauthorized`)
} }
} }
} }
// TODO change this logic to make it more secure // delete older session
if accessTokenErr != nil || expiresTimeObj.Sub(currentTimeObj).Minutes() <= 5 { sessionstore.DeleteUserSession(userID, fingerPrint)
// if access token has expired and refresh/session token is valid
// generate new accessToken authToken, err := token.CreateAuthToken(user, claimRoles)
currentRefreshToken := session.GetUserSession(userIdStr, token) if err != nil {
session.DeleteUserSession(userIdStr, token) return res, err
token, expiresAt, _ = utils.CreateAuthToken(user, constants.TokenTypeAccessToken, claimRoles) }
session.SetUserSession(userIdStr, token, currentRefreshToken) sessionstore.SetUserSession(user.ID, authToken.FingerPrint, authToken.RefreshToken.Token)
utils.SaveSessionInDB(user.ID, gc) cookie.SetCookie(gc, authToken.AccessToken.Token, authToken.RefreshToken.Token, authToken.FingerPrintHash)
res = &model.AuthResponse{
Message: `Session token refreshed`,
AccessToken: &authToken.AccessToken.Token,
ExpiresAt: &authToken.AccessToken.ExpiresAt,
User: user.AsAPIUser(),
} }
utils.SetCookie(gc, token)
res = &model.AuthResponse{
Message: `Token verified`,
AccessToken: &token,
ExpiresAt: &expiresAt,
User: utils.GetResponseUserData(user),
}
return res, nil return res, nil
} }

View File

@@ -8,12 +8,14 @@ import (
"time" "time"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@@ -114,19 +116,19 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
if err != nil { if err != nil {
return res, err return res, err
} }
userIdStr := fmt.Sprintf("%v", user.ID)
roles := strings.Split(user.Roles, ",") roles := strings.Split(user.Roles, ",")
userToReturn := utils.GetResponseUserData(user) userToReturn := user.AsAPIUser()
hostname := utils.GetHost(gc)
if !envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { if !envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
// insert verification request // insert verification request
verificationType := constants.VerificationTypeBasicAuthSignup verificationType := constants.VerificationTypeBasicAuthSignup
token, err := utils.CreateVerificationToken(params.Email, verificationType) verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: token, Token: verificationToken,
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
@@ -134,7 +136,7 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency
go func() { go func() {
email.SendVerificationMail(params.Email, token) email.SendVerificationMail(params.Email, verificationToken, hostname)
}() }()
res = &model.AuthResponse{ res = &model.AuthResponse{
@@ -143,20 +145,20 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
} }
} else { } else {
refreshToken, _, _ := utils.CreateAuthToken(user, constants.TokenTypeRefreshToken, roles) authToken, err := token.CreateAuthToken(user, roles)
if err != nil {
accessToken, expiresAt, _ := utils.CreateAuthToken(user, constants.TokenTypeAccessToken, roles) return res, err
}
session.SetUserSession(userIdStr, accessToken, refreshToken) sessionstore.SetUserSession(user.ID, authToken.FingerPrint, authToken.RefreshToken.Token)
cookie.SetCookie(gc, authToken.AccessToken.Token, authToken.RefreshToken.Token, authToken.FingerPrintHash)
utils.SaveSessionInDB(user.ID, gc) utils.SaveSessionInDB(user.ID, gc)
res = &model.AuthResponse{ res = &model.AuthResponse{
Message: `Signed up successfully.`, Message: `Signed up successfully.`,
AccessToken: &accessToken, AccessToken: &authToken.AccessToken.Token,
ExpiresAt: &expiresAt, ExpiresAt: &authToken.AccessToken.ExpiresAt,
User: userToReturn, User: userToReturn,
} }
utils.SetCookie(gc, accessToken)
} }
return res, nil return res, nil

View File

@@ -9,11 +9,14 @@ import (
"reflect" "reflect"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"golang.org/x/crypto/bcrypt"
) )
// UpdateEnvResolver is a resolver for update config mutation // UpdateEnvResolver is a resolver for update config mutation
@@ -26,7 +29,7 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
return res, err return res, err
} }
if !utils.IsSuperAdmin(gc) { if !token.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized") return res, fmt.Errorf("unauthorized")
} }
@@ -41,6 +44,23 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
return res, fmt.Errorf("error un-marshalling params: %t", err) return res, fmt.Errorf("error un-marshalling params: %t", err)
} }
// in case of admin secret change update the cookie with new hash
if params.AdminSecret != nil {
if params.OldAdminSecret == nil {
return res, errors.New("admin secret and old admin secret are required for secret change")
}
if *params.OldAdminSecret != envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) {
return res, errors.New("old admin secret is not correct")
}
if len(*params.AdminSecret) < 6 {
err = fmt.Errorf("admin secret must be at least 6 characters")
return res, err
}
}
updatedData := envstore.EnvInMemoryStoreObj.GetEnvStoreClone() updatedData := envstore.EnvInMemoryStoreObj.GetEnvStoreClone()
for key, value := range data { for key, value := range data {
if value != nil { if value != nil {
@@ -74,8 +94,31 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
updatedData.BoolEnv[constants.EnvKeyDisableMagicLinkLogin] = true updatedData.BoolEnv[constants.EnvKeyDisableMagicLinkLogin] = true
} }
} }
// check the roles change
if len(params.Roles) > 0 {
if len(params.DefaultRoles) > 0 {
// should be subset of roles
for _, role := range params.DefaultRoles {
if !utils.StringSliceContains(params.Roles, role) {
return res, fmt.Errorf("default role %s is not in roles", role)
}
}
}
}
if len(params.ProtectedRoles) > 0 {
for _, role := range params.ProtectedRoles {
if utils.StringSliceContains(params.Roles, role) || utils.StringSliceContains(params.DefaultRoles, role) {
return res, fmt.Errorf("protected role %s found roles or default roles", role)
}
}
}
// Update local store // Update local store
envstore.EnvInMemoryStoreObj.UpdateEnvStore(updatedData) envstore.EnvInMemoryStoreObj.UpdateEnvStore(updatedData)
sessionstore.InitSession()
oauth.InitOAuth()
// Fetch the current db store and update it // Fetch the current db store and update it
env, err := db.Provider.GetEnv() env, err := db.Provider.GetEnv()
@@ -83,32 +126,17 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
return res, err return res, err
} }
encryptedConfig, err := utils.EncryptEnvData(updatedData)
if err != nil {
return res, err
}
// in case of db change re-initialize db
if params.DatabaseType != nil || params.DatabaseURL != nil || params.DatabaseName != nil {
db.InitDB()
}
// in case of admin secret change update the cookie with new hash
if params.AdminSecret != nil { if params.AdminSecret != nil {
if params.OldAdminSecret == nil {
return res, errors.New("admin secret and old admin secret are required for secret change")
}
err := bcrypt.CompareHashAndPassword([]byte(*params.OldAdminSecret), []byte(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret)))
if err != nil {
return res, errors.New("old admin secret is not correct")
}
hashedKey, err := utils.EncryptPassword(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret)) hashedKey, err := utils.EncryptPassword(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
if err != nil { if err != nil {
return res, err return res, err
} }
utils.SetAdminCookie(gc, hashedKey) cookie.SetAdminCookie(gc, hashedKey)
}
encryptedConfig, err := utils.EncryptEnvData(updatedData)
if err != nil {
return res, err
} }
env.EnvData = encryptedConfig env.EnvData = encryptedConfig

View File

@@ -8,11 +8,13 @@ import (
"time" "time"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -25,29 +27,17 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
return res, err return res, err
} }
token, err := utils.GetAuthToken(gc) claims, err := token.ValidateAccessToken(gc)
if err != nil { if err != nil {
return res, err return res, err
} }
claim, err := utils.VerifyAuthToken(token)
if err != nil {
return res, err
}
id := fmt.Sprintf("%v", claim["id"])
sessionToken := session.GetUserSession(id, token)
if sessionToken == "" {
return res, fmt.Errorf(`unauthorized`)
}
// validate if all params are not empty // validate if all params are not empty
if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.OldPassword == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil { if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.OldPassword == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil {
return res, fmt.Errorf("please enter atleast one param to update") return res, fmt.Errorf("please enter at least one param to update")
} }
userEmail := fmt.Sprintf("%v", claim["email"]) userEmail := fmt.Sprintf("%v", claims["email"])
user, err := db.Provider.GetUserByEmail(userEmail) user, err := db.Provider.GetUserByEmail(userEmail)
if err != nil { if err != nil {
return res, err return res, err
@@ -123,20 +113,21 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
return res, fmt.Errorf("user with this email address already exists") return res, fmt.Errorf("user with this email address already exists")
} }
session.DeleteAllUserSession(fmt.Sprintf("%v", user.ID)) sessionstore.DeleteAllUserSession(fmt.Sprintf("%v", user.ID))
utils.DeleteCookie(gc) cookie.DeleteCookie(gc)
hostname := utils.GetHost(gc)
user.Email = newEmail user.Email = newEmail
user.EmailVerifiedAt = nil user.EmailVerifiedAt = nil
hasEmailChanged = true hasEmailChanged = true
// insert verification request // insert verification request
verificationType := constants.VerificationTypeUpdateEmail verificationType := constants.VerificationTypeUpdateEmail
token, err := utils.CreateVerificationToken(newEmail, verificationType) verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: token, Token: verificationToken,
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: newEmail, Email: newEmail,
@@ -144,7 +135,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency
go func() { go func() {
email.SendVerificationMail(newEmail, token) email.SendVerificationMail(newEmail, verificationToken, hostname)
}() }()
} }

View File

@@ -8,12 +8,14 @@ import (
"time" "time"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@@ -26,7 +28,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
return res, err return res, err
} }
if !utils.IsSuperAdmin(gc) { if !token.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized") return res, fmt.Errorf("unauthorized")
} }
@@ -93,19 +95,20 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
return res, fmt.Errorf("user with this email address already exists") return res, fmt.Errorf("user with this email address already exists")
} }
session.DeleteAllUserSession(fmt.Sprintf("%v", user.ID)) sessionstore.DeleteAllUserSession(fmt.Sprintf("%v", user.ID))
utils.DeleteCookie(gc) cookie.DeleteCookie(gc)
hostname := utils.GetHost(gc)
user.Email = newEmail user.Email = newEmail
user.EmailVerifiedAt = nil user.EmailVerifiedAt = nil
// insert verification request // insert verification request
verificationType := constants.VerificationTypeUpdateEmail verificationType := constants.VerificationTypeUpdateEmail
token, err := utils.CreateVerificationToken(newEmail, verificationType) verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: token, Token: verificationToken,
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: newEmail, Email: newEmail,
@@ -113,7 +116,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency
go func() { go func() {
email.SendVerificationMail(newEmail, token) email.SendVerificationMail(newEmail, verificationToken, hostname)
}() }()
} }
@@ -125,7 +128,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
inputRoles = append(inputRoles, *item) inputRoles = append(inputRoles, *item)
} }
if !utils.IsValidRoles(append([]string{}, append(envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyRoles), envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyProtectedRoles)...)...), inputRoles) { if !utils.IsValidRoles(inputRoles, append([]string{}, append(envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyRoles), envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyProtectedRoles)...)...)) {
return res, fmt.Errorf("invalid list of roles") return res, fmt.Errorf("invalid list of roles")
} }
@@ -133,8 +136,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
rolesToSave = strings.Join(inputRoles, ",") rolesToSave = strings.Join(inputRoles, ",")
} }
session.DeleteAllUserSession(fmt.Sprintf("%v", user.ID)) sessionstore.DeleteAllUserSession(fmt.Sprintf("%v", user.ID))
utils.DeleteCookie(gc) cookie.DeleteCookie(gc)
} }
if rolesToSave != "" { if rolesToSave != "" {

View File

@@ -6,29 +6,27 @@ import (
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
// UsersResolver is a resolver for users query // UsersResolver is a resolver for users query
// This is admin only query // This is admin only query
func UsersResolver(ctx context.Context) ([]*model.User, error) { func UsersResolver(ctx context.Context, params *model.PaginatedInput) (*model.Users, error) {
gc, err := utils.GinContextFromContext(ctx) gc, err := utils.GinContextFromContext(ctx)
var res []*model.User
if err != nil { if err != nil {
return res, err return nil, err
} }
if !utils.IsSuperAdmin(gc) { if !token.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
users, err := db.Provider.ListUsers() pagination := utils.GetPagination(params)
res, err := db.Provider.ListUsers(pagination)
if err != nil { if err != nil {
return res, err return nil, err
}
for i := 0; i < len(users); i++ {
res = append(res, utils.GetResponseUserData(users[i]))
} }
return res, nil return res, nil

View File

@@ -6,37 +6,27 @@ import (
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
// VerificationRequestsResolver is a resolver for verification requests query // VerificationRequestsResolver is a resolver for verification requests query
// This is admin only query // This is admin only query
func VerificationRequestsResolver(ctx context.Context) ([]*model.VerificationRequest, error) { func VerificationRequestsResolver(ctx context.Context, params *model.PaginatedInput) (*model.VerificationRequests, error) {
gc, err := utils.GinContextFromContext(ctx) gc, err := utils.GinContextFromContext(ctx)
var res []*model.VerificationRequest
if err != nil { if err != nil {
return res, err return nil, err
} }
if !utils.IsSuperAdmin(gc) { if !token.IsSuperAdmin(gc) {
return res, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
verificationRequests, err := db.Provider.ListVerificationRequests() pagination := utils.GetPagination(params)
res, err := db.Provider.ListVerificationRequests(pagination)
if err != nil { if err != nil {
return res, err return nil, err
}
for i := 0; i < len(verificationRequests); i++ {
res = append(res, &model.VerificationRequest{
ID: fmt.Sprintf("%v", verificationRequests[i].ID),
Email: &verificationRequests[i].Email,
Token: &verificationRequests[i].Token,
Identifier: &verificationRequests[i].Identifier,
Expires: &verificationRequests[i].ExpiresAt,
CreatedAt: &verificationRequests[i].CreatedAt,
UpdatedAt: &verificationRequests[i].UpdatedAt,
})
} }
return res, nil return res, nil

View File

@@ -6,10 +6,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/session" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
@@ -27,7 +28,7 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m
} }
// verify if token exists in db // verify if token exists in db
claim, err := utils.VerifyVerificationToken(params.Token) claim, err := token.VerifyVerificationToken(params.Token)
if err != nil { if err != nil {
return res, fmt.Errorf(`invalid token`) return res, fmt.Errorf(`invalid token`)
} }
@@ -45,20 +46,20 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m
db.Provider.DeleteVerificationRequest(verificationRequest) db.Provider.DeleteVerificationRequest(verificationRequest)
roles := strings.Split(user.Roles, ",") roles := strings.Split(user.Roles, ",")
refreshToken, _, _ := utils.CreateAuthToken(user, constants.TokenTypeRefreshToken, roles) authToken, err := token.CreateAuthToken(user, roles)
accessToken, expiresAt, _ := utils.CreateAuthToken(user, constants.TokenTypeAccessToken, roles) if err != nil {
return res, err
session.SetUserSession(user.ID, accessToken, refreshToken) }
sessionstore.SetUserSession(user.ID, authToken.FingerPrint, authToken.RefreshToken.Token)
cookie.SetCookie(gc, authToken.AccessToken.Token, authToken.RefreshToken.Token, authToken.FingerPrintHash)
utils.SaveSessionInDB(user.ID, gc) utils.SaveSessionInDB(user.ID, gc)
res = &model.AuthResponse{ res = &model.AuthResponse{
Message: `Email verified successfully.`, Message: `Email verified successfully.`,
AccessToken: &accessToken, AccessToken: &authToken.AccessToken.Token,
ExpiresAt: &expiresAt, ExpiresAt: &authToken.AccessToken.ExpiresAt,
User: utils.GetResponseUserData(user), User: user.AsAPIUser(),
} }
utils.SetCookie(gc, accessToken)
return res, nil return res, nil
} }

View File

@@ -1,18 +1,15 @@
package routes package routes
import ( import (
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/handlers" "github.com/authorizerdev/authorizer/server/handlers"
"github.com/authorizerdev/authorizer/server/middlewares" "github.com/authorizerdev/authorizer/server/middlewares"
"github.com/gin-contrib/location"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// InitRouter initializes gin router // InitRouter initializes gin router
func InitRouter() *gin.Engine { func InitRouter() *gin.Engine {
router := gin.Default() router := gin.Default()
router.Use(location.Default()) // router.Use(location.Default())
router.Use(middlewares.GinContextToContextMiddleware()) router.Use(middlewares.GinContextToContextMiddleware())
router.Use(middlewares.CORSMiddleware()) router.Use(middlewares.CORSMiddleware())
@@ -25,21 +22,21 @@ func InitRouter() *gin.Engine {
router.LoadHTMLGlob("templates/*") router.LoadHTMLGlob("templates/*")
// login page app related routes. // login page app related routes.
if !envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableLoginPage) { app := router.Group("/app")
app := router.Group("/app") {
{ app.Static("/favicon_io", "app/favicon_io")
app.Static("/build", "app/build") app.Static("/build", "app/build")
app.GET("/", handlers.AppHandler()) app.GET("/", handlers.AppHandler())
app.GET("/reset-password", handlers.AppHandler()) app.GET("/:page", handlers.AppHandler())
}
} }
// dashboard related routes // dashboard related routes
app := router.Group("/dashboard") dashboard := router.Group("/dashboard")
{ {
app.Static("/build", "dashboard/build") dashboard.Static("/favicon_io", "dashboard/favicon_io")
app.GET("/", handlers.DashboardHandler()) dashboard.Static("/build", "dashboard/build")
app.GET("/:page", handlers.DashboardHandler()) dashboard.GET("/", handlers.DashboardHandler())
dashboard.GET("/:page", handlers.DashboardHandler())
} }
return router return router
} }

View File

@@ -1,4 +1,4 @@
package session package sessionstore
import ( import (
"sync" "sync"
@@ -69,6 +69,19 @@ func (c *InMemoryStore) GetUserSession(userId, accessToken string) string {
return token return token
} }
// GetUserSessions returns all the user session token from the in-memory store.
func (c *InMemoryStore) GetUserSessions(userId string) map[string]string {
// c.mutex.Lock()
// defer c.mutex.Unlock()
sessionMap, ok := c.store[userId]
if !ok {
return nil
}
return sessionMap
}
// SetSocialLoginState sets the social login state in the in-memory store. // SetSocialLoginState sets the social login state in the in-memory store.
func (c *InMemoryStore) SetSocialLoginState(key, state string) { func (c *InMemoryStore) SetSocialLoginState(key, state string) {
c.mutex.Lock() c.mutex.Lock()

View File

@@ -1,4 +1,4 @@
package session package sessionstore
import ( import (
"context" "context"
@@ -60,6 +60,16 @@ func (c *RedisStore) GetUserSession(userId, accessToken string) string {
return token return token
} }
// GetUserSessions returns all the user session token from the redis store.
func (c *RedisStore) GetUserSessions(userID string) map[string]string {
res, err := c.store.HGetAll(c.ctx, "authorizer_"+userID).Result()
if err != nil {
log.Println("error getting token from redis store:", err)
}
return res
}
// SetSocialLoginState sets the social login state in redis store. // SetSocialLoginState sets the social login state in redis store.
func (c *RedisStore) SetSocialLoginState(key, state string) { func (c *RedisStore) SetSocialLoginState(key, state string) {
err := c.store.Set(c.ctx, key, state, 0).Err() err := c.store.Set(c.ctx, key, state, 0).Err()

View File

@@ -1,4 +1,4 @@
package session package sessionstore
import ( import (
"context" "context"
@@ -22,22 +22,22 @@ type SessionStore struct {
var SessionStoreObj SessionStore var SessionStoreObj SessionStore
// SetUserSession sets the user session in the session store // SetUserSession sets the user session in the session store
func SetUserSession(userId, accessToken, refreshToken string) { func SetUserSession(userId, fingerprint, refreshToken string) {
if SessionStoreObj.RedisMemoryStoreObj != nil { if SessionStoreObj.RedisMemoryStoreObj != nil {
SessionStoreObj.RedisMemoryStoreObj.AddUserSession(userId, accessToken, refreshToken) SessionStoreObj.RedisMemoryStoreObj.AddUserSession(userId, fingerprint, refreshToken)
} }
if SessionStoreObj.InMemoryStoreObj != nil { if SessionStoreObj.InMemoryStoreObj != nil {
SessionStoreObj.InMemoryStoreObj.AddUserSession(userId, accessToken, refreshToken) SessionStoreObj.InMemoryStoreObj.AddUserSession(userId, fingerprint, refreshToken)
} }
} }
// DeleteUserSession deletes the particular user session from the session store // DeleteUserSession deletes the particular user session from the session store
func DeleteUserSession(userId, accessToken string) { func DeleteUserSession(userId, fingerprint string) {
if SessionStoreObj.RedisMemoryStoreObj != nil { if SessionStoreObj.RedisMemoryStoreObj != nil {
SessionStoreObj.RedisMemoryStoreObj.DeleteUserSession(userId, accessToken) SessionStoreObj.RedisMemoryStoreObj.DeleteUserSession(userId, fingerprint)
} }
if SessionStoreObj.InMemoryStoreObj != nil { if SessionStoreObj.InMemoryStoreObj != nil {
SessionStoreObj.InMemoryStoreObj.DeleteUserSession(userId, accessToken) SessionStoreObj.InMemoryStoreObj.DeleteUserSession(userId, fingerprint)
} }
} }
@@ -52,17 +52,29 @@ func DeleteAllUserSession(userId string) {
} }
// GetUserSession returns the user session from the session store // GetUserSession returns the user session from the session store
func GetUserSession(userId, accessToken string) string { func GetUserSession(userId, fingerprint string) string {
if SessionStoreObj.RedisMemoryStoreObj != nil { if SessionStoreObj.RedisMemoryStoreObj != nil {
return SessionStoreObj.RedisMemoryStoreObj.GetUserSession(userId, accessToken) return SessionStoreObj.RedisMemoryStoreObj.GetUserSession(userId, fingerprint)
} }
if SessionStoreObj.InMemoryStoreObj != nil { if SessionStoreObj.InMemoryStoreObj != nil {
return SessionStoreObj.InMemoryStoreObj.GetUserSession(userId, accessToken) return SessionStoreObj.InMemoryStoreObj.GetUserSession(userId, fingerprint)
} }
return "" return ""
} }
// GetUserSessions returns all the user sessions from the session store
func GetUserSessions(userId string) map[string]string {
if SessionStoreObj.RedisMemoryStoreObj != nil {
return SessionStoreObj.RedisMemoryStoreObj.GetUserSessions(userId)
}
if SessionStoreObj.InMemoryStoreObj != nil {
return SessionStoreObj.InMemoryStoreObj.GetUserSessions(userId)
}
return nil
}
// ClearStore clears the session store for authorizer tokens // ClearStore clears the session store for authorizer tokens
func ClearStore() { func ClearStore() {
if SessionStoreObj.RedisMemoryStoreObj != nil { if SessionStoreObj.RedisMemoryStoreObj != nil {

View File

@@ -7,13 +7,12 @@ import (
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers" "github.com/authorizerdev/authorizer/server/resolvers"
"github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func adminSignupTests(t *testing.T, s TestSetup) { func adminSignupTests(t *testing.T, s TestSetup) {
t.Helper() t.Helper()
t.Run(`should complete admin login`, func(t *testing.T) { t.Run(`should complete admin signup`, func(t *testing.T) {
_, ctx := createContext(s) _, ctx := createContext(s)
_, err := resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{ _, err := resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{
AdminSecret: "admin", AdminSecret: "admin",
@@ -24,7 +23,7 @@ func adminSignupTests(t *testing.T, s TestSetup) {
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyAdminSecret, "") envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyAdminSecret, "")
_, err = resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{ _, err = resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{
AdminSecret: uuid.New().String(), AdminSecret: "admin123",
}) })
assert.Nil(t, err) assert.Nil(t, err)

View File

@@ -14,16 +14,13 @@ func TestEnvs(t *testing.T) {
env.InitEnv() env.InitEnv()
store := envstore.EnvInMemoryStoreObj.GetEnvStoreClone() store := envstore.EnvInMemoryStoreObj.GetEnvStoreClone()
assert.Equal(t, store.StringEnv[constants.EnvKeyAdminSecret], "admin")
assert.Equal(t, store.StringEnv[constants.EnvKeyEnv], "production") assert.Equal(t, store.StringEnv[constants.EnvKeyEnv], "production")
assert.False(t, store.BoolEnv[constants.EnvKeyDisableEmailVerification]) assert.False(t, store.BoolEnv[constants.EnvKeyDisableEmailVerification])
assert.False(t, store.BoolEnv[constants.EnvKeyDisableMagicLinkLogin]) assert.False(t, store.BoolEnv[constants.EnvKeyDisableMagicLinkLogin])
assert.False(t, store.BoolEnv[constants.EnvKeyDisableBasicAuthentication]) assert.False(t, store.BoolEnv[constants.EnvKeyDisableBasicAuthentication])
assert.Equal(t, store.StringEnv[constants.EnvKeyJwtType], "HS256") assert.Equal(t, store.StringEnv[constants.EnvKeyJwtType], "HS256")
assert.Equal(t, store.StringEnv[constants.EnvKeyJwtSecret], "random_string")
assert.Equal(t, store.StringEnv[constants.EnvKeyJwtRoleClaim], "role") assert.Equal(t, store.StringEnv[constants.EnvKeyJwtRoleClaim], "role")
assert.EqualValues(t, store.SliceEnv[constants.EnvKeyRoles], []string{"user"}) assert.EqualValues(t, store.SliceEnv[constants.EnvKeyRoles], []string{"user"})
assert.EqualValues(t, store.SliceEnv[constants.EnvKeyDefaultRoles], []string{"user"}) assert.EqualValues(t, store.SliceEnv[constants.EnvKeyDefaultRoles], []string{"user"})
assert.EqualValues(t, store.SliceEnv[constants.EnvKeyProtectedRoles], []string{"admin"})
assert.EqualValues(t, store.SliceEnv[constants.EnvKeyAllowedOrigins], []string{"*"}) assert.EqualValues(t, store.SliceEnv[constants.EnvKeyAllowedOrigins], []string{"*"})
} }

View File

@@ -0,0 +1,38 @@
package test
import (
"testing"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/authorizerdev/authorizer/server/token"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func isValidJWTTests(t *testing.T, s TestSetup) {
t.Helper()
_, ctx := createContext(s)
expiredToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbGxvd2VkX3JvbGVzIjpbIiJdLCJiaXJ0aGRhdGUiOm51bGwsImNyZWF0ZWRfYXQiOjAsImVtYWlsIjoiam9obi5kb2VAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJleHAiOjE2NDI5NjEwMTEsImV4dHJhIjp7IngtZXh0cmEtaWQiOiJkMmNhMjQwNy05MzZmLTQwYzQtOTQ2NS05Y2M5MWYxZTJhNDQifSwiZmFtaWx5X25hbWUiOm51bGwsImdlbmRlciI6bnVsbCwiZ2l2ZW5fbmFtZSI6bnVsbCwiaWF0IjoxNjQyOTYwOTgxLCJpZCI6ImQyY2EyNDA3LTkzNmYtNDBjNC05NDY1LTljYzkxZjFlMmE0NCIsIm1pZGRsZV9uYW1lIjpudWxsLCJuaWNrbmFtZSI6bnVsbCwicGhvbmVfbnVtYmVyIjpudWxsLCJwaG9uZV9udW1iZXJfdmVyaWZpZWQiOmZhbHNlLCJwaWN0dXJlIjpudWxsLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJqb2huLmRvZUBnbWFpbC5jb20iLCJyb2xlIjpbXSwic2lnbnVwX21ldGhvZHMiOiIiLCJ0b2tlbl90eXBlIjoiYWNjZXNzX3Rva2VuIiwidXBkYXRlZF9hdCI6MH0.FrdyeOC5e8uU1SowGj0omFJuwRnh4BrEk89S_fbEkzs"
t.Run(`should fail for invalid jwt`, func(t *testing.T) {
_, err := resolvers.IsValidJwtResolver(ctx, &model.IsValidJWTQueryInput{
Jwt: &expiredToken,
})
assert.NotNil(t, err)
})
t.Run(`should pass with valid jwt`, func(t *testing.T) {
authToken, err := token.CreateAuthToken(models.User{
ID: uuid.New().String(),
Email: "john.doe@gmail.com",
}, []string{})
assert.Nil(t, err)
res, err := resolvers.IsValidJwtResolver(ctx, &model.IsValidJWTQueryInput{
Jwt: &authToken.AccessToken.Token,
})
assert.Nil(t, err)
assert.True(t, res.Valid)
})
}

View File

@@ -2,6 +2,7 @@ package test
import ( import (
"fmt" "fmt"
"net/url"
"testing" "testing"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@@ -9,6 +10,8 @@ import (
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/resolvers" "github.com/authorizerdev/authorizer/server/resolvers"
"github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -27,12 +30,22 @@ func logoutTests(t *testing.T, s TestSetup) {
Token: verificationRequest.Token, Token: verificationRequest.Token,
}) })
sessions := sessionstore.GetUserSessions(verifyRes.User.ID)
fingerPrint := ""
refreshToken := ""
for key, val := range sessions {
fingerPrint = key
refreshToken = val
}
fingerPrintHash, _ := utils.EncryptAES([]byte(fingerPrint))
token := *verifyRes.AccessToken token := *verifyRes.AccessToken
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName), token)) cookie := fmt.Sprintf("%s=%s;%s=%s;%s=%s", envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+".fingerprint", url.QueryEscape(string(fingerPrintHash)), envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+".refresh_token", refreshToken, envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyCookieName)+".access_token", token)
req.Header.Set("Cookie", cookie)
_, err = resolvers.LogoutResolver(ctx) _, err = resolvers.LogoutResolver(ctx)
assert.Nil(t, err) assert.Nil(t, err)
_, err = resolvers.ProfileResolver(ctx)
assert.NotNil(t, err, "unauthorized")
cleanData(email) cleanData(email)
}) })
} }

Some files were not shown because too many files have changed in this diff Show More