Compare commits

...

94 Commits

Author SHA1 Message Date
Lakhan Samani
25cb9a831b feat: add cursor for pagination 2022-09-26 23:50:24 +05:30
Lakhan Samani
19e2153379 Update README.md 2022-09-15 12:24:47 +05:30
Lakhan Samani
221009bf0a Merge pull request #229 from ruessej/main
feat: Add a option to disable httpOnly cookies
2022-09-15 11:22:27 +05:30
ruessej
6085c2d535 Fix incorrect type 2022-09-14 12:24:19 +02:00
Jerebtw
8e0c5e4380 Make the default value true 2022-09-14 11:56:48 +02:00
Lakhan Samani
21b70e4b26 Merge pull request #230 from authorizerdev/fix/github-oauth-scopes
fix: scope for github auth
2022-09-14 11:46:46 +05:30
Lakhan Samani
993693884d fix: scope for github auth 2022-09-14 11:45:38 +05:30
Lakhan Samani
ed849fa6f6 Merge branch 'main' of https://github.com/authorizerdev/authorizer 2022-09-14 10:44:09 +05:30
Lakhan Samani
aec1f5df53 fix: github endpoint to get user emails 2022-09-14 10:44:01 +05:30
ruessej
195bd1bc6a Add a option to disable httpOnly cookies 2022-09-12 14:37:42 +02:00
Lakhan Samani
45b4c41bca Merge pull request #228 from Deep-Codes/main 2022-09-10 11:40:11 +05:30
Deepankar
63d486821e fix: lint 2022-09-10 11:39:01 +05:30
Deep-Codes
4b56afdc98 fix(type): __authorizer__ on window 2022-09-10 11:23:20 +05:30
Lakhan Samani
6455ff956a fix: remove varible log 2022-09-10 10:52:56 +05:30
Lakhan Samani
3898e43fff feat: add button to jwt config as json 2022-09-10 10:50:15 +05:30
Lakhan Samani
2c305e5bde Update README.md 2022-09-09 10:24:30 +05:30
Lakhan Samani
b8fd08e576 Update README.md 2022-09-09 09:29:27 +05:30
Lakhan Samani
6dafa45051 fix: invalid login message
Resolves #224
2022-09-03 21:48:33 +05:30
Lakhan Samani
ead3514113 chore: update railway template 2022-08-31 13:09:00 +05:30
Lakhan Samani
75a413e5f2 Merge branch 'main' of https://github.com/authorizerdev/authorizer 2022-08-31 11:02:50 +05:30
Lakhan Samani
91bf0e2478 fix: use replace all 2022-08-31 11:02:46 +05:30
Lakhan Samani
7a1305cf96 Merge pull request #222 from Deep-Codes/main 2022-08-31 07:04:20 +05:30
Deep-Codes
ff5a6ec301 feat(server): add log to show PORT 2022-08-30 23:35:43 +05:30
Lakhan Samani
b7b97b4f8d Merge pull request #221 from Deep-Codes/main
fix(dashboard): users table overflow
2022-08-30 22:38:49 +05:30
Deep-Codes
d9bc989c74 fix(dashboard): users table overflow 2022-08-30 21:56:28 +05:30
Lakhan Samani
d1f80d4088 feat: add support for twitter login 2022-08-29 08:37:53 +05:30
Lakhan Samani
4b299f0da2 fix: log 2022-08-29 08:19:11 +05:30
Lakhan Samani
ed8006db4c Merge branch 'main' of https://github.com/authorizerdev/authorizer 2022-08-29 08:18:42 +05:30
Lakhan Samani
97f6c7d50a fix: authorize endpoint setting user session 2022-08-29 08:18:20 +05:30
Lakhan Samani
5e3f68a180 Merge pull request #216 from szczepad/feat/twitter-login
Feat/twitter login
2022-08-24 08:53:52 +05:30
szczepad
f73d1fc588 feat: Adds login via twitter 2022-08-22 09:25:10 +02:00
szczepad
aa232de426 fix: Uses whitespace as seperator for oauth scopes in state-string
This is necessary, as the previous delimiter (,) was being redacted
after a redirect. This resulted in the scopes not being correctly
parseable and the state not being fetched correctly after the
oauth-callback
2022-08-22 09:25:10 +02:00
Lakhan Samani
34ce754ef6 feat: bootstrap twitter login config 2022-08-22 09:03:29 +02:00
Lakhan Samani
5f385b2016 fix: remove unused file 2022-08-18 07:21:50 +05:30
Lakhan Samani
da7c17271e Merge pull request #215 from wabscale/main
fix: rootless container
2022-08-18 06:11:32 +05:30
John McCann Cunniff Jr
69fbd631ff fix: rootless container 2022-08-17 20:33:05 -04:00
Lakhan Samani
deb209e358 Update README.md 2022-08-15 22:28:43 +05:30
Lakhan Samani
ea6b4cbc8d Update README.md 2022-08-15 22:28:13 +05:30
Lakhan Samani
2f21a09b2e chore: bump app/authorizer-react 1.0.0 2022-08-15 21:06:57 +05:30
Lakhan Samani
4ab775f2c1 fix: apple & linkedin env config 2022-08-13 12:37:04 +05:30
Lakhan Samani
b6e8023104 Merge pull request #211 from authorizerdev/fix/email-template
fix email template
2022-08-13 11:58:07 +05:30
Lakhan Samani
4f1597e5d2 fix: update note on features 2022-08-13 11:57:03 +05:30
Lakhan Samani
4f81d1969e fix email template
- fix verification types
- add design to cassandra db provider for email_template
- fix default email verification types to include update_email
2022-08-13 11:34:24 +05:30
Lakhan Samani
ad3e615ac7 Merge pull request #210 from authorizerdev/fix/dashboard-ui
Fix/dashboard UI
2022-08-13 03:57:19 +05:30
anik-ghosh-au7
e9a2301d2b feat: [dashboard] add env options for multi factor auth 2022-08-11 17:50:45 +05:30
anik-ghosh-au7
48bbfa31af fix: template editor design 2022-08-11 17:08:23 +05:30
anik-ghosh-au7
d7f5f563cc fix: add design to email template 2022-08-11 16:45:59 +05:30
anik-ghosh-au7
6c29149fbe fix: email template editor 2022-08-11 15:08:50 +05:30
Lakhan Samani
bbd4d43317 fix: add padding to editor 2022-08-09 12:10:50 +05:30
Lakhan Samani
c4d2f62657 fix: clear form on close 2022-08-09 11:55:55 +05:30
Lakhan Samani
5d78bf178f fix: email template info 2022-08-09 11:41:51 +05:30
Lakhan Samani
58749497bd fix: payload example for webhook 2022-08-09 10:04:06 +05:30
Lakhan Samani
5c6e643efb Merge pull request #209 from authorizerdev/feat/send-email-based-on-template
feat: send email based on template
2022-08-09 09:17:29 +05:30
Lakhan Samani
7792cdbc5e fix: template respone & ui 2022-08-09 09:07:47 +05:30
Lakhan Samani
65803c3763 fix: remove todos 2022-08-09 01:53:21 +05:30
Lakhan Samani
81fce1a471 feat: send email based on template 2022-08-09 01:43:37 +05:30
Lakhan Samani
0714b4360b Merge pull request #206 from authorizerdev/feat/2fa
feat: add mutifactor authentication
2022-08-07 11:11:56 +05:30
Lakhan Samani
8f69d5746e Merge pull request #207 from authorizerdev/feat/email-template-ui
feat: email template UI + subject
2022-08-07 11:10:44 +05:30
Lakhan Samani
ebc11906ef Merge branch 'feat/2fa' of https://github.com/authorizerdev/authorizer into feat/2fa 2022-08-03 23:20:37 +05:30
Lakhan Samani
465a92de22 feat: add managing mfa 2022-08-03 23:20:23 +05:30
Lakhan Samani
a890013317 Update generate_otp.go 2022-08-02 18:26:05 +05:30
Lakhan Samani
587828b59b feat: add helper for updating all users 2022-08-02 14:12:36 +05:30
anik-ghosh-au7
85630a59c1 feat: add webhook payload example 2022-08-02 00:56:21 +05:30
anik-ghosh-au7
b4ef196bfb fix: update email template variables 2022-08-01 14:07:06 +05:30
anik-ghosh-au7
099b2a39b4 feat: add delete email template modal 2022-07-30 22:47:00 +05:30
anik-ghosh-au7
2d07baedf4 feat: fix update email template editor 2022-07-30 20:28:36 +05:30
anik-ghosh-au7
8b34e001ef feat: fix update email template editor 2022-07-30 20:15:49 +05:30
anik-ghosh-au7
617dcdde53 feat: fix update email template modal 2022-07-30 18:43:02 +05:30
anik-ghosh-au7
f2fb800323 feat: dashboard add email-template page 2022-07-30 16:05:35 +05:30
Lakhan Samani
236045ac54 feat: add resend otp test 2022-07-30 01:12:20 +05:30
Lakhan Samani
d89be44fe5 feat: add sending otp 2022-07-29 19:49:50 +05:30
Lakhan Samani
db4d711cba feat: add subject to email template 2022-07-29 16:15:57 +05:30
Lakhan Samani
0fc9e8ccaa feat: add EnvKeyIsEmailServiceEnabled 2022-07-29 16:00:12 +05:30
anik-ghosh-au7
4e3d73e767 feat: otp resolvers updated 2022-07-29 13:49:46 +05:30
anik-ghosh-au7
e3c58ffbb0 fix: login resolver multifactor auth 2022-07-28 11:18:06 +05:30
anik-ghosh-au7
f12491e42d fix: auth response schema updated 2022-07-27 15:28:12 +05:30
anik-ghosh-au7
d653fac340 Merge branch 'feat/2fa' of https://github.com/authorizerdev/authorizer into feat/2fa 2022-07-27 12:18:51 +05:30
anik-ghosh-au7
9fae8215d2 feat: dashboard - add actions to update is_multi_factor_auth_enabled 2022-07-27 12:18:32 +05:30
Lakhan Samani
4e23e49de4 fix: syntax 2022-07-25 18:08:07 +05:30
anik-ghosh-au7
ef22318d5c feat: add generate_otp util 2022-07-24 10:40:37 +05:30
anik-ghosh-au7
480438fb7a fix: remove duplicate code in verify otp resolver 2022-07-23 20:04:39 +05:30
Lakhan Samani
8db6649e5c Merge pull request #205 from anik-ghosh-au7/feat/2fa
update: verify otp resolver and test added
2022-07-23 18:37:04 +05:30
anik-ghosh-au7
49cc6033ab update: verify otp resolver and test added 2022-07-23 18:32:31 +05:30
Lakhan Samani
5d903ca170 Merge branch 'main' of https://github.com/authorizerdev/authorizer into feat/2fa 2022-07-23 16:52:30 +05:30
Lakhan Samani
44280be25a feat: add resolver for verify_otp 2022-07-23 16:44:39 +05:30
Lakhan Samani
f6029fb7bf feat: use upsert for otp + implement otp methods for cassandradb 2022-07-23 16:39:35 +05:30
Lakhan Samani
22ae3bca54 feat: add otp implementation for arangodb 2022-07-23 16:06:52 +05:30
Lakhan Samani
1a27d91957 feat: add otp implementation for mongodb 2022-07-23 16:01:46 +05:30
Lakhan Samani
f6c67243b9 feat: add otp model + implementation for sql 2022-07-23 15:55:06 +05:30
Lakhan Samani
9ba1239c11 Merge pull request #204 from anik-ghosh-au7/main
fix: collections names
2022-07-23 15:46:35 +05:30
anik-ghosh-au7
ed7ed73980 fix: collections names 2022-07-23 15:44:56 +05:30
Lakhan Samani
9ef5f33f7a feat: add is_multi_factor_auth_enabled 2022-07-23 15:26:44 +05:30
Lakhan Samani
0f081ac3c8 Update README.md 2022-07-20 23:08:48 +05:30
Lakhan Samani
3aa0fb20ce Update CONTRIBUTING.md 2022-07-20 23:08:44 +05:30
115 changed files with 6685 additions and 1617 deletions

View File

@@ -49,7 +49,7 @@ Please ask as many questions as you need, either directly in the issue or on [Di
6. Build Dashboard `make build-dashboard` 6. Build Dashboard `make build-dashboard`
7. Build App `make build-app` 7. Build App `make build-app`
8. Build Server `make clean && make` 8. Build Server `make clean && make`
> Note: if you don't have [`make`](https://www.ibm.com/docs/en/aix/7.2?topic=concepts-make-command), you can `cd` into `server` dir and build using the `go build` command > Note: if you don't have [`make`](https://www.ibm.com/docs/en/aix/7.2?topic=concepts-make-command), you can `cd` into `server` dir and build using the `go build` command. In that case you will have to build `dashboard` & `app` manually using `npm run build` on both dirs.
9. Run binary `./build/server` 9. Run binary `./build/server`
### Testing ### Testing

View File

@@ -21,13 +21,15 @@ RUN apk add build-base &&\
make build-dashboard make build-dashboard
FROM alpine:latest FROM alpine:latest
WORKDIR /root/ RUN adduser -D -h /authorizer -u 1000 -k /dev/null authorizer
WORKDIR /authorizer
RUN mkdir app dashboard RUN mkdir app dashboard
COPY --from=node-builder /authorizer/app/build app/build COPY --from=node-builder --chown=nobody:nobody /authorizer/app/build app/build
COPY --from=node-builder /authorizer/app/favicon_io app/favicon_io COPY --from=node-builder --chown=nobody:nobody /authorizer/app/favicon_io app/favicon_io
COPY --from=node-builder /authorizer/dashboard/build dashboard/build COPY --from=node-builder --chown=nobody:nobody /authorizer/dashboard/build dashboard/build
COPY --from=node-builder /authorizer/dashboard/favicon_io dashboard/favicon_io COPY --from=node-builder --chown=nobody:nobody /authorizer/dashboard/favicon_io dashboard/favicon_io
COPY --from=go-builder /authorizer/build build COPY --from=go-builder --chown=nobody:nobody /authorizer/build build
COPY templates templates COPY templates templates
EXPOSE 8080 EXPOSE 8080
USER authorizer
CMD [ "./build/server" ] CMD [ "./build/server" ]

View File

@@ -11,14 +11,26 @@ clean:
rm -rf build rm -rf build
test: test:
rm -rf server/test/test.db && rm -rf test.db && cd server && go clean --testcache && TEST_DBS="sqlite" go test -p 1 -v ./test rm -rf server/test/test.db && rm -rf test.db && cd server && go clean --testcache && TEST_DBS="sqlite" go test -p 1 -v ./test
test-mongodb:
docker run -d --name authorizer_mongodb_db -p 27017:27017 mongo:4.4.15
cd server && go clean --testcache && TEST_DBS="mongodb" go test -p 1 -v ./test
docker rm -vf authorizer_mongodb_db
test-scylladb:
docker run -d --name authorizer_scylla_db -p 9042:9042 scylladb/scylla
cd server && go clean --testcache && TEST_DBS="scylladb" go test -p 1 -v ./test
docker rm -vf authorizer_scylla_db
test-arangodb:
docker run -d --name authorizer_arangodb -p 8529:8529 -e ARANGO_NO_AUTH=1 arangodb/arangodb:3.8.4
cd server && go clean --testcache && TEST_DBS="arangodb" go test -p 1 -v ./test
docker rm -vf authorizer_arangodb
test-all-db: test-all-db:
rm -rf server/test/test.db && rm -rf test.db rm -rf server/test/test.db && rm -rf test.db
docker run -d --name authorizer_scylla_db -p 9042:9042 scylladb/scylla docker run -d --name authorizer_scylla_db -p 9042:9042 scylladb/scylla
docker run -d --name authorizer_mongodb_db -p 27017:27017 mongo:4.4.15 docker run -d --name authorizer_mongodb_db -p 27017:27017 mongo:4.4.15
docker run -d --name authorizer_arangodb -p 8529:8529 -e ARANGO_NO_AUTH=1 arangodb/arangodb:3.8.4 docker run -d --name authorizer_arangodb -p 8529:8529 -e ARANGO_NO_AUTH=1 arangodb/arangodb:3.8.4
cd server && go clean --testcache && TEST_DBS="sqlite,mongodb,arangodb,scylladb" go test -p 1 -v ./test cd server && go clean --testcache && TEST_DBS="sqlite,mongodb,arangodb,scylladb" go test -p 1 -v ./test
docker rm -vf authorizer_mongodb_db
docker rm -vf authorizer_scylla_db docker rm -vf authorizer_scylla_db
docker rm -vf authorizer_mongodb_db
docker rm -vf authorizer_arangodb docker rm -vf authorizer_arangodb
generate: generate:
cd server && go get github.com/99designs/gqlgen/cmd@v0.14.0 && go run github.com/99designs/gqlgen generate cd server && go get github.com/99designs/gqlgen/cmd@v0.14.0 && go run github.com/99designs/gqlgen generate

View File

@@ -7,19 +7,17 @@
Authorizer Authorizer
</h1> </h1>
**Authorizer** is an open-source authentication and authorization solution for your applications. Bring your database and have complete control over the user information. You can self-host authorizer instances and connect to any database (Currently supports [Postgres](https://www.postgresql.org/), [MySQL](https://www.mysql.com/), [SQLite](https://www.sqlite.org/index.html), [SQLServer](https://www.microsoft.com/en-us/sql-server/), [MongoDB](https://mongodb.com/), [ArangoDB](https://www.arangodb.com/)). **Authorizer** is an open-source authentication and authorization solution for your applications. Bring your database and have complete control over the user information. You can self-host authorizer instances and connect to any database (Currently supports 11+ databases including [Postgres](https://www.postgresql.org/), [MySQL](https://www.mysql.com/), [SQLite](https://www.sqlite.org/index.html), [SQLServer](https://www.microsoft.com/en-us/sql-server/), [YugaByte](https://www.yugabyte.com/), [MariaDB](https://mariadb.org/), [PlanetScale](https://planetscale.com/), [CassandraDB](https://cassandra.apache.org/_/index.html), [ScyllaDB](https://www.scylladb.com/), [MongoDB](https://mongodb.com/), [ArangoDB](https://www.arangodb.com/)).
## Table of contents For more information check:
- [Introduction](#introduction)
- [Getting Started](#getting-started)
- [Contributing](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md)
- [Docs](http://docs.authorizer.dev/) - [Docs](http://docs.authorizer.dev/)
- [Join Community](https://discord.gg/Zv2D5h6kkK) - [Discord Community](https://discord.gg/Zv2D5h6kkK)
- [Contributing Guide](https://github.com/authorizerdev/authorizer/blob/main/.github/CONTRIBUTING.md)
# Introduction # Introduction
<img src="https://github.com/authorizerdev/authorizer/blob/main/assets/authorizer-architecture.png" style="height:20em"/> <img src="https://docs.authorizer.dev/images/authorizer-arch.png" style="height:20em"/>
#### We offer the following functionality #### We offer the following functionality
@@ -29,20 +27,22 @@
- ✅ OAuth2 and OpenID compatible APIs - ✅ OAuth2 and OpenID compatible APIs
- ✅ APIs to update profile securely - ✅ APIs to update profile securely
- ✅ Forgot password flow using email - ✅ Forgot password flow using email
- ✅ Social logins (Google, Github, Facebook, more coming soon) - ✅ Social logins (Google, Github, Facebook, LinkedIn, Apple more coming soon)
- ✅ Role-based access management - ✅ Role-based access management
- ✅ Password-less login with magic link login - ✅ Password-less login with magic link login
- ✅ Multi factor authentication
- ✅ Email templating
- ✅ Webhooks
## Roadmap ## Roadmap
- 2 Factor authentication - [VueJS SDK](https://github.com/authorizerdev/authorizer-vue)
- VueJS SDK - [Svelte SDK](https://github.com/authorizerdev/authorizer-svelte)
- Svelte SDK - [Golang SDK](https://github.com/authorizerdev/authorizer-go)
- React Native SDK - React Native SDK
- Flutter SDK - Flutter SDK
- Android Native SDK - Android Native SDK
- iOS native SDK - iOS native SDK
- Golang SDK
- Python SDK - Python SDK
- PHP SDK - PHP SDK
- WordPress plugin - WordPress plugin
@@ -63,11 +63,11 @@
Deploy production ready Authorizer instance using one click deployment options available below Deploy production ready Authorizer instance using one click deployment options available below
| **Infra provider** | **One-click link** | **Additional information** | | **Infra provider** | **One-click link** | **Additional information** |
| :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------: | | :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------: |
| 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) | | Railway.app | <a href="https://railway.app/new/template/nwXp1C?referralCode=FEF4uT"><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) | | 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) |
| 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) | | 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 Authorizer Using Source Code ### Deploy Authorizer Using Source Code
@@ -89,7 +89,7 @@ This guide helps you practice using Authorizer to evaluate it before you use it
5. Build Dashboard `make build-dashboard` 5. Build Dashboard `make build-dashboard`
6. Build App `make build-app` 6. Build App `make build-app`
7. Build Server `make clean && make` 7. Build Server `make clean && make`
> Note: if you don't have [`make`](https://www.ibm.com/docs/en/aix/7.2?topic=concepts-make-command), you can `cd` into `server` dir and build using the `go build` command > Note: if you don't have [`make`](https://www.ibm.com/docs/en/aix/7.2?topic=concepts-make-command), you can `cd` into `server` dir and build using the `go build` command. In that case you will have to build `dashboard` & `app` manually using `npm run build` on both dirs.
8. Run binary `./build/server` 8. Run binary `./build/server`
### Deploy Authorizer using binaries ### Deploy Authorizer using binaries

50
app/package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^0.25.0", "@authorizerdev/authorizer-react": "^1.1.0",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",
@@ -26,22 +26,22 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-js": { "node_modules/@authorizerdev/authorizer-js": {
"version": "0.14.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.0.tgz",
"integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==", "integrity": "sha512-MdEw1SjhIm7pXq20AscHSbnAta2PC3w7GNBY52/OzmlBXUGH3ooUQX/aszbYOse3FlhapcrGrRvg4sNM7faGAg==",
"dependencies": { "dependencies": {
"node-fetch": "^2.6.1" "cross-fetch": "^3.1.5"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@authorizerdev/authorizer-react": { "node_modules/@authorizerdev/authorizer-react": {
"version": "0.25.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.0.tgz",
"integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==", "integrity": "sha512-8ooyBREFI6ohHApVOPQitFr7T0w0SlpEVZruvU9oqa8OQ77UBxLQh+PCRKKPw7FeQRdCdh/VQyl17W7Xphp1NA==",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "^0.14.0", "@authorizerdev/authorizer-js": "^1.1.0",
"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"
@@ -404,6 +404,14 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
}, },
"node_modules/cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dependencies": {
"node-fetch": "2.6.7"
}
},
"node_modules/css-color-keywords": { "node_modules/css-color-keywords": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -852,19 +860,19 @@
}, },
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": { "@authorizerdev/authorizer-js": {
"version": "0.14.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.1.0.tgz",
"integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==", "integrity": "sha512-MdEw1SjhIm7pXq20AscHSbnAta2PC3w7GNBY52/OzmlBXUGH3ooUQX/aszbYOse3FlhapcrGrRvg4sNM7faGAg==",
"requires": { "requires": {
"node-fetch": "^2.6.1" "cross-fetch": "^3.1.5"
} }
}, },
"@authorizerdev/authorizer-react": { "@authorizerdev/authorizer-react": {
"version": "0.25.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.0.tgz",
"integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==", "integrity": "sha512-8ooyBREFI6ohHApVOPQitFr7T0w0SlpEVZruvU9oqa8OQ77UBxLQh+PCRKKPw7FeQRdCdh/VQyl17W7Xphp1NA==",
"requires": { "requires": {
"@authorizerdev/authorizer-js": "^0.14.0", "@authorizerdev/authorizer-js": "^1.1.0",
"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"
@@ -1161,6 +1169,14 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
}, },
"cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"requires": {
"node-fetch": "2.6.7"
}
},
"css-color-keywords": { "css-color-keywords": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",

View File

@@ -11,7 +11,7 @@
"author": "Lakhan Samani", "author": "Lakhan Samani",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^0.25.0", "@authorizerdev/authorizer-react": "^1.1.0",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",

View File

@@ -4,6 +4,12 @@ import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
import Root from './Root'; import Root from './Root';
import { createRandomString } from './utils/common'; import { createRandomString } from './utils/common';
declare global {
interface Window {
__authorizer__: any;
}
}
export default function App() { export default function App() {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const state = searchParams.get('state') || createRandomString(); const state = searchParams.get('state') || createRandomString();
@@ -24,7 +30,6 @@ export default function App() {
urlProps.redirectURL = window.location.origin + '/app'; urlProps.redirectURL = window.location.origin + '/app';
} }
const globalState: Record<string, string> = { const globalState: Record<string, string> = {
// @ts-ignore
...window['__authorizer__'], ...window['__authorizer__'],
...urlProps, ...urlProps,
}; };

View File

@@ -24,11 +24,16 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-draft-wysiwyg": "^1.15.0",
"react-dropzone": "^12.0.4", "react-dropzone": "^12.0.4",
"react-email-editor": "^1.6.1",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-router-dom": "^6.2.1", "react-router-dom": "^6.2.1",
"typescript": "^4.5.4", "typescript": "^4.5.4",
"urql": "^2.0.6" "urql": "^2.0.6"
},
"devDependencies": {
"@types/react-email-editor": "^1.1.7"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@@ -1191,6 +1196,15 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/react-email-editor": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@types/react-email-editor/-/react-email-editor-1.1.7.tgz",
"integrity": "sha512-OURTAgaE9pjA6KiU97k13fPdoglI1ZyowUuZ0nu5tTSyrw5PiZoYzYEf9y25YTjmw/ohxT5yqoP0tt+AjSh1qQ==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-router": { "node_modules/@types/react-router": {
"version": "5.1.17", "version": "5.1.17",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.17.tgz", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.17.tgz",
@@ -1306,6 +1320,11 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -1383,6 +1402,15 @@
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
}, },
"node_modules/draftjs-utils": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/draftjs-utils/-/draftjs-utils-0.10.2.tgz",
"integrity": "sha512-EstHqr3R3JVcilJrBaO/A+01GvwwKmC7e4TCjC7S94ZeMh4IVmf60OuQXtHHpwItK8C2JCi3iljgN5KHkJboUg==",
"peerDependencies": {
"draft-js": "^0.11.x",
"immutable": "3.x.x || 4.x.x"
}
},
"node_modules/error-ex": { "node_modules/error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -1802,6 +1830,15 @@
"react-is": "^16.7.0" "react-is": "^16.7.0"
} }
}, },
"node_modules/html-to-draftjs": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/html-to-draftjs/-/html-to-draftjs-1.5.0.tgz",
"integrity": "sha512-kggLXBNciKDwKf+KYsuE+V5gw4dZ7nHyGMX9m0wy7urzWjKGWyNFetmArRLvRV0VrxKN70WylFsJvMTJx02OBQ==",
"peerDependencies": {
"draft-js": "^0.10.x || ^0.11.x",
"immutable": "3.x.x || 4.x.x"
}
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -1856,6 +1893,14 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
}, },
"node_modules/linkify-it": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -1991,6 +2036,24 @@
"react": "17.0.2" "react": "17.0.2"
} }
}, },
"node_modules/react-draft-wysiwyg": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/react-draft-wysiwyg/-/react-draft-wysiwyg-1.15.0.tgz",
"integrity": "sha512-p1cYZcWc6/ALFBVksbFoCM3b29fGQDlZLIMrXng0TU/UElxIOF2/AWWo4L5auIYVhmqKTZ0NkNjnXOzGGuxyeA==",
"dependencies": {
"classnames": "^2.2.6",
"draftjs-utils": "^0.10.2",
"html-to-draftjs": "^1.5.0",
"linkify-it": "^2.2.0",
"prop-types": "^15.7.2"
},
"peerDependencies": {
"draft-js": "^0.10.x || ^0.11.x",
"immutable": "3.x.x || 4.x.x",
"react": "0.13.x || 0.14.x || ^15.0.0-0 || 15.x.x || ^16.0.0-0 || ^16.x.x || ^17.x.x || ^18.x.x",
"react-dom": "0.13.x || 0.14.x || ^15.0.0-0 || 15.x.x || ^16.0.0-0 || ^16.x.x || ^17.x.x || ^18.x.x"
}
},
"node_modules/react-dropzone": { "node_modules/react-dropzone": {
"version": "12.0.4", "version": "12.0.4",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz",
@@ -2007,6 +2070,14 @@
"react": ">= 16.8" "react": ">= 16.8"
} }
}, },
"node_modules/react-email-editor": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/react-email-editor/-/react-email-editor-1.6.1.tgz",
"integrity": "sha512-pEWpRmTY0ok03cwTGqEOoEldnzThhuRGTrcMnv8W3/jc5MTfcr9USU/IQ9HrVvFStLKoxYBIQnSKY+iCYWOtSQ==",
"peerDependencies": {
"react": "15.x || 16.x || 17.x"
}
},
"node_modules/react-fast-compare": { "node_modules/react-fast-compare": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
@@ -2275,6 +2346,11 @@
"node": ">=4.2.0" "node": ">=4.2.0"
} }
}, },
"node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"node_modules/urql": { "node_modules/urql": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/urql/-/urql-2.0.6.tgz", "resolved": "https://registry.npmjs.org/urql/-/urql-2.0.6.tgz",
@@ -3218,6 +3294,15 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/react-email-editor": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@types/react-email-editor/-/react-email-editor-1.1.7.tgz",
"integrity": "sha512-OURTAgaE9pjA6KiU97k13fPdoglI1ZyowUuZ0nu5tTSyrw5PiZoYzYEf9y25YTjmw/ohxT5yqoP0tt+AjSh1qQ==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-router": { "@types/react-router": {
"version": "5.1.17", "version": "5.1.17",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.17.tgz", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.17.tgz",
@@ -3316,6 +3401,11 @@
} }
} }
}, },
"classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"color-convert": { "color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -3390,6 +3480,12 @@
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
}, },
"draftjs-utils": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/draftjs-utils/-/draftjs-utils-0.10.2.tgz",
"integrity": "sha512-EstHqr3R3JVcilJrBaO/A+01GvwwKmC7e4TCjC7S94ZeMh4IVmf60OuQXtHHpwItK8C2JCi3iljgN5KHkJboUg==",
"requires": {}
},
"error-ex": { "error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -3659,6 +3755,12 @@
"react-is": "^16.7.0" "react-is": "^16.7.0"
} }
}, },
"html-to-draftjs": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/html-to-draftjs/-/html-to-draftjs-1.5.0.tgz",
"integrity": "sha512-kggLXBNciKDwKf+KYsuE+V5gw4dZ7nHyGMX9m0wy7urzWjKGWyNFetmArRLvRV0VrxKN70WylFsJvMTJx02OBQ==",
"requires": {}
},
"import-fresh": { "import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -3704,6 +3806,14 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
}, },
"linkify-it": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
"requires": {
"uc.micro": "^1.0.1"
}
},
"lodash": { "lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -3814,6 +3924,18 @@
"scheduler": "^0.20.2" "scheduler": "^0.20.2"
} }
}, },
"react-draft-wysiwyg": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/react-draft-wysiwyg/-/react-draft-wysiwyg-1.15.0.tgz",
"integrity": "sha512-p1cYZcWc6/ALFBVksbFoCM3b29fGQDlZLIMrXng0TU/UElxIOF2/AWWo4L5auIYVhmqKTZ0NkNjnXOzGGuxyeA==",
"requires": {
"classnames": "^2.2.6",
"draftjs-utils": "^0.10.2",
"html-to-draftjs": "^1.5.0",
"linkify-it": "^2.2.0",
"prop-types": "^15.7.2"
}
},
"react-dropzone": { "react-dropzone": {
"version": "12.0.4", "version": "12.0.4",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz",
@@ -3824,6 +3946,12 @@
"prop-types": "^15.8.1" "prop-types": "^15.8.1"
} }
}, },
"react-email-editor": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/react-email-editor/-/react-email-editor-1.6.1.tgz",
"integrity": "sha512-pEWpRmTY0ok03cwTGqEOoEldnzThhuRGTrcMnv8W3/jc5MTfcr9USU/IQ9HrVvFStLKoxYBIQnSKY+iCYWOtSQ==",
"requires": {}
},
"react-fast-compare": { "react-fast-compare": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
@@ -4020,6 +4148,11 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz",
"integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==" "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg=="
}, },
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"urql": { "urql": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/urql/-/urql-2.0.6.tgz", "resolved": "https://registry.npmjs.org/urql/-/urql-2.0.6.tgz",

View File

@@ -26,10 +26,15 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-draft-wysiwyg": "^1.15.0",
"react-dropzone": "^12.0.4", "react-dropzone": "^12.0.4",
"react-email-editor": "^1.6.1",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-router-dom": "^6.2.1", "react-router-dom": "^6.2.1",
"typescript": "^4.5.4", "typescript": "^4.5.4",
"urql": "^2.0.6" "urql": "^2.0.6"
},
"devDependencies": {
"@types/react-email-editor": "^1.1.7"
} }
} }

View File

@@ -0,0 +1,106 @@
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 { DeleteEmailTemplate } from '../graphql/mutation';
import { capitalizeFirstLetter } from '../utils';
interface deleteEmailTemplateModalInputPropTypes {
emailTemplateId: string;
eventName: string;
fetchEmailTemplatesData: Function;
}
const DeleteEmailTemplateModal = ({
emailTemplateId,
eventName,
fetchEmailTemplatesData,
}: deleteEmailTemplateModalInputPropTypes) => {
const client = useClient();
const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
const deleteHandler = async () => {
const res = await client
.mutation(DeleteEmailTemplate, { params: { id: emailTemplateId } })
.toPromise();
if (res.error) {
toast({
title: capitalizeFirstLetter(res.error.message),
isClosable: true,
status: 'error',
position: 'bottom-right',
});
return;
} else if (res.data?._delete_email_template) {
toast({
title: capitalizeFirstLetter(res.data?._delete_email_template.message),
isClosable: true,
status: 'success',
position: 'bottom-right',
});
}
onClose();
fetchEmailTemplatesData();
};
return (
<>
<MenuItem onClick={onOpen}>Delete</MenuItem>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Delete Email Template</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">
Email template for event <b>{eventName}</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 DeleteEmailTemplateModal;

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Flex, Stack, Text } from '@chakra-ui/react'; import { Divider, Flex, Stack, Text } from '@chakra-ui/react';
import InputField from '../InputField'; import InputField from '../InputField';
import { SwitchInputType } from '../../constants'; import { SwitchInputType } from '../../constants';
@@ -10,7 +10,7 @@ const Features = ({ variables, setVariables }: any) => {
<Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={5}> <Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={5}>
Disable Features Disable Features
</Text> </Text>
<Stack spacing={6} padding="2% 0%"> <Stack spacing={6}>
<Flex> <Flex>
<Flex w="100%" justifyContent="start" alignItems="center"> <Flex w="100%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Disable Login Page:</Text> <Text fontSize="sm">Disable Login Page:</Text>
@@ -83,6 +83,48 @@ const Features = ({ variables, setVariables }: any) => {
/> />
</Flex> </Flex>
</Flex> </Flex>
<Flex alignItems="center">
<Flex w="100%" alignItems="baseline" flexDir="column">
<Text fontSize="sm">
Disable Multi Factor Authentication (MFA):
</Text>
<Text fontSize="x-small">
Note: Enabling this will ignore Enforcing MFA shown below and will
also ignore the user MFA setting.
</Text>
</Flex>
<Flex justifyContent="start" mb={3}>
<InputField
variables={variables}
setVariables={setVariables}
inputType={SwitchInputType.DISABLE_MULTI_FACTOR_AUTHENTICATION}
/>
</Flex>
</Flex>
</Stack>
<Divider paddingY={5} />
<Text fontSize="md" paddingTop={5} fontWeight="bold" mb={5}>
Enable Features
</Text>
<Stack spacing={6}>
<Flex alignItems="center">
<Flex w="100%" alignItems="baseline" flexDir="column">
<Text fontSize="sm">
Enforce Multi Factor Authentication (MFA):
</Text>
<Text fontSize="x-small">
Note: If you disable enforcing after it was enabled, it will still
keep MFA enabled for older users.
</Text>
</Flex>
<Flex justifyContent="start" mb={3}>
<InputField
variables={variables}
setVariables={setVariables}
inputType={SwitchInputType.ENFORCE_MULTI_FACTOR_AUTHENTICATION}
/>
</Flex>
</Flex>
</Stack> </Stack>
</div> </div>
); );

View File

@@ -1,154 +1,201 @@
import React from "react"; import React from 'react';
import { Flex, Stack, Center, Text, useMediaQuery } from "@chakra-ui/react";
import { import {
HiddenInputType, Flex,
TextInputType, Stack,
TextAreaInputType, Center,
} from "../../constants"; Text,
import GenerateKeysModal from "../GenerateKeysModal"; useMediaQuery,
import InputField from "../InputField"; Button,
useToast,
} from '@chakra-ui/react';
import {
HiddenInputType,
TextInputType,
TextAreaInputType,
} from '../../constants';
import GenerateKeysModal from '../GenerateKeysModal';
import InputField from '../InputField';
import { copyTextToClipboard } from '../../utils';
const JSTConfigurations = ({ const JSTConfigurations = ({
variables, variables,
setVariables, setVariables,
fieldVisibility, fieldVisibility,
setFieldVisibility, setFieldVisibility,
SelectInputType, SelectInputType,
getData, getData,
HMACEncryptionType, HMACEncryptionType,
RSAEncryptionType, RSAEncryptionType,
ECDSAEncryptionType, ECDSAEncryptionType,
}: any) => { }: any) => {
const [isNotSmallerScreen] = useMediaQuery("(min-width:600px)"); const [isNotSmallerScreen] = useMediaQuery('(min-width:600px)');
const toast = useToast();
return ( const copyJSON = async () => {
<div> try {
{" "} await copyTextToClipboard(
<Flex JSON.stringify({
borderRadius={5} type: variables.JWT_TYPE,
width="100%" key: variables.JWT_PUBLIC_KEY || variables.JWT_SECRET,
justifyContent="space-between" })
alignItems="center" );
paddingTop="2%" toast({
> title: `JWT config copied successfully`,
<Text isClosable: true,
fontSize={isNotSmallerScreen ? "md" : "sm"} status: 'success',
fontWeight="bold" position: 'bottom-right',
mb={5} });
> } catch (err) {
JWT (JSON Web Tokens) Configurations console.error({
</Text> message: `Failed to copy JWT config`,
<Flex mb={7}> error: err,
<GenerateKeysModal jwtType={variables.JWT_TYPE} getData={getData} /> });
</Flex> toast({
</Flex> title: `Failed to copy JWT config`,
<Stack spacing={6} padding="2% 0%"> isClosable: true,
<Flex direction={isNotSmallerScreen ? "row" : "column"}> status: 'error',
<Flex w="30%" justifyContent="start" alignItems="center"> position: 'bottom-right',
<Text fontSize="sm">JWT Type:</Text> });
</Flex> }
<Flex };
w={isNotSmallerScreen ? "70%" : "100%"}
mt={isNotSmallerScreen ? "0" : "2"} return (
> <div>
<InputField {' '}
borderRadius={5} <Flex
variables={variables} borderRadius={5}
setVariables={setVariables} width="100%"
inputType={SelectInputType} justifyContent="space-between"
value={SelectInputType} alignItems="center"
options={{ paddingTop="2%"
...HMACEncryptionType, >
...RSAEncryptionType, <Text
...ECDSAEncryptionType, fontSize={isNotSmallerScreen ? 'md' : 'sm'}
}} fontWeight="bold"
/> mb={5}
</Flex> >
</Flex> JWT (JSON Web Tokens) Configurations
{Object.values(HMACEncryptionType).includes(variables.JWT_TYPE) ? ( </Text>
<Flex direction={isNotSmallerScreen ? "row" : "column"}> <Flex mb={7}>
<Flex w="30%" justifyContent="start" alignItems="center"> <Button
<Text fontSize="sm">JWT Secret</Text> colorScheme="blue"
</Flex> h="1.75rem"
<Center size="sm"
w={isNotSmallerScreen ? "70%" : "100%"} variant="ghost"
mt={isNotSmallerScreen ? "0" : "2"} onClick={copyJSON}
> >
<InputField Copy As JSON Config
borderRadius={5} </Button>
variables={variables} <GenerateKeysModal jwtType={variables.JWT_TYPE} getData={getData} />
setVariables={setVariables} </Flex>
fieldVisibility={fieldVisibility} </Flex>
setFieldVisibility={setFieldVisibility} <Stack spacing={6} padding="2% 0%">
inputType={HiddenInputType.JWT_SECRET} <Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
/> <Flex w="30%" justifyContent="start" alignItems="center">
</Center> <Text fontSize="sm">JWT Type:</Text>
</Flex> </Flex>
) : ( <Flex
<> w={isNotSmallerScreen ? '70%' : '100%'}
<Flex direction={isNotSmallerScreen ? "row" : "column"}> mt={isNotSmallerScreen ? '0' : '2'}
<Flex w="30%" justifyContent="start" alignItems="center"> >
<Text fontSize="sm">Public Key</Text> <InputField
</Flex> borderRadius={5}
<Center variables={variables}
w={isNotSmallerScreen ? "70%" : "100%"} setVariables={setVariables}
mt={isNotSmallerScreen ? "0" : "2"} inputType={SelectInputType}
> value={SelectInputType}
<InputField options={{
borderRadius={5} ...HMACEncryptionType,
variables={variables} ...RSAEncryptionType,
setVariables={setVariables} ...ECDSAEncryptionType,
inputType={TextAreaInputType.JWT_PUBLIC_KEY} }}
placeholder="Add public key here" />
minH="25vh" </Flex>
/> </Flex>
</Center> {Object.values(HMACEncryptionType).includes(variables.JWT_TYPE) ? (
</Flex> <Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
<Flex direction={isNotSmallerScreen ? "row" : "column"}> <Flex w="30%" justifyContent="start" alignItems="center">
<Flex w="30%" justifyContent="start" alignItems="center"> <Text fontSize="sm">JWT Secret</Text>
<Text fontSize="sm">Private Key</Text> </Flex>
</Flex> <Center
<Center w={isNotSmallerScreen ? '70%' : '100%'}
w={isNotSmallerScreen ? "70%" : "100%"} mt={isNotSmallerScreen ? '0' : '2'}
mt={isNotSmallerScreen ? "0" : "2"} >
> <InputField
<InputField borderRadius={5}
borderRadius={5} variables={variables}
variables={variables} setVariables={setVariables}
setVariables={setVariables} fieldVisibility={fieldVisibility}
inputType={TextAreaInputType.JWT_PRIVATE_KEY} setFieldVisibility={setFieldVisibility}
placeholder="Add private key here" inputType={HiddenInputType.JWT_SECRET}
minH="25vh" />
/> </Center>
</Center> </Flex>
</Flex> ) : (
</> <>
)} <Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
<Flex direction={isNotSmallerScreen ? "row" : "column"}> <Flex w="30%" justifyContent="start" alignItems="center">
<Flex <Text fontSize="sm">Public Key</Text>
w={isNotSmallerScreen ? "30%" : "40%"} </Flex>
justifyContent="start" <Center
alignItems="center" w={isNotSmallerScreen ? '70%' : '100%'}
> mt={isNotSmallerScreen ? '0' : '2'}
<Text fontSize="sm" orientation="vertical"> >
JWT Role Claim: <InputField
</Text> borderRadius={5}
</Flex> variables={variables}
<Center setVariables={setVariables}
w={isNotSmallerScreen ? "70%" : "100%"} inputType={TextAreaInputType.JWT_PUBLIC_KEY}
mt={isNotSmallerScreen ? "0" : "2"} placeholder="Add public key here"
> minH="25vh"
<InputField />
borderRadius={5} </Center>
variables={variables} </Flex>
setVariables={setVariables} <Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
inputType={TextInputType.JWT_ROLE_CLAIM} <Flex w="30%" justifyContent="start" alignItems="center">
/> <Text fontSize="sm">Private Key</Text>
</Center> </Flex>
</Flex> <Center
</Stack> w={isNotSmallerScreen ? '70%' : '100%'}
</div> mt={isNotSmallerScreen ? '0' : '2'}
); >
<InputField
borderRadius={5}
variables={variables}
setVariables={setVariables}
inputType={TextAreaInputType.JWT_PRIVATE_KEY}
placeholder="Add private key here"
minH="25vh"
/>
</Center>
</Flex>
</>
)}
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
<Flex
w={isNotSmallerScreen ? '30%' : '40%'}
justifyContent="start"
alignItems="center"
>
<Text fontSize="sm" orientation="vertical">
JWT Role Claim:
</Text>
</Flex>
<Center
w={isNotSmallerScreen ? '70%' : '100%'}
mt={isNotSmallerScreen ? '0' : '2'}
>
<InputField
borderRadius={5}
variables={variables}
setVariables={setVariables}
inputType={TextInputType.JWT_ROLE_CLAIM}
/>
</Center>
</Flex>
</Stack>
</div>
);
}; };
export default JSTConfigurations; export default JSTConfigurations;

View File

@@ -15,6 +15,7 @@ import {
FaFacebookF, FaFacebookF,
FaLinkedin, FaLinkedin,
FaApple, FaApple,
FaTwitter,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { TextInputType, HiddenInputType } from '../../constants'; import { TextInputType, HiddenInputType } from '../../constants';
@@ -108,7 +109,7 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility} fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility} setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.GOOGLE_CLIENT_SECRET} inputType={HiddenInputType.GOOGLE_CLIENT_SECRET}
placeholder="Google Secret" placeholder="Google Client Secret"
/> />
</Center> </Center>
</Flex> </Flex>
@@ -146,7 +147,7 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility} fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility} setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.GITHUB_CLIENT_SECRET} inputType={HiddenInputType.GITHUB_CLIENT_SECRET}
placeholder="Github Secret" placeholder="Github Client Secret"
/> />
</Center> </Center>
</Flex> </Flex>
@@ -184,7 +185,7 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility} fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility} setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.FACEBOOK_CLIENT_SECRET} inputType={HiddenInputType.FACEBOOK_CLIENT_SECRET}
placeholder="Facebook Secret" placeholder="Facebook Client Secret"
/> />
</Center> </Center>
</Flex> </Flex>
@@ -260,7 +261,45 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility} fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility} setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.APPLE_CLIENT_SECRET} inputType={HiddenInputType.APPLE_CLIENT_SECRET}
placeholder="Apple CLient Secret" placeholder="Apple Client Secret"
/>
</Center>
</Flex>
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
<Center
w={isNotSmallerScreen ? '55px' : '35px'}
h="35px"
marginRight="1.5%"
border="1px solid #3b5998"
borderRadius="5px"
>
<FaTwitter />
</Center>
<Center
w={isNotSmallerScreen ? '70%' : '100%'}
mt={isNotSmallerScreen ? '0' : '3'}
marginRight="1.5%"
>
<InputField
borderRadius={5}
variables={envVariables}
setVariables={setVariables}
inputType={TextInputType.TWITTER_CLIENT_ID}
placeholder="Twitter Client ID"
/>
</Center>
<Center
w={isNotSmallerScreen ? '70%' : '100%'}
mt={isNotSmallerScreen ? '0' : '3'}
>
<InputField
borderRadius={5}
variables={envVariables}
setVariables={setVariables}
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.TWITTER_CLIENT_SECRET}
placeholder="Twitter Client Secret"
/> />
</Center> </Center>
</Flex> </Flex>

View File

@@ -12,7 +12,6 @@ import {
Select, Select,
Textarea, Textarea,
Switch, Switch,
Code,
Text, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { import {

View File

@@ -31,6 +31,7 @@ import {
FiUsers, FiUsers,
FiChevronDown, FiChevronDown,
FiLink, FiLink,
FiFileText,
} from 'react-icons/fi'; } from 'react-icons/fi';
import { BiCustomize } from 'react-icons/bi'; import { BiCustomize } from 'react-icons/bi';
import { AiOutlineKey } from 'react-icons/ai'; import { AiOutlineKey } from 'react-icons/ai';
@@ -113,6 +114,7 @@ const LinkItems: Array<LinkItemProps> = [
}, },
{ name: 'Users', icon: FiUsers, route: '/users' }, { name: 'Users', icon: FiUsers, route: '/users' },
{ name: 'Webhooks', icon: FiLink, route: '/webhooks' }, { name: 'Webhooks', icon: FiLink, route: '/webhooks' },
{ name: 'Email Templates', icon: FiFileText, route: '/email-templates' },
]; ];
interface SidebarProps extends BoxProps { interface SidebarProps extends BoxProps {

View File

@@ -0,0 +1,457 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
Center,
Flex,
Input,
InputGroup,
MenuItem,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Select,
Text,
useDisclosure,
useToast,
Alert,
AlertIcon,
Collapse,
Box,
TableContainer,
Table,
Thead,
Tr,
Th,
Tbody,
Td,
Code,
} from '@chakra-ui/react';
import { FaPlus, FaAngleDown, FaAngleUp } from 'react-icons/fa';
import { useClient } from 'urql';
import EmailEditor from 'react-email-editor';
import {
UpdateModalViews,
EmailTemplateInputDataFields,
emailTemplateEventNames,
emailTemplateVariables,
} from '../constants';
import { capitalizeFirstLetter } from '../utils';
import { AddEmailTemplate, EditEmailTemplate } from '../graphql/mutation';
interface selectedEmailTemplateDataTypes {
[EmailTemplateInputDataFields.ID]: string;
[EmailTemplateInputDataFields.EVENT_NAME]: string;
[EmailTemplateInputDataFields.SUBJECT]: string;
[EmailTemplateInputDataFields.CREATED_AT]: number;
[EmailTemplateInputDataFields.TEMPLATE]: string;
[EmailTemplateInputDataFields.DESIGN]: string;
}
interface UpdateEmailTemplateInputPropTypes {
view: UpdateModalViews;
selectedTemplate?: selectedEmailTemplateDataTypes;
fetchEmailTemplatesData: Function;
}
interface templateVariableDataTypes {
text: string;
value: string;
description: string;
}
interface emailTemplateDataType {
[EmailTemplateInputDataFields.EVENT_NAME]: string;
[EmailTemplateInputDataFields.SUBJECT]: string;
}
interface validatorDataType {
[EmailTemplateInputDataFields.SUBJECT]: boolean;
}
const initTemplateData: emailTemplateDataType = {
[EmailTemplateInputDataFields.EVENT_NAME]: emailTemplateEventNames.Signup,
[EmailTemplateInputDataFields.SUBJECT]: '',
};
const initTemplateValidatorData: validatorDataType = {
[EmailTemplateInputDataFields.SUBJECT]: true,
};
const UpdateEmailTemplate = ({
view,
selectedTemplate,
fetchEmailTemplatesData,
}: UpdateEmailTemplateInputPropTypes) => {
const client = useClient();
const toast = useToast();
const emailEditorRef = useRef(null);
const { isOpen, onOpen, onClose } = useDisclosure();
const [loading, setLoading] = useState<boolean>(false);
const [templateVariables, setTemplateVariables] = useState<
templateVariableDataTypes[]
>([]);
const [templateData, setTemplateData] = useState<emailTemplateDataType>({
...initTemplateData,
});
const [validator, setValidator] = useState<validatorDataType>({
...initTemplateValidatorData,
});
const [isDynamicVariableInfoOpen, setIsDynamicVariableInfoOpen] =
useState<boolean>(false);
const onReady = () => {
if (selectedTemplate) {
const { design } = selectedTemplate;
try {
const designData = JSON.parse(design);
// @ts-ignore
emailEditorRef.current.editor.loadDesign(designData);
} catch (error) {
console.error(error);
onClose();
}
}
};
const inputChangehandler = (inputType: string, value: any) => {
if (inputType !== EmailTemplateInputDataFields.EVENT_NAME) {
setValidator({
...validator,
[inputType]: value?.trim().length,
});
}
setTemplateData({ ...templateData, [inputType]: value });
};
const validateData = () => {
return (
!loading &&
templateData[EmailTemplateInputDataFields.EVENT_NAME].length > 0 &&
templateData[EmailTemplateInputDataFields.SUBJECT].length > 0 &&
validator[EmailTemplateInputDataFields.SUBJECT]
);
};
const saveData = async () => {
if (!validateData()) return;
setLoading(true);
// @ts-ignore
return await emailEditorRef.current.editor.exportHtml(async (data) => {
const { design, html } = data;
if (!html || !design) {
setLoading(false);
return;
}
const params = {
[EmailTemplateInputDataFields.EVENT_NAME]:
templateData[EmailTemplateInputDataFields.EVENT_NAME],
[EmailTemplateInputDataFields.SUBJECT]:
templateData[EmailTemplateInputDataFields.SUBJECT],
[EmailTemplateInputDataFields.TEMPLATE]: html.trim(),
[EmailTemplateInputDataFields.DESIGN]: JSON.stringify(design),
};
let res: any = {};
if (
view === UpdateModalViews.Edit &&
selectedTemplate?.[EmailTemplateInputDataFields.ID]
) {
res = await client
.mutation(EditEmailTemplate, {
params: {
...params,
id: selectedTemplate[EmailTemplateInputDataFields.ID],
},
})
.toPromise();
} else {
res = await client.mutation(AddEmailTemplate, { params }).toPromise();
}
setLoading(false);
if (res.error) {
toast({
title: capitalizeFirstLetter(res.error.message),
isClosable: true,
status: 'error',
position: 'bottom-right',
});
} else if (
res.data?._add_email_template ||
res.data?._update_email_template
) {
toast({
title: capitalizeFirstLetter(
res.data?._add_email_template?.message ||
res.data?._update_email_template?.message
),
isClosable: true,
status: 'success',
position: 'bottom-right',
});
setTemplateData({
...initTemplateData,
});
setValidator({ ...initTemplateValidatorData });
fetchEmailTemplatesData();
}
view === UpdateModalViews.ADD && onClose();
});
};
const resetData = () => {
if (selectedTemplate) {
setTemplateData(selectedTemplate);
} else {
setTemplateData({ ...initTemplateData });
}
};
useEffect(() => {
if (
isOpen &&
view === UpdateModalViews.Edit &&
selectedTemplate &&
Object.keys(selectedTemplate || {}).length
) {
const { id, created_at, template, design, ...rest } = selectedTemplate;
setTemplateData(rest);
}
}, [isOpen]);
useEffect(() => {
const updatedTemplateVariables = Object.entries(
emailTemplateVariables
).reduce((acc, [key, val]): any => {
if (
(templateData[EmailTemplateInputDataFields.EVENT_NAME] !==
emailTemplateEventNames['Verify Otp'] &&
val === emailTemplateVariables.otp) ||
(templateData[EmailTemplateInputDataFields.EVENT_NAME] ===
emailTemplateEventNames['Verify Otp'] &&
val === emailTemplateVariables.verification_url)
) {
return acc;
}
return [
...acc,
{
text: key,
value: val.value,
description: val.description,
},
];
}, []);
setTemplateVariables(updatedTemplateVariables);
}, [templateData[EmailTemplateInputDataFields.EVENT_NAME]]);
return (
<>
{view === UpdateModalViews.ADD ? (
<Button
leftIcon={<FaPlus />}
colorScheme="blue"
variant="solid"
onClick={onOpen}
isDisabled={false}
size="sm"
>
<Center h="100%">Add Template</Center>{' '}
</Button>
) : (
<MenuItem onClick={onOpen}>Edit</MenuItem>
)}
<Modal
isOpen={isOpen}
onClose={() => {
resetData();
onClose();
}}
size="6xl"
>
<ModalOverlay />
<ModalContent>
<ModalHeader>
{view === UpdateModalViews.ADD
? 'Add New Email Template'
: 'Edit Email Template'}
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex
flexDirection="column"
border="1px"
borderRadius="md"
borderColor="gray.200"
p="5"
>
<Alert
status="info"
onClick={() =>
setIsDynamicVariableInfoOpen(!isDynamicVariableInfoOpen)
}
borderRadius="5"
marginBottom={5}
cursor="pointer"
fontSize="sm"
>
<AlertIcon />
<Flex
width="100%"
justifyContent="space-between"
alignItems="center"
>
<Box width="85%">
<b>Note:</b> You can add set of dynamic variables to subject
and email body. Click here to see the set of dynamic
variables.
</Box>
{isDynamicVariableInfoOpen ? <FaAngleUp /> : <FaAngleDown />}
</Flex>
</Alert>
<Collapse
style={{
width: '100%',
}}
in={isDynamicVariableInfoOpen}
>
<TableContainer
background="gray.100"
borderRadius={5}
height={200}
width="100%"
overflowY="auto"
overflowWrap="break-word"
>
<Table variant="simple">
<Thead>
<Tr>
<Th>Variable</Th>
<Th>Description</Th>
</Tr>
</Thead>
<Tbody>
{templateVariables.map((i) => (
<Tr key={i.text}>
<Td>
<Code fontSize="sm">{`{{.${i.text}}}`}</Code>
</Td>
<Td>
<Text
size="sm"
fontSize="sm"
overflowWrap="break-word"
width="100%"
>
{i.description}
</Text>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Collapse>
<Flex
width="100%"
justifyContent="space-between"
alignItems="center"
marginBottom="2%"
>
<Flex flex="1">Event Name</Flex>
<Flex flex="3">
<Select
size="md"
value={
templateData[EmailTemplateInputDataFields.EVENT_NAME]
}
onChange={(e) =>
inputChangehandler(
EmailTemplateInputDataFields.EVENT_NAME,
e.currentTarget.value
)
}
>
{Object.entries(emailTemplateEventNames).map(
([key, value]: any) => (
<option value={value} key={key}>
{key}
</option>
)
)}
</Select>
</Flex>
</Flex>
<Flex
width="100%"
justifyContent="start"
alignItems="center"
marginBottom="2%"
>
<Flex flex="1">Subject</Flex>
<Flex flex="3">
<InputGroup size="md">
<Input
pr="4.5rem"
type="text"
placeholder="Subject Line"
value={templateData[EmailTemplateInputDataFields.SUBJECT]}
isInvalid={
!validator[EmailTemplateInputDataFields.SUBJECT]
}
onChange={(e) =>
inputChangehandler(
EmailTemplateInputDataFields.SUBJECT,
e.currentTarget.value
)
}
/>
</InputGroup>
</Flex>
</Flex>
<Flex
width="100%"
justifyContent="flex-start"
alignItems="center"
marginBottom="2%"
>
Template Body
</Flex>
<Flex
width="100%"
justifyContent="flex-start"
alignItems="center"
border="1px solid"
borderColor="gray.200"
>
<EmailEditor ref={emailEditorRef} onReady={onReady} />
</Flex>
</Flex>
</ModalBody>
<ModalFooter>
<Button
variant="outline"
onClick={resetData}
isDisabled={loading}
marginRight="5"
>
Reset
</Button>
<Button
colorScheme="blue"
variant="solid"
isLoading={loading}
onClick={saveData}
isDisabled={!validateData()}
>
<Center h="100%" pt="5%">
Save
</Center>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};
export default UpdateEmailTemplate;

View File

@@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react';
import { import {
Button, Button,
Center, Center,
Code,
Collapse,
Flex, Flex,
Input, Input,
InputGroup, InputGroup,
@@ -19,20 +21,33 @@ import {
Text, Text,
useDisclosure, useDisclosure,
useToast, useToast,
Alert,
AlertIcon,
Divider,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { FaMinusCircle, FaPlus } from 'react-icons/fa'; import {
FaAngleDown,
FaAngleUp,
FaMinusCircle,
FaPlus,
FaRegClone,
} from 'react-icons/fa';
import { useClient } from 'urql'; import { useClient } from 'urql';
import { import {
webhookEventNames, webhookEventNames,
ArrayInputOperations, ArrayInputOperations,
WebhookInputDataFields, WebhookInputDataFields,
WebhookInputHeaderFields, WebhookInputHeaderFields,
UpdateWebhookModalViews, UpdateModalViews,
webhookVerifiedStatus, webhookVerifiedStatus,
webhookPayloadExample,
} from '../constants'; } from '../constants';
import { capitalizeFirstLetter, validateURI } from '../utils'; import {
capitalizeFirstLetter,
copyTextToClipboard,
validateURI,
} from '../utils';
import { AddWebhook, EditWebhook, TestEndpoint } from '../graphql/mutation'; import { AddWebhook, EditWebhook, TestEndpoint } from '../graphql/mutation';
import { rest } from 'lodash';
import { BiCheckCircle, BiError, BiErrorCircle } from 'react-icons/bi'; import { BiCheckCircle, BiError, BiErrorCircle } from 'react-icons/bi';
interface headersDataType { interface headersDataType {
@@ -54,7 +69,7 @@ interface selecetdWebhookDataTypes {
} }
interface UpdateWebhookModalInputPropTypes { interface UpdateWebhookModalInputPropTypes {
view: UpdateWebhookModalViews; view: UpdateModalViews;
selectedWebhook?: selecetdWebhookDataTypes; selectedWebhook?: selecetdWebhookDataTypes;
fetchWebookData: Function; fetchWebookData: Function;
} }
@@ -82,7 +97,7 @@ interface validatorDataType {
} }
const initWebhookData: webhookDataType = { const initWebhookData: webhookDataType = {
[WebhookInputDataFields.EVENT_NAME]: webhookEventNames.USER_LOGIN, [WebhookInputDataFields.EVENT_NAME]: webhookEventNames['User login'],
[WebhookInputDataFields.ENDPOINT]: '', [WebhookInputDataFields.ENDPOINT]: '',
[WebhookInputDataFields.ENABLED]: true, [WebhookInputDataFields.ENABLED]: true,
[WebhookInputDataFields.HEADERS]: [{ ...initHeadersData }], [WebhookInputDataFields.HEADERS]: [{ ...initHeadersData }],
@@ -103,6 +118,7 @@ const UpdateWebhookModal = ({
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [verifyingEndpoint, setVerifyingEndpoint] = useState<boolean>(false); const [verifyingEndpoint, setVerifyingEndpoint] = useState<boolean>(false);
const [isShowingPayload, setIsShowingPayload] = useState<boolean>(false);
const [webhook, setWebhook] = useState<webhookDataType>({ const [webhook, setWebhook] = useState<webhookDataType>({
...initWebhookData, ...initWebhookData,
}); });
@@ -254,7 +270,7 @@ const UpdateWebhookModal = ({
const params = getParams(); const params = getParams();
let res: any = {}; let res: any = {};
if ( if (
view === UpdateWebhookModalViews.Edit && view === UpdateModalViews.Edit &&
selectedWebhook?.[WebhookInputDataFields.ID] selectedWebhook?.[WebhookInputDataFields.ID]
) { ) {
res = await client res = await client
@@ -292,12 +308,12 @@ const UpdateWebhookModal = ({
setValidator({ ...initWebhookValidatorData }); setValidator({ ...initWebhookValidatorData });
fetchWebookData(); fetchWebookData();
} }
view === UpdateWebhookModalViews.ADD && onClose(); view === UpdateModalViews.ADD && onClose();
}; };
useEffect(() => { useEffect(() => {
if ( if (
isOpen && isOpen &&
view === UpdateWebhookModalViews.Edit && view === UpdateModalViews.Edit &&
selectedWebhook && selectedWebhook &&
Object.keys(selectedWebhook || {}).length Object.keys(selectedWebhook || {}).length
) { ) {
@@ -347,7 +363,7 @@ const UpdateWebhookModal = ({
}; };
return ( return (
<> <>
{view === UpdateWebhookModalViews.ADD ? ( {view === UpdateModalViews.ADD ? (
<Button <Button
leftIcon={<FaPlus />} leftIcon={<FaPlus />}
colorScheme="blue" colorScheme="blue"
@@ -365,9 +381,7 @@ const UpdateWebhookModal = ({
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<ModalHeader> <ModalHeader>
{view === UpdateWebhookModalViews.ADD {view === UpdateModalViews.ADD ? 'Add New Webhook' : 'Edit Webhook'}
? 'Add New Webhook'
: 'Edit Webhook'}
</ModalHeader> </ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody> <ModalBody>
@@ -457,11 +471,12 @@ const UpdateWebhookModal = ({
</Text> </Text>
</Flex> </Flex>
</Flex> </Flex>
<Flex <Flex
width="100%" width="100%"
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
marginBottom="2%" marginBottom="5%"
> >
<Flex>Headers</Flex> <Flex>Headers</Flex>
<Flex> <Flex>
@@ -478,7 +493,8 @@ const UpdateWebhookModal = ({
</Button> </Button>
</Flex> </Flex>
</Flex> </Flex>
<Flex flexDirection="column" maxH={220} overflowY="scroll">
<Flex flexDirection="column" maxH={220} overflowY="auto">
{webhook[WebhookInputDataFields.HEADERS]?.map( {webhook[WebhookInputDataFields.HEADERS]?.map(
(headerData, index) => ( (headerData, index) => (
<Flex <Flex
@@ -547,6 +563,54 @@ const UpdateWebhookModal = ({
) )
)} )}
</Flex> </Flex>
<Divider marginY={5} />
<Alert
status="info"
onClick={() => setIsShowingPayload(!isShowingPayload)}
borderRadius="5"
cursor="pointer"
fontSize="sm"
>
<AlertIcon />
<Flex
width="100%"
justifyContent="space-between"
alignItems="center"
>
Checkout the example payload
{isShowingPayload ? <FaAngleUp /> : <FaAngleDown />}
</Flex>
</Alert>
<Collapse
style={{
marginTop: 10,
width: '100%',
}}
in={isShowingPayload}
>
<Code
width="inherit"
borderRadius={5}
padding={2}
position="relative"
>
<pre style={{ overflow: 'auto' }}>
{webhookPayloadExample}
</pre>
{isShowingPayload && (
<Flex
position="absolute"
top={4}
right={4}
cursor="pointer"
onClick={() => copyTextToClipboard(webhookPayloadExample)}
>
<FaRegClone color="#bfbfbf" />
</Flex>
)}
</Code>
</Collapse>
</Flex> </Flex>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>

View File

@@ -9,6 +9,7 @@ export const TextInputType = {
FACEBOOK_CLIENT_ID: 'FACEBOOK_CLIENT_ID', FACEBOOK_CLIENT_ID: 'FACEBOOK_CLIENT_ID',
LINKEDIN_CLIENT_ID: 'LINKEDIN_CLIENT_ID', LINKEDIN_CLIENT_ID: 'LINKEDIN_CLIENT_ID',
APPLE_CLIENT_ID: 'APPLE_CLIENT_ID', APPLE_CLIENT_ID: 'APPLE_CLIENT_ID',
TWITTER_CLIENT_ID: 'TWITTER_CLIENT_ID',
JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM', JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM',
REDIS_URL: 'REDIS_URL', REDIS_URL: 'REDIS_URL',
SMTP_HOST: 'SMTP_HOST', SMTP_HOST: 'SMTP_HOST',
@@ -35,6 +36,7 @@ export const HiddenInputType = {
FACEBOOK_CLIENT_SECRET: 'FACEBOOK_CLIENT_SECRET', FACEBOOK_CLIENT_SECRET: 'FACEBOOK_CLIENT_SECRET',
LINKEDIN_CLIENT_SECRET: 'LINKEDIN_CLIENT_SECRET', LINKEDIN_CLIENT_SECRET: 'LINKEDIN_CLIENT_SECRET',
APPLE_CLIENT_SECRET: 'APPLE_CLIENT_SECRET', APPLE_CLIENT_SECRET: 'APPLE_CLIENT_SECRET',
TWITTER_CLIENT_SECRET: 'TWITTER_CLIENT_SECRET',
JWT_SECRET: 'JWT_SECRET', JWT_SECRET: 'JWT_SECRET',
SMTP_PASSWORD: 'SMTP_PASSWORD', SMTP_PASSWORD: 'SMTP_PASSWORD',
ADMIN_SECRET: 'ADMIN_SECRET', ADMIN_SECRET: 'ADMIN_SECRET',
@@ -68,6 +70,8 @@ export const SwitchInputType = {
DISABLE_SIGN_UP: 'DISABLE_SIGN_UP', DISABLE_SIGN_UP: 'DISABLE_SIGN_UP',
DISABLE_REDIS_FOR_ENV: 'DISABLE_REDIS_FOR_ENV', DISABLE_REDIS_FOR_ENV: 'DISABLE_REDIS_FOR_ENV',
DISABLE_STRONG_PASSWORD: 'DISABLE_STRONG_PASSWORD', DISABLE_STRONG_PASSWORD: 'DISABLE_STRONG_PASSWORD',
DISABLE_MULTI_FACTOR_AUTHENTICATION: 'DISABLE_MULTI_FACTOR_AUTHENTICATION',
ENFORCE_MULTI_FACTOR_AUTHENTICATION: 'ENFORCE_MULTI_FACTOR_AUTHENTICATION',
}; };
export const DateInputType = { export const DateInputType = {
@@ -108,6 +112,8 @@ export interface envVarTypes {
LINKEDIN_CLIENT_SECRET: string; LINKEDIN_CLIENT_SECRET: string;
APPLE_CLIENT_ID: string; APPLE_CLIENT_ID: string;
APPLE_CLIENT_SECRET: string; APPLE_CLIENT_SECRET: string;
TWITTER_CLIENT_ID: string;
TWITTER_CLIENT_SECRET: string;
ROLES: [string] | []; ROLES: [string] | [];
DEFAULT_ROLES: [string] | []; DEFAULT_ROLES: [string] | [];
PROTECTED_ROLES: [string] | []; PROTECTED_ROLES: [string] | [];
@@ -138,6 +144,8 @@ export interface envVarTypes {
DATABASE_TYPE: string; DATABASE_TYPE: string;
DATABASE_URL: string; DATABASE_URL: string;
ACCESS_TOKEN_EXPIRY_TIME: string; ACCESS_TOKEN_EXPIRY_TIME: string;
DISABLE_MULTI_FACTOR_AUTHENTICATION: boolean;
ENFORCE_MULTI_FACTOR_AUTHENTICATION: boolean;
} }
export const envSubViews = { export const envSubViews = {
@@ -162,12 +170,21 @@ export enum WebhookInputDataFields {
HEADERS = 'headers', HEADERS = 'headers',
} }
export enum EmailTemplateInputDataFields {
ID = 'id',
EVENT_NAME = 'event_name',
SUBJECT = 'subject',
CREATED_AT = 'created_at',
TEMPLATE = 'template',
DESIGN = 'design',
}
export enum WebhookInputHeaderFields { export enum WebhookInputHeaderFields {
KEY = 'key', KEY = 'key',
VALUE = 'value', VALUE = 'value',
} }
export enum UpdateWebhookModalViews { export enum UpdateModalViews {
ADD = 'add', ADD = 'add',
Edit = 'edit', Edit = 'edit',
} }
@@ -175,12 +192,21 @@ export enum UpdateWebhookModalViews {
export const pageLimits: number[] = [5, 10, 15]; export const pageLimits: number[] = [5, 10, 15];
export const webhookEventNames = { export const webhookEventNames = {
USER_SIGNUP: 'user.signup', 'User signup': 'user.signup',
USER_CREATED: 'user.created', 'User created': 'user.created',
USER_LOGIN: 'user.login', 'User login': 'user.login',
USER_DELETED: 'user.deleted', 'User deleted': 'user.deleted',
USER_ACCESS_ENABLED: 'user.access_enabled', 'User access enabled': 'user.access_enabled',
USER_ACCESS_REVOKED: 'user.access_revoked', 'User access revoked': 'user.access_revoked',
};
export const emailTemplateEventNames = {
Signup: 'basic_auth_signup',
'Magic Link Login': 'magic_link_login',
'Update Email': 'update_email',
'Forgot Password': 'forgot_password',
'Verify Otp': 'verify_otp',
'Invite member': 'invite_member',
}; };
export enum webhookVerifiedStatus { export enum webhookVerifiedStatus {
@@ -188,3 +214,117 @@ export enum webhookVerifiedStatus {
NOT_VERIFIED = 'not_verified', NOT_VERIFIED = 'not_verified',
PENDING = 'verification_pending', PENDING = 'verification_pending',
} }
export const emailTemplateVariables = {
'user.id': {
description: `User identifier`,
value: '{.user.id}}',
},
'user.email': {
description: 'User email address',
value: '{.user.email}}',
},
'user.given_name': {
description: `User first name`,
value: '{.user.given_name}}',
},
'user.family_name': {
description: `User last name`,
value: '{.user.family_name}}',
},
'user.middle_name': {
description: `Middle name of user`,
value: '{.user.middle_name}}',
},
'user.nickname': {
description: `Nick name of user`,
value: '{.user.nickname}}',
},
'user.preferred_username': {
description: `Username, by default it is email`,
value: '{.user.preferred_username}}',
},
'user.signup_methods': {
description: `Comma separated list of methods using which user has signed up`,
value: '{.user.signup_methods}}',
},
'user.email_verified': {
description: `Whether email is verified or not`,
value: '{.user.email_verified}}',
},
'user.picture': {
description: `URL of the user profile picture`,
value: '{.user.picture}}',
},
'user.roles': {
description: `Comma separated list of roles assigned to user`,
value: '{.user.roles}}',
},
'user.gender': {
description: `Gender of user`,
value: '{.user.gender}}',
},
'user.birthdate': {
description: `BirthDate of user`,
value: '{.user.birthdate}}',
},
'user.phone_number': {
description: `Phone number of user`,
value: '{.user.phone_number}}',
},
'user.phone_number_verified': {
description: `Whether phone number is verified or not`,
value: '{.user.phone_number_verified}}',
},
'user.created_at': {
description: `User created at time`,
value: '{.user.created_at}}',
},
'user.updated_at': {
description: `Last updated time at user`,
value: '{.user.updated_at}}',
},
'organization.name': {
description: `Organization name`,
value: '{.organization.name}}',
},
'organization.logo': {
description: `Organization logo`,
value: '{.organization.logo}}',
},
verification_url: {
description: `Verification URL in case of events other than verify otp`,
value: '{.verification_url}}',
},
otp: {
description: `OTP sent during login with Multi factor authentication`,
value: '{.otp}}',
},
};
export const webhookPayloadExample: string = `{
"event_name":"user.login",
"user":{
"birthdate":null,
"created_at":1657524721,
"email":"lakhan.m.samani@gmail.com",
"email_verified":true,
"family_name":"Samani",
"gender":null,
"given_name":"Lakhan",
"id":"466d0b31-1b87-420e-bea5-09d05d79c586",
"middle_name":null,
"nickname":null,
"phone_number":null,
"phone_number_verified":false,
"picture":"https://lh3.googleusercontent.com/a-/AFdZucppvU6a2zIDkX0wvhhapVjT0ZMKDlYCkQDi3NxcUg=s96-c",
"preferred_username":"lakhan.m.samani@gmail.com",
"revoked_timestamp":null,
"roles":[
"user"
],
"signup_methods":"google",
"updated_at":1657526492
},
"auth_recipe":"google"
}`;

View File

@@ -112,3 +112,27 @@ export const TestEndpoint = `
} }
} }
`; `;
export const AddEmailTemplate = `
mutation addEmailTemplate($params: AddEmailTemplateRequest!) {
_add_email_template(params: $params) {
message
}
}
`;
export const EditEmailTemplate = `
mutation editEmailTemplate($params: UpdateEmailTemplateRequest!) {
_update_email_template(params: $params) {
message
}
}
`;
export const DeleteEmailTemplate = `
mutation deleteEmailTemplate($params: DeleteEmailTemplateRequest!) {
_delete_email_template(params: $params) {
message
}
}
`;

View File

@@ -18,48 +18,52 @@ export const AdminSessionQuery = `
export const EnvVariablesQuery = ` export const EnvVariablesQuery = `
query { query {
_env{ _env{
CLIENT_ID, CLIENT_ID
CLIENT_SECRET, CLIENT_SECRET
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET, GOOGLE_CLIENT_SECRET
GITHUB_CLIENT_ID, GITHUB_CLIENT_ID
GITHUB_CLIENT_SECRET, GITHUB_CLIENT_SECRET
FACEBOOK_CLIENT_ID, FACEBOOK_CLIENT_ID
FACEBOOK_CLIENT_SECRET, FACEBOOK_CLIENT_SECRET
LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_ID
LINKEDIN_CLIENT_SECRET, LINKEDIN_CLIENT_SECRET
APPLE_CLIENT_ID, APPLE_CLIENT_ID
APPLE_CLIENT_SECRET, APPLE_CLIENT_SECRET
DEFAULT_ROLES, TWITTER_CLIENT_ID
PROTECTED_ROLES, TWITTER_CLIENT_SECRET
ROLES, DEFAULT_ROLES
JWT_TYPE, PROTECTED_ROLES
JWT_SECRET, ROLES
JWT_ROLE_CLAIM, JWT_TYPE
JWT_PRIVATE_KEY, JWT_SECRET
JWT_PUBLIC_KEY, JWT_ROLE_CLAIM
REDIS_URL, JWT_PRIVATE_KEY
SMTP_HOST, JWT_PUBLIC_KEY
SMTP_PORT, REDIS_URL
SMTP_USERNAME, SMTP_HOST
SMTP_PASSWORD, SMTP_PORT
SENDER_EMAIL, SMTP_USERNAME
ALLOWED_ORIGINS, SMTP_PASSWORD
ORGANIZATION_NAME, SENDER_EMAIL
ORGANIZATION_LOGO, ALLOWED_ORIGINS
ADMIN_SECRET, ORGANIZATION_NAME
DISABLE_LOGIN_PAGE, ORGANIZATION_LOGO
DISABLE_MAGIC_LINK_LOGIN, ADMIN_SECRET
DISABLE_EMAIL_VERIFICATION, DISABLE_LOGIN_PAGE
DISABLE_BASIC_AUTHENTICATION, DISABLE_MAGIC_LINK_LOGIN
DISABLE_SIGN_UP, DISABLE_EMAIL_VERIFICATION
DISABLE_STRONG_PASSWORD, DISABLE_BASIC_AUTHENTICATION
DISABLE_REDIS_FOR_ENV, DISABLE_SIGN_UP
CUSTOM_ACCESS_TOKEN_SCRIPT, DISABLE_STRONG_PASSWORD
DATABASE_NAME, DISABLE_REDIS_FOR_ENV
DATABASE_TYPE, CUSTOM_ACCESS_TOKEN_SCRIPT
DATABASE_URL, DATABASE_NAME
ACCESS_TOKEN_EXPIRY_TIME, DATABASE_TYPE
DATABASE_URL
ACCESS_TOKEN_EXPIRY_TIME
DISABLE_MULTI_FACTOR_AUTHENTICATION
ENFORCE_MULTI_FACTOR_AUTHENTICATION
} }
} }
`; `;
@@ -89,6 +93,7 @@ export const UserDetailsQuery = `
roles roles
created_at created_at
revoked_timestamp revoked_timestamp
is_multi_factor_auth_enabled
} }
} }
} }
@@ -122,6 +127,27 @@ export const WebhooksDataQuery = `
} }
`; `;
export const EmailTemplatesQuery = `
query getEmailTemplates($params: PaginatedInput!) {
_email_templates(params: $params) {
email_templates {
id
event_name
subject
created_at
template
design
}
pagination {
limit
page
offset
total
}
}
}
`;
export const WebhookLogsQuery = ` export const WebhookLogsQuery = `
query getWebhookLogs($params: ListWebhookLogRequest!) { query getWebhookLogs($params: ListWebhookLogRequest!) {
_webhook_logs(params: $params) { _webhook_logs(params: $params) {

View File

@@ -0,0 +1,348 @@
import React, { useEffect, useState } from 'react';
import { useClient } from 'urql';
import {
Box,
Button,
Center,
Flex,
IconButton,
Menu,
MenuButton,
MenuList,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Select,
Spinner,
Table,
TableCaption,
Tbody,
Td,
Text,
Th,
Thead,
Tooltip,
Tr,
} from '@chakra-ui/react';
import {
FaAngleDoubleLeft,
FaAngleDoubleRight,
FaAngleDown,
FaAngleLeft,
FaAngleRight,
FaExclamationCircle,
} from 'react-icons/fa';
import UpdateEmailTemplateModal from '../components/UpdateEmailTemplateModal';
import {
pageLimits,
UpdateModalViews,
EmailTemplateInputDataFields,
} from '../constants';
import { EmailTemplatesQuery } from '../graphql/queries';
import dayjs from 'dayjs';
import DeleteEmailTemplateModal from '../components/DeleteEmailTemplateModal';
interface paginationPropTypes {
limit: number;
page: number;
offset: number;
total: number;
maxPages: number;
}
interface EmailTemplateDataType {
[EmailTemplateInputDataFields.ID]: string;
[EmailTemplateInputDataFields.EVENT_NAME]: string;
[EmailTemplateInputDataFields.SUBJECT]: string;
[EmailTemplateInputDataFields.CREATED_AT]: number;
[EmailTemplateInputDataFields.TEMPLATE]: string;
[EmailTemplateInputDataFields.DESIGN]: string;
}
const EmailTemplates = () => {
const client = useClient();
const [loading, setLoading] = useState<boolean>(false);
const [emailTemplatesData, setEmailTemplatesData] = useState<
EmailTemplateDataType[]
>([]);
const [paginationProps, setPaginationProps] = useState<paginationPropTypes>({
limit: 5,
page: 1,
offset: 0,
total: 0,
maxPages: 1,
});
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 fetchEmailTemplatesData = async () => {
setLoading(true);
const res = await client
.query(EmailTemplatesQuery, {
params: {
pagination: {
limit: paginationProps.limit,
page: paginationProps.page,
},
},
})
.toPromise();
if (res.data?._email_templates) {
const { pagination, email_templates: emailTemplates } =
res.data?._email_templates;
const maxPages = getMaxPages(pagination);
if (emailTemplates?.length) {
setEmailTemplatesData(emailTemplates);
setPaginationProps({ ...paginationProps, ...pagination, maxPages });
} else {
if (paginationProps.page !== 1) {
setPaginationProps({
...paginationProps,
...pagination,
maxPages,
page: 1,
});
}
}
}
setLoading(false);
};
const paginationHandler = (value: Record<string, number>) => {
setPaginationProps({ ...paginationProps, ...value });
};
useEffect(() => {
fetchEmailTemplatesData();
}, [paginationProps.page, paginationProps.limit]);
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">
Email Templates
</Text>
<UpdateEmailTemplateModal
view={UpdateModalViews.ADD}
fetchEmailTemplatesData={fetchEmailTemplatesData}
/>
</Flex>
{!loading ? (
emailTemplatesData.length ? (
<Table variant="simple">
<Thead>
<Tr>
<Th>Event Name</Th>
<Th>Subject</Th>
<Th>Created At</Th>
<Th>Actions</Th>
</Tr>
</Thead>
<Tbody>
{emailTemplatesData.map((templateData: EmailTemplateDataType) => (
<Tr
key={templateData[EmailTemplateInputDataFields.ID]}
style={{ fontSize: 14 }}
>
<Td maxW="300">
{templateData[EmailTemplateInputDataFields.EVENT_NAME]}
</Td>
<Td>{templateData[EmailTemplateInputDataFields.SUBJECT]}</Td>
<Td>
{dayjs(templateData.created_at * 1000).format(
'MMM DD, YYYY'
)}
</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>
<UpdateEmailTemplateModal
view={UpdateModalViews.Edit}
selectedTemplate={templateData}
fetchEmailTemplatesData={fetchEmailTemplatesData}
/>
<DeleteEmailTemplateModal
emailTemplateId={
templateData[EmailTemplateInputDataFields.ID]
}
eventName={
templateData[
EmailTemplateInputDataFields.EVENT_NAME
]
}
fetchEmailTemplatesData={fetchEmailTemplatesData}
/>
</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),
})
}
>
{pageLimits.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>
);
};
export default EmailTemplates;

View File

@@ -50,6 +50,8 @@ const Environment = () => {
LINKEDIN_CLIENT_SECRET: '', LINKEDIN_CLIENT_SECRET: '',
APPLE_CLIENT_ID: '', APPLE_CLIENT_ID: '',
APPLE_CLIENT_SECRET: '', APPLE_CLIENT_SECRET: '',
TWITTER_CLIENT_ID: '',
TWITTER_CLIENT_SECRET: '',
ROLES: [], ROLES: [],
DEFAULT_ROLES: [], DEFAULT_ROLES: [],
PROTECTED_ROLES: [], PROTECTED_ROLES: [],
@@ -80,6 +82,8 @@ const Environment = () => {
DATABASE_TYPE: '', DATABASE_TYPE: '',
DATABASE_URL: '', DATABASE_URL: '',
ACCESS_TOKEN_EXPIRY_TIME: '', ACCESS_TOKEN_EXPIRY_TIME: '',
DISABLE_MULTI_FACTOR_AUTHENTICATION: false,
ENFORCE_MULTI_FACTOR_AUTHENTICATION: false,
}); });
const [fieldVisibility, setFieldVisibility] = React.useState< const [fieldVisibility, setFieldVisibility] = React.useState<
@@ -90,6 +94,7 @@ const Environment = () => {
FACEBOOK_CLIENT_SECRET: false, FACEBOOK_CLIENT_SECRET: false,
LINKEDIN_CLIENT_SECRET: false, LINKEDIN_CLIENT_SECRET: false,
APPLE_CLIENT_SECRET: false, APPLE_CLIENT_SECRET: false,
TWITTER_CLIENT_SECRET: false,
JWT_SECRET: false, JWT_SECRET: false,
SMTP_PASSWORD: false, SMTP_PASSWORD: false,
ADMIN_SECRET: false, ADMIN_SECRET: false,

View File

@@ -29,6 +29,7 @@ import {
MenuItem, MenuItem,
useToast, useToast,
Spinner, Spinner,
TableContainer
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { import {
FaAngleLeft, FaAngleLeft,
@@ -68,6 +69,7 @@ interface userDataTypes {
roles: [string]; roles: [string];
created_at: number; created_at: number;
revoked_timestamp: number; revoked_timestamp: number;
is_multi_factor_auth_enabled?: boolean;
} }
const enum updateAccessActions { const enum updateAccessActions {
@@ -250,6 +252,33 @@ export default function Users() {
break; break;
} }
}; };
const multiFactorAuthUpdateHandler = async (user: userDataTypes) => {
const res = await client
.mutation(UpdateUser, {
params: {
id: user.id,
is_multi_factor_auth_enabled: !user.is_multi_factor_auth_enabled,
},
})
.toPromise();
if (res.data?._update_user?.id) {
toast({
title: `Multi factor authentication ${user.is_multi_factor_auth_enabled ? 'disabled' : 'enabled'
} for user`,
isClosable: true,
status: 'success',
position: 'bottom-right',
});
updateUserList();
return;
}
toast({
title: 'Multi factor authentication update failed for user',
isClosable: true,
status: 'error',
position: 'bottom-right',
});
};
return ( return (
<Box m="5" py="5" px="10" bg="white" rounded="md"> <Box m="5" py="5" px="10" bg="white" rounded="md">
@@ -264,229 +293,262 @@ export default function Users() {
</Flex> </Flex>
{!loading ? ( {!loading ? (
userList.length > 0 ? ( userList.length > 0 ? (
<Table variant="simple"> <TableContainer>
<Thead> <Table variant="simple">
<Tr> <Thead>
<Th>Email</Th> <Tr>
<Th>Created At</Th> <Th>Email</Th>
<Th>Signup Methods</Th> <Th>Created At</Th>
<Th>Roles</Th> <Th>Signup Methods</Th>
<Th>Verified</Th> <Th>Roles</Th>
<Th>Access</Th> <Th>Verified</Th>
<Th>Actions</Th> <Th>Access</Th>
</Tr> <Th>
</Thead> <Tooltip label="MultiFactor Authentication Enabled / Disabled">
<Tbody> MFA
{userList.map((user: userDataTypes) => { </Tooltip>
const { email_verified, created_at, ...rest }: any = user; </Th>
return ( <Th>Actions</Th>
<Tr key={user.id} style={{ fontSize: 14 }}> </Tr>
<Td maxW="300">{user.email}</Td> </Thead>
<Td> <Tbody>
{dayjs(user.created_at * 1000).format('MMM DD, YYYY')} {userList.map((user: userDataTypes) => {
</Td> const { email_verified, created_at, ...rest }: any = user;
<Td>{user.signup_methods}</Td> return (
<Td>{user.roles.join(', ')}</Td> <Tr key={user.id} style={{ fontSize: 14 }}>
<Td> <Td maxW="300">{user.email}</Td>
<Tag <Td>
size="sm" {dayjs(user.created_at * 1000).format('MMM DD, YYYY')}
variant="outline" </Td>
colorScheme={user.email_verified ? 'green' : 'yellow'} <Td>{user.signup_methods}</Td>
> <Td>{user.roles.join(', ')}</Td>
{user.email_verified.toString()} <Td>
</Tag> <Tag
</Td> size="sm"
<Td> variant="outline"
<Tag colorScheme={user.email_verified ? 'green' : 'yellow'}
size="sm" >
variant="outline" {user.email_verified.toString()}
colorScheme={user.revoked_timestamp ? 'red' : 'green'} </Tag>
> </Td>
{user.revoked_timestamp ? 'Revoked' : 'Enabled'} <Td>
</Tag> <Tag
</Td> size="sm"
<Td> variant="outline"
<Menu> colorScheme={user.revoked_timestamp ? 'red' : 'green'}
<MenuButton as={Button} variant="unstyled" size="sm"> >
<Flex {user.revoked_timestamp ? 'Revoked' : 'Enabled'}
justifyContent="space-between" </Tag>
alignItems="center" </Td>
> <Td>
<Text fontSize="sm" fontWeight="light"> <Tag
Menu size="sm"
</Text> variant="outline"
<FaAngleDown style={{ marginLeft: 10 }} /> colorScheme={
</Flex> user.is_multi_factor_auth_enabled ? 'green' : 'red'
</MenuButton> }
<MenuList> >
{!user.email_verified && ( {user.is_multi_factor_auth_enabled
<MenuItem ? 'Enabled'
onClick={() => userVerificationHandler(user)} : 'Disabled'}
</Tag>
</Td>
<Td>
<Menu>
<MenuButton as={Button} variant="unstyled" size="sm">
<Flex
justifyContent="space-between"
alignItems="center"
> >
Verify User <Text fontSize="sm" fontWeight="light">
</MenuItem> Menu
)} </Text>
<EditUserModal <FaAngleDown style={{ marginLeft: 10 }} />
user={rest} </Flex>
updateUserList={updateUserList} </MenuButton>
/> <MenuList>
<DeleteUserModal {!user.email_verified && (
user={rest} <MenuItem
updateUserList={updateUserList} onClick={() => userVerificationHandler(user)}
/> >
{user.revoked_timestamp ? ( Verify User
<MenuItem </MenuItem>
onClick={() => )}
updateAccessHandler( <EditUserModal
user.id, user={rest}
updateAccessActions.ENABLE updateUserList={updateUserList}
) />
} <DeleteUserModal
> user={rest}
Enable Access updateUserList={updateUserList}
</MenuItem> />
) : ( {user.revoked_timestamp ? (
<MenuItem <MenuItem
onClick={() => onClick={() =>
updateAccessHandler( updateAccessHandler(
user.id, user.id,
updateAccessActions.REVOKE updateAccessActions.ENABLE
) )
} }
> >
Revoke Access Enable Access
</MenuItem> </MenuItem>
)} ) : (
</MenuList> <MenuItem
</Menu> onClick={() =>
</Td> updateAccessHandler(
</Tr> user.id,
); updateAccessActions.REVOKE
})} )
</Tbody> }
{(paginationProps.maxPages > 1 || paginationProps.total >= 5) && ( >
<TableCaption> Revoke Access
<Flex </MenuItem>
justifyContent="space-between" )}
alignItems="center" {user.is_multi_factor_auth_enabled ? (
m="2% 0" <MenuItem
> onClick={() => multiFactorAuthUpdateHandler(user)}
<Flex flex="1"> >
<Tooltip label="First Page"> Disable MultiFactor Authentication
<IconButton </MenuItem>
aria-label="icon button" ) : (
onClick={() => <MenuItem
onClick={() => multiFactorAuthUpdateHandler(user)}
>
Enable MultiFactor Authentication
</MenuItem>
)}
</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({ paginationHandler({
page: 1, page: 1,
limit: parseInt(e.target.value),
}) })
} }
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 /> {getLimits(paginationProps).map((pageSize) => (
<NumberInputStepper> <option key={pageSize} value={pageSize}>
<NumberIncrementStepper /> Show {pageSize}
<NumberDecrementStepper /> </option>
</NumberInputStepper> ))}
</NumberInput> </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>
<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 flex="1"> </TableCaption>
<Tooltip label="Next Page"> )}
<IconButton </Table>
aria-label="icon button" </TableContainer>
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 <Flex
flexDirection="column" flexDirection="column"

View File

@@ -8,7 +8,6 @@ import {
IconButton, IconButton,
Menu, Menu,
MenuButton, MenuButton,
MenuItem,
MenuList, MenuList,
NumberDecrementStepper, NumberDecrementStepper,
NumberIncrementStepper, NumberIncrementStepper,
@@ -40,7 +39,7 @@ import UpdateWebhookModal from '../components/UpdateWebhookModal';
import { import {
pageLimits, pageLimits,
WebhookInputDataFields, WebhookInputDataFields,
UpdateWebhookModalViews, UpdateModalViews,
} from '../constants'; } from '../constants';
import { WebhooksDataQuery } from '../graphql/queries'; import { WebhooksDataQuery } from '../graphql/queries';
import DeleteWebhookModal from '../components/DeleteWebhookModal'; import DeleteWebhookModal from '../components/DeleteWebhookModal';
@@ -125,7 +124,7 @@ const Webhooks = () => {
Webhooks Webhooks
</Text> </Text>
<UpdateWebhookModal <UpdateWebhookModal
view={UpdateWebhookModalViews.ADD} view={UpdateModalViews.ADD}
fetchWebookData={fetchWebookData} fetchWebookData={fetchWebookData}
/> />
</Flex> </Flex>
@@ -196,7 +195,7 @@ const Webhooks = () => {
</MenuButton> </MenuButton>
<MenuList> <MenuList>
<UpdateWebhookModal <UpdateWebhookModal
view={UpdateWebhookModalViews.Edit} view={UpdateModalViews.Edit}
selectedWebhook={webhook} selectedWebhook={webhook}
fetchWebookData={fetchWebookData} fetchWebookData={fetchWebookData}
/> />

View File

@@ -3,6 +3,7 @@ import { Outlet, Route, Routes } from 'react-router-dom';
import { useAuthContext } from '../contexts/AuthContext'; import { useAuthContext } from '../contexts/AuthContext';
import { DashboardLayout } from '../layouts/DashboardLayout'; import { DashboardLayout } from '../layouts/DashboardLayout';
import EmailTemplates from '../pages/EmailTemplates';
const Auth = lazy(() => import('../pages/Auth')); const Auth = lazy(() => import('../pages/Auth'));
const Environment = lazy(() => import('../pages/Environment')); const Environment = lazy(() => import('../pages/Environment'));
@@ -31,6 +32,7 @@ export const AppRoutes = () => {
</Route> </Route>
<Route path="users" element={<Users />} /> <Route path="users" element={<Users />} />
<Route path="webhooks" element={<Webhooks />} /> <Route path="webhooks" element={<Webhooks />} />
<Route path="email-templates" element={<EmailTemplates />} />
<Route path="*" element={<Home />} /> <Route path="*" element={<Home />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -29,19 +29,16 @@ const fallbackCopyTextToClipboard = (text: string) => {
document.body.removeChild(textArea); document.body.removeChild(textArea);
}; };
export const copyTextToClipboard = (text: string) => { export const copyTextToClipboard = async (text: string) => {
if (!navigator.clipboard) { if (!navigator.clipboard) {
fallbackCopyTextToClipboard(text); fallbackCopyTextToClipboard(text);
return; return;
} }
navigator.clipboard.writeText(text).then( try {
() => { navigator.clipboard.writeText(text);
console.log('Async: Copying to clipboard was successful!'); } catch (err) {
}, throw err;
(err) => { }
console.error('Async: Could not copy text: ', err);
}
);
}; };
export const getObjectDiff = (obj1: any, obj2: any) => { export const getObjectDiff = (obj1: any, obj2: any) => {

View File

@@ -15,4 +15,6 @@ const (
AuthRecipeMethodLinkedIn = "linkedin" AuthRecipeMethodLinkedIn = "linkedin"
// AuthRecipeMethodApple is the apple auth method // AuthRecipeMethodApple is the apple auth method
AuthRecipeMethodApple = "apple" AuthRecipeMethodApple = "apple"
// AuthRecipeMethodTwitter is the twitter auth method
AuthRecipeMethodTwitter = "twitter"
) )

View File

@@ -47,6 +47,12 @@ const (
EnvKeySmtpPassword = "SMTP_PASSWORD" EnvKeySmtpPassword = "SMTP_PASSWORD"
// EnvKeySenderEmail key for env variable SENDER_EMAIL // EnvKeySenderEmail key for env variable SENDER_EMAIL
EnvKeySenderEmail = "SENDER_EMAIL" EnvKeySenderEmail = "SENDER_EMAIL"
// EnvKeyIsEmailServiceEnabled key for env variable IS_EMAIL_SERVICE_ENABLED
EnvKeyIsEmailServiceEnabled = "IS_EMAIL_SERVICE_ENABLED"
// EnvKeyAppCookieSecure key for env variable APP_COOKIE_SECURE
EnvKeyAppCookieSecure = "APP_COOKIE_SECURE"
// EnvKeyAdminCookieSecure key for env variable ADMIN_COOKIE_SECURE
EnvKeyAdminCookieSecure = "ADMIN_COOKIE_SECURE"
// EnvKeyJwtType key for env variable JWT_TYPE // EnvKeyJwtType key for env variable JWT_TYPE
EnvKeyJwtType = "JWT_TYPE" EnvKeyJwtType = "JWT_TYPE"
// EnvKeyJwtSecret key for env variable JWT_SECRET // EnvKeyJwtSecret key for env variable JWT_SECRET
@@ -83,6 +89,10 @@ const (
EnvKeyAppleClientID = "APPLE_CLIENT_ID" EnvKeyAppleClientID = "APPLE_CLIENT_ID"
// EnvKeyAppleClientSecret key for env variable APPLE_CLIENT_SECRET // EnvKeyAppleClientSecret key for env variable APPLE_CLIENT_SECRET
EnvKeyAppleClientSecret = "APPLE_CLIENT_SECRET" EnvKeyAppleClientSecret = "APPLE_CLIENT_SECRET"
// EnvKeyTwitterClientID key for env variable TWITTER_CLIENT_ID
EnvKeyTwitterClientID = "TWITTER_CLIENT_ID"
// EnvKeyTwitterClientSecret key for env variable TWITTER_CLIENT_SECRET
EnvKeyTwitterClientSecret = "TWITTER_CLIENT_SECRET"
// EnvKeyOrganizationName key for env variable ORGANIZATION_NAME // EnvKeyOrganizationName key for env variable ORGANIZATION_NAME
EnvKeyOrganizationName = "ORGANIZATION_NAME" EnvKeyOrganizationName = "ORGANIZATION_NAME"
// EnvKeyOrganizationLogo key for env variable ORGANIZATION_LOGO // EnvKeyOrganizationLogo key for env variable ORGANIZATION_LOGO
@@ -117,6 +127,12 @@ const (
EnvKeyDisableRedisForEnv = "DISABLE_REDIS_FOR_ENV" EnvKeyDisableRedisForEnv = "DISABLE_REDIS_FOR_ENV"
// EnvKeyDisableStrongPassword key for env variable DISABLE_STRONG_PASSWORD // EnvKeyDisableStrongPassword key for env variable DISABLE_STRONG_PASSWORD
EnvKeyDisableStrongPassword = "DISABLE_STRONG_PASSWORD" EnvKeyDisableStrongPassword = "DISABLE_STRONG_PASSWORD"
// EnvKeyEnforceMultiFactorAuthentication is key for env variable ENFORCE_MULTI_FACTOR_AUTHENTICATION
// If enforced and changed later on, existing user will have MFA but new user will not have MFA
EnvKeyEnforceMultiFactorAuthentication = "ENFORCE_MULTI_FACTOR_AUTHENTICATION"
// EnvKeyDisableMultiFactorAuthentication is key for env variable DISABLE_MULTI_FACTOR_AUTHENTICATION
// this variable is used to completely disable multi factor authentication. It will have no effect on profile preference
EnvKeyDisableMultiFactorAuthentication = "DISABLE_MULTI_FACTOR_AUTHENTICATION"
// Slice variables // Slice variables
// EnvKeyRoles key for env variable ROLES // EnvKeyRoles key for env variable ROLES

View File

@@ -9,9 +9,11 @@ const (
// Ref: https://docs.github.com/en/developers/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps#3-your-github-app-accesses-the-api-with-the-users-access-token // Ref: https://docs.github.com/en/developers/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps#3-your-github-app-accesses-the-api-with-the-users-access-token
GithubUserInfoURL = "https://api.github.com/user" GithubUserInfoURL = "https://api.github.com/user"
// Get github user emails when user info email is empty Ref: https://stackoverflow.com/a/35387123 // Get github user emails when user info email is empty Ref: https://stackoverflow.com/a/35387123
GithubUserEmails = "https://api/github.com/user/emails" GithubUserEmails = "https://api.github.com/user/emails"
// Ref: https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api // Ref: https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api
LinkedInUserInfoURL = "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,emailAddress,profilePicture(displayImage~:playableStreams))" LinkedInUserInfoURL = "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,emailAddress,profilePicture(displayImage~:playableStreams))"
LinkedInEmailURL = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))" LinkedInEmailURL = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))"
TwitterUserInfoURL = "https://api.twitter.com/2/users/me?user.fields=id,name,profile_image_url,username"
) )

View File

@@ -9,4 +9,8 @@ const (
VerificationTypeUpdateEmail = "update_email" VerificationTypeUpdateEmail = "update_email"
// VerificationTypeForgotPassword is the forgot_password verification type // VerificationTypeForgotPassword is the forgot_password verification type
VerificationTypeForgotPassword = "forgot_password" VerificationTypeForgotPassword = "forgot_password"
// VerificationTypeInviteMember is the invite_member verification type
VerificationTypeInviteMember = "invite_member"
// VerificationTypeOTP is the otp verification type
VerificationTypeOTP = "verify_otp"
) )

View File

@@ -3,15 +3,24 @@ package cookie
import ( import (
"net/url" "net/url"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// SetAdminCookie sets the admin cookie in the response // SetAdminCookie sets the admin cookie in the response
func SetAdminCookie(gc *gin.Context, token string) { func SetAdminCookie(gc *gin.Context, token string) {
secure := true adminCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAdminCookieSecure)
httpOnly := true if err != nil {
log.Debug("Error while getting admin cookie secure from env variable: %v", err)
adminCookieSecure = true
}
secure := adminCookieSecure
httpOnly := adminCookieSecure
hostname := parsers.GetHost(gc) hostname := parsers.GetHost(gc)
host, _ := parsers.GetHostParts(hostname) host, _ := parsers.GetHostParts(hostname)
gc.SetCookie(constants.AdminCookieName, token, 3600, "/", host, secure, httpOnly) gc.SetCookie(constants.AdminCookieName, token, 3600, "/", host, secure, httpOnly)
@@ -35,8 +44,14 @@ func GetAdminCookie(gc *gin.Context) (string, error) {
// DeleteAdminCookie sets the response cookie to empty // DeleteAdminCookie sets the response cookie to empty
func DeleteAdminCookie(gc *gin.Context) { func DeleteAdminCookie(gc *gin.Context) {
secure := true adminCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAdminCookieSecure)
httpOnly := true if err != nil {
log.Debug("Error while getting admin cookie secure from env variable: %v", err)
adminCookieSecure = true
}
secure := adminCookieSecure
httpOnly := adminCookieSecure
hostname := parsers.GetHost(gc) hostname := parsers.GetHost(gc)
host, _ := parsers.GetHostParts(hostname) host, _ := parsers.GetHostParts(hostname)
gc.SetCookie(constants.AdminCookieName, "", -1, "/", host, secure, httpOnly) gc.SetCookie(constants.AdminCookieName, "", -1, "/", host, secure, httpOnly)

View File

@@ -4,15 +4,24 @@ import (
"net/http" "net/http"
"net/url" "net/url"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// SetSession sets the session cookie in the response // SetSession sets the session cookie in the response
func SetSession(gc *gin.Context, sessionID string) { func SetSession(gc *gin.Context, sessionID string) {
secure := true appCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAppCookieSecure)
httpOnly := true if err != nil {
log.Debug("Error while getting app cookie secure from env variable: %v", err)
appCookieSecure = true
}
secure := appCookieSecure
httpOnly := appCookieSecure
hostname := parsers.GetHost(gc) hostname := parsers.GetHost(gc)
host, _ := parsers.GetHostParts(hostname) host, _ := parsers.GetHostParts(hostname)
domain := parsers.GetDomainName(hostname) domain := parsers.GetDomainName(hostname)
@@ -30,8 +39,14 @@ func SetSession(gc *gin.Context, sessionID string) {
// DeleteSession sets session cookies to expire // DeleteSession sets session cookies to expire
func DeleteSession(gc *gin.Context) { func DeleteSession(gc *gin.Context) {
secure := true appCookieSecure, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyAppCookieSecure)
httpOnly := true if err != nil {
log.Debug("Error while getting app cookie secure from env variable: %v", err)
appCookieSecure = true
}
secure := appCookieSecure
httpOnly := appCookieSecure
hostname := parsers.GetHost(gc) hostname := parsers.GetHost(gc)
host, _ := parsers.GetHostParts(hostname) host, _ := parsers.GetHostParts(hostname)
domain := parsers.GetDomainName(hostname) domain := parsers.GetDomainName(hostname)

View File

@@ -12,7 +12,9 @@ type EmailTemplate struct {
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"` ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"`
EventName string `gorm:"unique" json:"event_name" bson:"event_name" cql:"event_name"` EventName string `gorm:"unique" json:"event_name" bson:"event_name" cql:"event_name"`
Subject string `gorm:"type:text" json:"subject" bson:"subject" cql:"subject"`
Template string `gorm:"type:text" json:"template" bson:"template" cql:"template"` Template string `gorm:"type:text" json:"template" bson:"template" cql:"template"`
Design string `gorm:"type:text" json:"design" bson:"design" cql:"design"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"` CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
} }
@@ -26,7 +28,9 @@ func (e *EmailTemplate) AsAPIEmailTemplate() *model.EmailTemplate {
return &model.EmailTemplate{ return &model.EmailTemplate{
ID: id, ID: id,
EventName: e.EventName, EventName: e.EventName,
Subject: e.Subject,
Template: e.Template, Template: e.Template,
Design: e.Design,
CreatedAt: refs.NewInt64Ref(e.CreatedAt), CreatedAt: refs.NewInt64Ref(e.CreatedAt),
UpdatedAt: refs.NewInt64Ref(e.UpdatedAt), UpdatedAt: refs.NewInt64Ref(e.UpdatedAt),
} }

View File

@@ -9,6 +9,7 @@ type CollectionList struct {
Webhook string Webhook string
WebhookLog string WebhookLog string
EmailTemplate string EmailTemplate string
OTP string
} }
var ( var (
@@ -20,8 +21,9 @@ var (
VerificationRequest: Prefix + "verification_requests", VerificationRequest: Prefix + "verification_requests",
Session: Prefix + "sessions", Session: Prefix + "sessions",
Env: Prefix + "env", Env: Prefix + "env",
Webhook: Prefix + "webhook", Webhook: Prefix + "webhooks",
WebhookLog: Prefix + "webhook_log", WebhookLog: Prefix + "webhook_logs",
EmailTemplate: Prefix + "email_template", EmailTemplate: Prefix + "email_templates",
OTP: Prefix + "otps",
} }
) )

12
server/db/models/otp.go Normal file
View File

@@ -0,0 +1,12 @@
package models
// OTP model for database
type OTP struct {
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"`
Email string `gorm:"unique" json:"email" bson:"email" cql:"email"`
Otp string `json:"otp" bson:"otp" cql:"otp"`
ExpiresAt int64 `json:"expires_at" bson:"expires_at" cql:"expires_at"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
}

View File

@@ -1,6 +1,7 @@
package models package models
import ( import (
"encoding/json"
"strings" "strings"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
@@ -14,51 +15,60 @@ type User struct {
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"` ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"`
Email string `gorm:"unique" json:"email" bson:"email" cql:"email"` Email string `gorm:"unique" json:"email" bson:"email" cql:"email"`
EmailVerifiedAt *int64 `json:"email_verified_at" bson:"email_verified_at" cql:"email_verified_at"` EmailVerifiedAt *int64 `json:"email_verified_at" bson:"email_verified_at" cql:"email_verified_at"`
Password *string `gorm:"type:text" json:"password" bson:"password" cql:"password"` Password *string `gorm:"type:text" json:"password" bson:"password" cql:"password"`
SignupMethods string `json:"signup_methods" bson:"signup_methods" cql:"signup_methods"` SignupMethods string `json:"signup_methods" bson:"signup_methods" cql:"signup_methods"`
GivenName *string `json:"given_name" bson:"given_name" cql:"given_name"` GivenName *string `json:"given_name" bson:"given_name" cql:"given_name"`
FamilyName *string `json:"family_name" bson:"family_name" cql:"family_name"` FamilyName *string `json:"family_name" bson:"family_name" cql:"family_name"`
MiddleName *string `json:"middle_name" bson:"middle_name" cql:"middle_name"` MiddleName *string `json:"middle_name" bson:"middle_name" cql:"middle_name"`
Nickname *string `json:"nickname" bson:"nickname" cql:"nickname"` Nickname *string `json:"nickname" bson:"nickname" cql:"nickname"`
Gender *string `json:"gender" bson:"gender" cql:"gender"` Gender *string `json:"gender" bson:"gender" cql:"gender"`
Birthdate *string `json:"birthdate" bson:"birthdate" cql:"birthdate"` Birthdate *string `json:"birthdate" bson:"birthdate" cql:"birthdate"`
PhoneNumber *string `gorm:"unique" json:"phone_number" bson:"phone_number" cql:"phone_number"` PhoneNumber *string `gorm:"unique" json:"phone_number" bson:"phone_number" cql:"phone_number"`
PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at" cql:"phone_number_verified_at"` PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at" cql:"phone_number_verified_at"`
Picture *string `gorm:"type:text" json:"picture" bson:"picture" cql:"picture"` Picture *string `gorm:"type:text" json:"picture" bson:"picture" cql:"picture"`
Roles string `json:"roles" bson:"roles" cql:"roles"` Roles string `json:"roles" bson:"roles" cql:"roles"`
RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp" cql:"revoked_timestamp"` RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp" cql:"revoked_timestamp"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"` IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled" bson:"is_multi_factor_auth_enabled" cql:"is_multi_factor_auth_enabled"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
} }
func (user *User) AsAPIUser() *model.User { func (user *User) AsAPIUser() *model.User {
isEmailVerified := user.EmailVerifiedAt != nil isEmailVerified := user.EmailVerifiedAt != nil
isPhoneVerified := user.PhoneNumberVerifiedAt != nil isPhoneVerified := user.PhoneNumberVerifiedAt != nil
id := user.ID // id := user.ID
if strings.Contains(id, Collections.WebhookLog+"/") { // if strings.Contains(id, Collections.User+"/") {
id = strings.TrimPrefix(id, Collections.WebhookLog+"/") // id = strings.TrimPrefix(id, Collections.User+"/")
} // }
return &model.User{ return &model.User{
ID: id, ID: user.ID,
Email: user.Email, Email: user.Email,
EmailVerified: isEmailVerified, EmailVerified: isEmailVerified,
SignupMethods: user.SignupMethods, SignupMethods: user.SignupMethods,
GivenName: user.GivenName, GivenName: user.GivenName,
FamilyName: user.FamilyName, FamilyName: user.FamilyName,
MiddleName: user.MiddleName, MiddleName: user.MiddleName,
Nickname: user.Nickname, Nickname: user.Nickname,
PreferredUsername: refs.NewStringRef(user.Email), PreferredUsername: refs.NewStringRef(user.Email),
Gender: user.Gender, Gender: user.Gender,
Birthdate: user.Birthdate, Birthdate: user.Birthdate,
PhoneNumber: user.PhoneNumber, PhoneNumber: user.PhoneNumber,
PhoneNumberVerified: &isPhoneVerified, PhoneNumberVerified: &isPhoneVerified,
Picture: user.Picture, Picture: user.Picture,
Roles: strings.Split(user.Roles, ","), Roles: strings.Split(user.Roles, ","),
RevokedTimestamp: user.RevokedTimestamp, RevokedTimestamp: user.RevokedTimestamp,
CreatedAt: refs.NewInt64Ref(user.CreatedAt), IsMultiFactorAuthEnabled: user.IsMultiFactorAuthEnabled,
UpdatedAt: refs.NewInt64Ref(user.UpdatedAt), CreatedAt: refs.NewInt64Ref(user.CreatedAt),
UpdatedAt: refs.NewInt64Ref(user.UpdatedAt),
} }
} }
func (user *User) ToMap() map[string]interface{} {
res := map[string]interface{}{}
data, _ := json.Marshal(user) // Convert to a json string
json.Unmarshal(data, &res) // Convert to a map
return res
}

View File

@@ -25,8 +25,8 @@ type VerificationRequest struct {
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest { func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
id := v.ID id := v.ID
if strings.Contains(id, Collections.WebhookLog+"/") { if strings.Contains(id, Collections.VerificationRequest+"/") {
id = strings.TrimPrefix(id, Collections.WebhookLog+"/") id = strings.TrimPrefix(id, Collections.VerificationRequest+"/")
} }
return &model.VerificationRequest{ return &model.VerificationRequest{

View File

@@ -16,6 +16,7 @@ import (
func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate models.EmailTemplate) (*model.EmailTemplate, error) { func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate models.EmailTemplate) (*model.EmailTemplate, error) {
if emailTemplate.ID == "" { if emailTemplate.ID == "" {
emailTemplate.ID = uuid.New().String() emailTemplate.ID = uuid.New().String()
emailTemplate.Key = emailTemplate.ID
} }
emailTemplate.Key = emailTemplate.ID emailTemplate.Key = emailTemplate.ID

View File

@@ -15,6 +15,7 @@ import (
func (p *provider) AddEnv(ctx context.Context, env models.Env) (models.Env, error) { func (p *provider) AddEnv(ctx context.Context, env models.Env) (models.Env, error) {
if env.ID == "" { if env.ID == "" {
env.ID = uuid.New().String() env.ID = uuid.New().String()
env.Key = env.ID
} }
env.CreatedAt = time.Now().Unix() env.CreatedAt = time.Now().Unix()

View File

@@ -0,0 +1,92 @@
package arangodb
import (
"context"
"fmt"
"time"
"github.com/arangodb/go-driver"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/google/uuid"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) {
otp, _ := p.GetOTPByEmail(ctx, otpParam.Email)
shouldCreate := false
if otp == nil {
id := uuid.NewString()
otp = &models.OTP{
ID: id,
Key: id,
Otp: otpParam.Otp,
Email: otpParam.Email,
ExpiresAt: otpParam.ExpiresAt,
CreatedAt: time.Now().Unix(),
}
shouldCreate = true
} else {
otp.Otp = otpParam.Otp
otp.ExpiresAt = otpParam.ExpiresAt
}
otp.UpdatedAt = time.Now().Unix()
otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP)
var meta driver.DocumentMeta
var err error
if shouldCreate {
meta, err = otpCollection.CreateDocument(ctx, otp)
} else {
meta, err = otpCollection.UpdateDocument(ctx, otp.Key, otp)
}
if err != nil {
return nil, err
}
otp.Key = meta.Key
otp.ID = meta.ID.String()
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
query := fmt.Sprintf("FOR d in %s FILTER d.email == @email RETURN d", models.Collections.OTP)
bindVars := map[string]interface{}{
"email": emailAddress,
}
cursor, err := p.db.Query(ctx, query, bindVars)
if err != nil {
return nil, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if otp.Key == "" {
return nil, fmt.Errorf("email template not found")
}
break
}
_, err := cursor.ReadDocument(ctx, &otp)
if err != nil {
return nil, err
}
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP)
_, err := otpCollection.RemoveDocument(ctx, otp.ID)
if err != nil {
return err
}
return nil
}

View File

@@ -148,6 +148,20 @@ func NewProvider() (*provider, error) {
Sparse: true, Sparse: true,
}) })
otpCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.OTP)
if !otpCollectionExists {
_, err = arangodb.CreateCollection(ctx, models.Collections.OTP, nil)
if err != nil {
return nil, err
}
}
otpCollection, _ := arangodb.Collection(nil, models.Collections.OTP)
otpCollection.EnsureHashIndex(ctx, []string{"email"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
return &provider{ return &provider{
db: arangodb, db: arangodb,
}, err }, err

View File

@@ -12,6 +12,7 @@ import (
func (p *provider) AddSession(ctx context.Context, session models.Session) error { func (p *provider) AddSession(ctx context.Context, session models.Session) error {
if session.ID == "" { if session.ID == "" {
session.ID = uuid.New().String() session.ID = uuid.New().String()
session.Key = session.ID
} }
session.CreatedAt = time.Now().Unix() session.CreatedAt = time.Now().Unix()

View File

@@ -2,22 +2,26 @@ package arangodb
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/arangodb/go-driver" "github.com/arangodb/go-driver"
arangoDriver "github.com/arangodb/go-driver" arangoDriver "github.com/arangodb/go-driver"
"github.com/google/uuid"
"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/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid"
) )
// AddUser to save user information in database // AddUser to save user information in database
func (p *provider) AddUser(ctx context.Context, user models.User) (models.User, error) { func (p *provider) AddUser(ctx context.Context, user models.User) (models.User, error) {
if user.ID == "" { if user.ID == "" {
user.ID = uuid.New().String() user.ID = uuid.New().String()
user.Key = user.ID
} }
if user.Roles == "" { if user.Roles == "" {
@@ -65,7 +69,7 @@ func (p *provider) DeleteUser(ctx context.Context, user models.User) error {
query := fmt.Sprintf(`FOR d IN %s FILTER d.user_id == @user_id REMOVE { _key: d._key } IN %s`, models.Collections.Session, models.Collections.Session) query := fmt.Sprintf(`FOR d IN %s FILTER d.user_id == @user_id REMOVE { _key: d._key } IN %s`, models.Collections.Session, models.Collections.Session)
bindVars := map[string]interface{}{ bindVars := map[string]interface{}{
"user_id": user.ID, "user_id": user.Key,
} }
cursor, err := p.db.Query(ctx, query, bindVars) cursor, err := p.db.Query(ctx, query, bindVars)
if err != nil { if err != nil {
@@ -174,3 +178,36 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil return user, nil
} }
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
userInfoBytes, err := json.Marshal(data)
if err != nil {
return err
}
query := ""
if ids != nil && len(ids) > 0 {
keysArray := ""
for _, id := range ids {
keysArray += fmt.Sprintf("'%s', ", id)
}
keysArray = strings.Trim(keysArray, " ")
keysArray = strings.TrimSuffix(keysArray, ",")
query = fmt.Sprintf("FOR u IN %s FILTER u._id IN [%s] UPDATE u._key with %s IN %s", models.Collections.User, keysArray, string(userInfoBytes), models.Collections.User)
} else {
query = fmt.Sprintf("FOR u IN %s UPDATE u._key with %s IN %s", models.Collections.User, string(userInfoBytes), models.Collections.User)
}
_, err = p.db.Query(ctx, query, nil)
if err != nil {
return err
}
return nil
}

View File

@@ -15,6 +15,7 @@ import (
func (p *provider) AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error) { func (p *provider) AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error) {
if verificationRequest.ID == "" { if verificationRequest.ID == "" {
verificationRequest.ID = uuid.New().String() verificationRequest.ID = uuid.New().String()
verificationRequest.Key = verificationRequest.ID
} }
verificationRequest.CreatedAt = time.Now().Unix() verificationRequest.CreatedAt = time.Now().Unix()

View File

@@ -16,6 +16,7 @@ import (
func (p *provider) AddWebhook(ctx context.Context, webhook models.Webhook) (*model.Webhook, error) { func (p *provider) AddWebhook(ctx context.Context, webhook models.Webhook) (*model.Webhook, error) {
if webhook.ID == "" { if webhook.ID == "" {
webhook.ID = uuid.New().String() webhook.ID = uuid.New().String()
webhook.Key = webhook.ID
} }
webhook.Key = webhook.ID webhook.Key = webhook.ID

View File

@@ -16,6 +16,7 @@ import (
func (p *provider) AddWebhookLog(ctx context.Context, webhookLog models.WebhookLog) (*model.WebhookLog, error) { func (p *provider) AddWebhookLog(ctx context.Context, webhookLog models.WebhookLog) (*model.WebhookLog, error) {
if webhookLog.ID == "" { if webhookLog.ID == "" {
webhookLog.ID = uuid.New().String() webhookLog.ID = uuid.New().String()
webhookLog.Key = webhookLog.ID
} }
webhookLog.Key = webhookLog.ID webhookLog.Key = webhookLog.ID

View File

@@ -29,7 +29,7 @@ func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate models.Em
return nil, fmt.Errorf("Email template with %s event_name already exists", emailTemplate.EventName) return nil, fmt.Errorf("Email template with %s event_name already exists", emailTemplate.EventName)
} }
insertQuery := fmt.Sprintf("INSERT INTO %s (id, event_name, template, created_at, updated_at) VALUES ('%s', '%s', '%s', %d, %d)", KeySpace+"."+models.Collections.EmailTemplate, emailTemplate.ID, emailTemplate.EventName, emailTemplate.Template, emailTemplate.CreatedAt, emailTemplate.UpdatedAt) insertQuery := fmt.Sprintf("INSERT INTO %s (id, event_name, subject, design, template, created_at, updated_at) VALUES ('%s', '%s', '%s','%s','%s', %d, %d)", KeySpace+"."+models.Collections.EmailTemplate, emailTemplate.ID, emailTemplate.EventName, emailTemplate.Subject, emailTemplate.Design, emailTemplate.Template, emailTemplate.CreatedAt, emailTemplate.UpdatedAt)
err := p.db.Query(insertQuery).Exec() err := p.db.Query(insertQuery).Exec()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -103,14 +103,14 @@ func (p *provider) ListEmailTemplate(ctx context.Context, pagination model.Pagin
// there is no offset in cassandra // there is no offset in cassandra
// so we fetch till limit + offset // so we fetch till limit + offset
// and return the results from offset to limit // and return the results from offset to limit
query := fmt.Sprintf("SELECT id, event_name, template, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.EmailTemplate, pagination.Limit+pagination.Offset) query := fmt.Sprintf("SELECT id, event_name, subject, design, template, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.EmailTemplate, pagination.Limit+pagination.Offset)
scanner := p.db.Query(query).Iter().Scanner() scanner := p.db.Query(query).Iter().Scanner()
counter := int64(0) counter := int64(0)
for scanner.Next() { for scanner.Next() {
if counter >= pagination.Offset { if counter >= pagination.Offset {
var emailTemplate models.EmailTemplate var emailTemplate models.EmailTemplate
err := scanner.Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) err := scanner.Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Subject, &emailTemplate.Design, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -128,8 +128,8 @@ func (p *provider) ListEmailTemplate(ctx context.Context, pagination model.Pagin
// GetEmailTemplateByID to get EmailTemplate by id // GetEmailTemplateByID to get EmailTemplate by id
func (p *provider) GetEmailTemplateByID(ctx context.Context, emailTemplateID string) (*model.EmailTemplate, error) { func (p *provider) GetEmailTemplateByID(ctx context.Context, emailTemplateID string) (*model.EmailTemplate, error) {
var emailTemplate models.EmailTemplate var emailTemplate models.EmailTemplate
query := fmt.Sprintf(`SELECT id, event_name, template, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1`, KeySpace+"."+models.Collections.EmailTemplate, emailTemplateID) query := fmt.Sprintf(`SELECT id, event_name, subject, design, template, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1`, KeySpace+"."+models.Collections.EmailTemplate, emailTemplateID)
err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Subject, &emailTemplate.Design, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -139,8 +139,8 @@ func (p *provider) GetEmailTemplateByID(ctx context.Context, emailTemplateID str
// GetEmailTemplateByEventName to get EmailTemplate by event_name // GetEmailTemplateByEventName to get EmailTemplate by event_name
func (p *provider) GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error) { func (p *provider) GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error) {
var emailTemplate models.EmailTemplate var emailTemplate models.EmailTemplate
query := fmt.Sprintf(`SELECT id, event_name, template, created_at, updated_at FROM %s WHERE event_name = '%s' LIMIT 1 ALLOW FILTERING`, KeySpace+"."+models.Collections.EmailTemplate, eventName) query := fmt.Sprintf(`SELECT id, event_name, subject, design, template, created_at, updated_at FROM %s WHERE event_name = '%s' LIMIT 1 ALLOW FILTERING`, KeySpace+"."+models.Collections.EmailTemplate, eventName)
err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Subject, &emailTemplate.Design, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -0,0 +1,67 @@
package cassandradb
import (
"context"
"fmt"
"time"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/gocql/gocql"
"github.com/google/uuid"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) {
otp, _ := p.GetOTPByEmail(ctx, otpParam.Email)
shouldCreate := false
if otp == nil {
shouldCreate = true
otp = &models.OTP{
ID: uuid.NewString(),
Otp: otpParam.Otp,
Email: otpParam.Email,
ExpiresAt: otpParam.ExpiresAt,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
} else {
otp.Otp = otpParam.Otp
otp.ExpiresAt = otpParam.ExpiresAt
}
otp.UpdatedAt = time.Now().Unix()
query := ""
if shouldCreate {
query = fmt.Sprintf(`INSERT INTO %s (id, email, otp, expires_at, created_at, updated_at) VALUES ('%s', '%s', '%s', %d, %d, %d)`, KeySpace+"."+models.Collections.OTP, otp.ID, otp.Email, otp.Otp, otp.ExpiresAt, otp.CreatedAt, otp.UpdatedAt)
} else {
query = fmt.Sprintf(`UPDATE %s SET otp = '%s', expires_at = %d, updated_at = %d WHERE id = '%s'`, KeySpace+"."+models.Collections.OTP, otp.Otp, otp.ExpiresAt, otp.UpdatedAt, otp.ID)
}
err := p.db.Query(query).Exec()
if err != nil {
return nil, err
}
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
query := fmt.Sprintf(`SELECT id, email, otp, expires_at, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING`, KeySpace+"."+models.Collections.OTP, emailAddress)
err := p.db.Query(query).Consistency(gocql.One).Scan(&otp.ID, &otp.Email, &otp.Otp, &otp.ExpiresAt, &otp.CreatedAt, &otp.UpdatedAt)
if err != nil {
return nil, err
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
query := fmt.Sprintf("DELETE FROM %s WHERE id = '%s'", KeySpace+"."+models.Collections.OTP, otp.ID)
err := p.db.Query(query).Exec()
if err != nil {
return err
}
return nil
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/gocql/gocql" "github.com/gocql/gocql"
cansandraDriver "github.com/gocql/gocql" cansandraDriver "github.com/gocql/gocql"
log "github.com/sirupsen/logrus"
) )
type provider struct { type provider struct {
@@ -99,6 +100,7 @@ func NewProvider() (*provider, error) {
cassandraClient.Consistency = gocql.LocalQuorum cassandraClient.Consistency = gocql.LocalQuorum
cassandraClient.ConnectTimeout = 10 * time.Second cassandraClient.ConnectTimeout = 10 * time.Second
cassandraClient.ProtoVersion = 4 cassandraClient.ProtoVersion = 4
cassandraClient.Timeout = 30 * time.Minute // for large data
session, err := cassandraClient.CreateSession() session, err := cassandraClient.CreateSession()
if err != nil { if err != nil {
@@ -159,6 +161,13 @@ func NewProvider() (*provider, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// add is_multi_factor_auth_enabled on users table
userTableAlterQuery := fmt.Sprintf(`ALTER TABLE %s.%s ADD is_multi_factor_auth_enabled boolean`, KeySpace, models.Collections.User)
err = session.Query(userTableAlterQuery).Exec()
if err != nil {
log.Debug("Failed to alter table as column exists: ", err)
// return nil, err
}
// token is reserved keyword in cassandra, hence we need to use jwt_token // token is reserved keyword in cassandra, hence we need to use jwt_token
verificationRequestCollectionQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, jwt_token text, identifier text, expires_at bigint, email text, nonce text, redirect_uri text, created_at bigint, updated_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.VerificationRequest) verificationRequestCollectionQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, jwt_token text, identifier text, expires_at bigint, email text, nonce text, redirect_uri text, created_at bigint, updated_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.VerificationRequest)
@@ -214,6 +223,24 @@ func NewProvider() (*provider, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// add subject on email_templates table
emailTemplateAlterQuery := fmt.Sprintf(`ALTER TABLE %s.%s ADD (subject text, design text);`, KeySpace, models.Collections.EmailTemplate)
err = session.Query(emailTemplateAlterQuery).Exec()
if err != nil {
log.Debug("Failed to alter table as column exists: ", err)
// continue
}
otpCollection := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, email text, otp text, expires_at bigint, updated_at bigint, created_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.OTP)
err = session.Query(otpCollection).Exec()
if err != nil {
return nil, err
}
otpIndexQuery := fmt.Sprintf("CREATE INDEX IF NOT EXISTS authorizer_otp_email ON %s.%s (email)", KeySpace, models.Collections.OTP)
err = session.Query(otpIndexQuery).Exec()
if err != nil {
return nil, err
}
return &provider{ return &provider{
db: session, db: session,

View File

@@ -107,7 +107,7 @@ func (p *provider) UpdateUser(ctx context.Context, user models.User) (models.Use
} }
if value == nil { if value == nil {
updateFields += fmt.Sprintf("%s = null,", key) updateFields += fmt.Sprintf("%s = null, ", key)
continue continue
} }
@@ -122,7 +122,6 @@ func (p *provider) UpdateUser(ctx context.Context, user models.User) (models.Use
updateFields = strings.TrimSuffix(updateFields, ",") updateFields = strings.TrimSuffix(updateFields, ",")
query := fmt.Sprintf("UPDATE %s SET %s WHERE id = '%s'", KeySpace+"."+models.Collections.User, updateFields, user.ID) query := fmt.Sprintf("UPDATE %s SET %s WHERE id = '%s'", KeySpace+"."+models.Collections.User, updateFields, user.ID)
err = p.db.Query(query).Exec() err = p.db.Query(query).Exec()
if err != nil { if err != nil {
return user, err return user, err
@@ -173,14 +172,14 @@ func (p *provider) ListUsers(ctx context.Context, pagination model.Pagination) (
// there is no offset in cassandra // there is no offset in cassandra
// so we fetch till limit + offset // so we fetch till limit + offset
// and return the results from offset to limit // and return the results from offset to limit
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.User, pagination.Limit+pagination.Offset) query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.User, pagination.Limit+pagination.Offset)
scanner := p.db.Query(query).Iter().Scanner() scanner := p.db.Query(query).Iter().Scanner()
counter := int64(0) counter := int64(0)
for scanner.Next() { for scanner.Next() {
if counter >= pagination.Offset { if counter >= pagination.Offset {
var user models.User var user models.User
err := scanner.Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt) err := scanner.Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -197,8 +196,8 @@ func (p *provider) ListUsers(ctx context.Context, pagination model.Pagination) (
// GetUserByEmail to get user information from database using email address // GetUserByEmail to get user information from database using email address
func (p *provider) GetUserByEmail(ctx context.Context, email string) (models.User, error) { func (p *provider) GetUserByEmail(ctx context.Context, email string) (models.User, error) {
var user models.User var user models.User
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING", KeySpace+"."+models.Collections.User, email) query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING", KeySpace+"."+models.Collections.User, email)
err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt) err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
if err != nil { if err != nil {
return user, err return user, err
} }
@@ -208,10 +207,95 @@ func (p *provider) GetUserByEmail(ctx context.Context, email string) (models.Use
// GetUserByID to get user information from database using user ID // GetUserByID to get user information from database using user ID
func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, error) { func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, error) {
var user models.User var user models.User
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, id) query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, id)
err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt) err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
if err != nil { if err != nil {
return user, err return user, err
} }
return user, nil return user, nil
} }
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
updateFields := ""
for key, value := range data {
if key == "_id" {
continue
}
if key == "_key" {
continue
}
if value == nil {
updateFields += fmt.Sprintf("%s = null,", key)
continue
}
valueType := reflect.TypeOf(value)
if valueType.Name() == "string" {
updateFields += fmt.Sprintf("%s = '%s', ", key, value.(string))
} else {
updateFields += fmt.Sprintf("%s = %v, ", key, value)
}
}
updateFields = strings.Trim(updateFields, " ")
updateFields = strings.TrimSuffix(updateFields, ",")
query := ""
if ids != nil && len(ids) > 0 {
idsString := ""
for _, id := range ids {
idsString += fmt.Sprintf("'%s', ", id)
}
idsString = strings.Trim(idsString, " ")
idsString = strings.TrimSuffix(idsString, ",")
query = fmt.Sprintf("UPDATE %s SET %s WHERE id IN (%s)", KeySpace+"."+models.Collections.User, updateFields, idsString)
err := p.db.Query(query).Exec()
if err != nil {
return err
}
} else {
// get all ids
getUserIDsQuery := fmt.Sprintf(`SELECT id FROM %s`, KeySpace+"."+models.Collections.User)
scanner := p.db.Query(getUserIDsQuery).Iter().Scanner()
// only 100 ids are allowed in 1 query
// hence we need create multiple update queries
idsString := ""
idsStringArray := []string{idsString}
counter := 1
for scanner.Next() {
var id string
err := scanner.Scan(&id)
if err == nil {
idsString += fmt.Sprintf("'%s', ", id)
}
counter++
if counter > 100 {
idsStringArray = append(idsStringArray, idsString)
counter = 1
idsString = ""
} else {
// update the last index of array when count is less than 100
idsStringArray[len(idsStringArray)-1] = idsString
}
}
for _, idStr := range idsStringArray {
idStr = strings.Trim(idStr, " ")
idStr = strings.TrimSuffix(idStr, ",")
query = fmt.Sprintf("UPDATE %s SET %s WHERE id IN (%s)", KeySpace+"."+models.Collections.User, updateFields, idStr)
err := p.db.Query(query).Exec()
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,70 @@
package mongodb
import (
"context"
"time"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) {
otp, _ := p.GetOTPByEmail(ctx, otpParam.Email)
shouldCreate := false
if otp == nil {
id := uuid.NewString()
otp = &models.OTP{
ID: id,
Key: id,
Otp: otpParam.Otp,
Email: otpParam.Email,
ExpiresAt: otpParam.ExpiresAt,
CreatedAt: time.Now().Unix(),
}
shouldCreate = true
} else {
otp.Otp = otpParam.Otp
otp.ExpiresAt = otpParam.ExpiresAt
}
otp.UpdatedAt = time.Now().Unix()
otpCollection := p.db.Collection(models.Collections.OTP, options.Collection())
var err error
if shouldCreate {
_, err = otpCollection.InsertOne(ctx, otp)
} else {
_, err = otpCollection.UpdateOne(ctx, bson.M{"_id": bson.M{"$eq": otp.ID}}, bson.M{"$set": otp}, options.MergeUpdateOptions())
}
if err != nil {
return nil, err
}
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
otpCollection := p.db.Collection(models.Collections.OTP, options.Collection())
err := otpCollection.FindOne(ctx, bson.M{"email": emailAddress}).Decode(&otp)
if err != nil {
return nil, err
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
otpCollection := p.db.Collection(models.Collections.OTP, options.Collection())
_, err := otpCollection.DeleteOne(nil, bson.M{"_id": otp.ID}, options.Delete())
if err != nil {
return err
}
return nil
}

View File

@@ -110,6 +110,15 @@ func NewProvider() (*provider, error) {
}, },
}, options.CreateIndexes()) }, options.CreateIndexes())
mongodb.CreateCollection(ctx, models.Collections.OTP, options.CreateCollection())
otpCollection := mongodb.Collection(models.Collections.OTP, options.Collection())
otpCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
{
Keys: bson.M{"email": 1},
Options: options.Index().SetUnique(true).SetSparse(true),
},
}, options.CreateIndexes())
return &provider{ return &provider{
db: mongodb, db: mongodb,
}, nil }, nil

View File

@@ -9,7 +9,9 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid" "github.com/google/uuid"
log "github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
) )
@@ -129,3 +131,27 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil return user, nil
} }
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
userCollection := p.db.Collection(models.Collections.User, options.Collection())
var res *mongo.UpdateResult
var err error
if ids != nil && len(ids) > 0 {
res, err = userCollection.UpdateMany(ctx, bson.M{"_id": bson.M{"$in": ids}}, bson.M{"$set": data})
} else {
res, err = userCollection.UpdateMany(ctx, bson.M{}, bson.M{"$set": data})
}
if err != nil {
return err
} else {
log.Info("Updated users: ", res.ModifiedCount)
}
return nil
}

View File

@@ -0,0 +1,22 @@
package provider_template
import (
"context"
"github.com/authorizerdev/authorizer/server/db/models"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) {
return nil, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
return nil, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
return nil
}

View File

@@ -60,3 +60,12 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil return user, nil
} }
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
return nil
}

View File

@@ -20,6 +20,9 @@ type Provider interface {
GetUserByEmail(ctx context.Context, email string) (models.User, error) GetUserByEmail(ctx context.Context, 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
GetUserByID(ctx context.Context, id string) (models.User, error) GetUserByID(ctx context.Context, id string) (models.User, error)
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error
// AddVerification to save verification request in database // AddVerification to save verification request in database
AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error) AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error)
@@ -72,4 +75,11 @@ type Provider interface {
GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error) GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error)
// DeleteEmailTemplate to delete EmailTemplate // DeleteEmailTemplate to delete EmailTemplate
DeleteEmailTemplate(ctx context.Context, emailTemplate *model.EmailTemplate) error DeleteEmailTemplate(ctx context.Context, emailTemplate *model.EmailTemplate) error
// UpsertOTP to add or update otp
UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error)
// GetOTPByEmail to get otp for a given email address
GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error)
// DeleteOTP to delete otp
DeleteOTP(ctx context.Context, otp *models.OTP) error
} }

View File

@@ -0,0 +1,53 @@
package sql
import (
"context"
"time"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/google/uuid"
"gorm.io/gorm/clause"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) {
if otp.ID == "" {
otp.ID = uuid.New().String()
}
otp.Key = otp.ID
otp.CreatedAt = time.Now().Unix()
otp.UpdatedAt = time.Now().Unix()
res := p.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}},
DoUpdates: clause.AssignmentColumns([]string{"otp", "expires_at", "updated_at"}),
}).Create(&otp)
if res.Error != nil {
return nil, res.Error
}
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
result := p.db.Where("email = ?", emailAddress).First(&otp)
if result.Error != nil {
return nil, result.Error
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
result := p.db.Delete(&models.OTP{
ID: otp.ID,
})
if result.Error != nil {
return result.Error
}
return nil
}

View File

@@ -40,6 +40,7 @@ func NewProvider() (*provider, error) {
NamingStrategy: schema.NamingStrategy{ NamingStrategy: schema.NamingStrategy{
TablePrefix: models.Prefix, TablePrefix: models.Prefix,
}, },
AllowGlobalUpdate: true,
} }
dbType := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseType dbType := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseType
@@ -60,7 +61,7 @@ func NewProvider() (*provider, error) {
return nil, err return nil, err
} }
err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, models.WebhookLog{}, models.EmailTemplate{}) err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, models.WebhookLog{}, models.EmailTemplate{}, &models.OTP{})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -9,6 +9,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
@@ -121,3 +122,22 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil return user, nil
} }
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
var res *gorm.DB
if ids != nil && len(ids) > 0 {
res = p.db.Model(&models.User{}).Where("id in ?", ids).Updates(data)
} else {
res = p.db.Model(&models.User{}).Updates(data)
}
if res.Error != nil {
return res.Error
}
return nil
}

View File

@@ -2,8 +2,8 @@ package email
import ( import (
"bytes" "bytes"
"context"
"crypto/tls" "crypto/tls"
"encoding/json"
"strconv" "strconv"
"text/template" "text/template"
@@ -11,27 +11,75 @@ import (
gomail "gopkg.in/mail.v2" gomail "gopkg.in/mail.v2"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
) )
// addEmailTemplate is used to add html template in email body func getDefaultTemplate(event string) *model.EmailTemplate {
func addEmailTemplate(a string, b map[string]interface{}, templateName string) string { switch event {
tmpl, err := template.New(templateName).Parse(a) case constants.VerificationTypeBasicAuthSignup, constants.VerificationTypeMagicLinkLogin, constants.VerificationTypeUpdateEmail:
if err != nil { return &model.EmailTemplate{
output, _ := json.Marshal(b) Subject: emailVerificationSubject,
return string(output) Template: emailVerificationTemplate,
}
case constants.VerificationTypeForgotPassword:
return &model.EmailTemplate{
Subject: forgotPasswordSubject,
Template: forgotPasswordTemplate,
}
case constants.VerificationTypeInviteMember:
return &model.EmailTemplate{
Subject: inviteEmailSubject,
Template: inviteEmailTemplate,
}
case constants.VerificationTypeOTP:
return &model.EmailTemplate{
Subject: otpEmailSubject,
Template: otpEmailTemplate,
}
default:
return nil
} }
buf := &bytes.Buffer{}
err = tmpl.Execute(buf, b)
if err != nil {
panic(err)
}
s := buf.String()
return s
} }
// SendMail function to send mail func getEmailTemplate(event string, data map[string]interface{}) (*model.EmailTemplate, error) {
func SendMail(to []string, Subject, bodyMessage string) error { ctx := context.Background()
tmp, err := db.Provider.GetEmailTemplateByEventName(ctx, event)
if err != nil || tmp == nil {
tmp = getDefaultTemplate(event)
}
templ, err := template.New(event + "_template.tmpl").Parse(tmp.Template)
if err != nil {
return nil, err
}
buf := &bytes.Buffer{}
err = templ.Execute(buf, data)
if err != nil {
return nil, err
}
templateString := buf.String()
subject, err := template.New(event + "_subject.tmpl").Parse(tmp.Subject)
if err != nil {
return nil, err
}
buf = &bytes.Buffer{}
err = subject.Execute(buf, data)
if err != nil {
return nil, err
}
subjectString := buf.String()
return &model.EmailTemplate{
Template: templateString,
Subject: subjectString,
}, nil
}
// SendEmail function to send mail
func SendEmail(to []string, event string, data map[string]interface{}) error {
// dont trigger email sending in case of test // dont trigger email sending in case of test
envKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyEnv) envKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyEnv)
if err != nil { if err != nil {
@@ -40,6 +88,13 @@ func SendMail(to []string, Subject, bodyMessage string) error {
if envKey == constants.TestEnv { if envKey == constants.TestEnv {
return nil return nil
} }
tmp, err := getEmailTemplate(event, data)
if err != nil {
log.Errorf("Failed to get event template: ", err)
return err
}
m := gomail.NewMessage() m := gomail.NewMessage()
senderEmail, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeySenderEmail) senderEmail, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeySenderEmail)
if err != nil { if err != nil {
@@ -79,8 +134,8 @@ func SendMail(to []string, Subject, bodyMessage string) error {
m.SetHeader("From", senderEmail) m.SetHeader("From", senderEmail)
m.SetHeader("To", to...) m.SetHeader("To", to...)
m.SetHeader("Subject", Subject) m.SetHeader("Subject", tmp.Subject)
m.SetBody("text/html", bodyMessage) m.SetBody("text/html", tmp.Template)
port, _ := strconv.Atoi(smtpPort) port, _ := strconv.Atoi(smtpPort)
d := gomail.NewDialer(smtpHost, port, smtpUsername, smtpPassword) d := gomail.NewDialer(smtpHost, port, smtpUsername, smtpPassword)
if !isProd { if !isProd {

View File

@@ -1,19 +1,8 @@
package email package email
import ( const (
log "github.com/sirupsen/logrus" emailVerificationSubject = "Please verify your email"
emailVerificationTemplate = `
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/memorystore"
)
// SendVerificationMail to send verification email
func SendVerificationMail(toEmail, token, hostname string) error {
// The receiver needs to be in slice as the receive supports multiple receiver
Receiver := []string{toEmail}
Subject := "Please verify your email"
message := `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
@@ -98,23 +87,4 @@ func SendVerificationMail(toEmail, token, hostname string) error {
</body> </body>
</html> </html>
` `
data := make(map[string]interface{}, 3) )
var err error
data["org_logo"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
if err != nil {
return err
}
data["org_name"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
if err != nil {
return err
}
data["verification_url"] = hostname + "/verify_email?token=" + token
message = addEmailTemplate(message, data, "verify_email.tmpl")
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
err = SendMail(Receiver, Subject, message)
if err != nil {
log.Warn("error sending email: ", err)
}
return err
}

View File

@@ -1,28 +1,8 @@
package email package email
import ( const (
"github.com/authorizerdev/authorizer/server/constants" forgotPasswordSubject = "Reset Password"
"github.com/authorizerdev/authorizer/server/memorystore" forgotPasswordTemplate = `
)
// SendForgotPasswordMail to send forgot password email
func SendForgotPasswordMail(toEmail, token, hostname string) error {
resetPasswordUrl, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyResetPasswordURL)
if err != nil {
return err
}
if resetPasswordUrl == "" {
if err := memorystore.Provider.UpdateEnvVariable(constants.EnvKeyResetPasswordURL, hostname+"/app/reset-password"); err != nil {
return err
}
}
// The receiver needs to be in slice as the receive supports multiple receiver
Receiver := []string{toEmail}
Subject := "Reset Password"
message := `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
@@ -73,13 +53,13 @@ func SendForgotPasswordMail(toEmail, token, hostname string) error {
<table width="100%%" cellspacing="0" cellpadding="0"> <table width="100%%" cellspacing="0" cellpadding="0">
<tbody> <tbody>
<tr> <tr>
<td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank" clicktracking="off"><img src="{{.org_logo}}" alt="icon" style="display: block;" title="icon" width="30"></a></td> <td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank" clicktracking="off"><img src="{{.organization.logo}}" alt="icon" style="display: block;" title="icon" width="30"></a></td>
</tr> </tr>
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;"> <tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;"> <td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
<p>Hey there 👋</p> <p>Hey there 👋</p>
<p>We have received a request to reset password for email: <b>{{.org_name}}</b>. If this is correct, please reset the password clicking the button below.</p> <br/> <p>We have received a request to reset password for email: <b>{{.organization.name}}</b>. If this is correct, please reset the password clicking the button below.</p> <br/>
<a clicktracking="off" href="{{.verification_url}}" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Reset Password</a> <a clicktracking="off" href="{{.verification_url}}" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Reset Password</a>
</td> </td>
</tr> </tr>
@@ -106,18 +86,4 @@ func SendForgotPasswordMail(toEmail, token, hostname string) error {
</body> </body>
</html> </html>
` `
)
data := make(map[string]interface{}, 3)
data["org_logo"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
if err != nil {
return err
}
data["org_name"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
if err != nil {
return err
}
data["verification_url"] = resetPasswordUrl + "?token=" + token
message = addEmailTemplate(message, data, "reset_password_email.tmpl")
return SendMail(Receiver, Subject, message)
}

View File

@@ -1,19 +1,8 @@
package email package email
import ( const (
log "github.com/sirupsen/logrus" inviteEmailSubject = "Please accept the invitation"
inviteEmailTemplate = `
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/memorystore"
)
// InviteEmail to send invite email
func InviteEmail(toEmail, token, verificationURL, redirectURI string) error {
// The receiver needs to be in slice as the receive supports multiple receiver
Receiver := []string{toEmail}
Subject := "Please accept the invitation"
message := `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head> <head>
@@ -64,13 +53,13 @@ func InviteEmail(toEmail, token, verificationURL, redirectURI string) error {
<table width="100%%" cellspacing="0" cellpadding="0"> <table width="100%%" cellspacing="0" cellpadding="0">
<tbody> <tbody>
<tr> <tr>
<td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank" clicktracking="off"><img src="{{.org_logo}}" alt="icon" style="display: block;" title="icon" width="30"></a></td> <td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank" clicktracking="off"><img src="{{.organization.logo}}" alt="icon" style="display: block;" title="icon" width="30"></a></td>
</tr> </tr>
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;"> <tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;"> <td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
<p>Hi there 👋</p> <p>Hi there 👋</p>
<p>Join us! You are invited to sign-up for <b>{{.org_name}}</b>. Please accept the invitation by clicking the button below.</p> <br/> <p>Join us! You are invited to sign-up for <b>{{.organization.name}}</b>. Please accept the invitation by clicking the button below.</p> <br/>
<a <a
clicktracking="off" href="{{.verification_url}}" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Get Started</a> clicktracking="off" href="{{.verification_url}}" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Get Started</a>
</td> </td>
@@ -98,23 +87,4 @@ func InviteEmail(toEmail, token, verificationURL, redirectURI string) error {
</body> </body>
</html> </html>
` `
data := make(map[string]interface{}, 3) )
var err error
data["org_logo"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
if err != nil {
return err
}
data["org_name"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
if err != nil {
return err
}
data["verification_url"] = verificationURL + "?token=" + token + "&redirect_uri=" + redirectURI
message = addEmailTemplate(message, data, "invite_email.tmpl")
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
err = SendMail(Receiver, Subject, message)
if err != nil {
log.Warn("error sending email: ", err)
}
return err
}

88
server/email/otp.go Normal file
View File

@@ -0,0 +1,88 @@
package email
const (
otpEmailSubject = "OTP for your multi factor authentication"
otpEmailTemplate = `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="telephone=no" name="format-detection">
<title></title>
<!--[if (mso 16)]>
<style type="text/css">
a {}
</style>
<![endif]-->
<!--[if gte mso 9]><style>sup { font-size: 100%% !important; }</style><![endif]-->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG></o:AllowPNG>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body style="font-family: sans-serif;">
<div class="es-wrapper-color">
<!--[if gte mso 9]>
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
<v:fill type="tile" color="#ffffff"></v:fill>
</v:background>
<![endif]-->
<table class="es-wrapper" width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-email-paddings" valign="top">
<table class="es-content esd-footer-popover" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td class="esd-stripe" align="center">
<table class="es-content-body" style="border-left:1px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent;border-bottom:1px solid transparent;padding:20px 0px;" width="600" cellspacing="0" cellpadding="0" bgcolor="#ffffff" align="center">
<tbody>
<tr>
<td class="esd-structure es-p20t es-p40b es-p40r es-p40l" esd-custom-block-id="8537" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-container-frame" width="518" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank" clicktracking="off"><img src="{{.organization.logo}}" alt="icon" style="display: block;" title="icon" width="30"></a></td>
</tr>
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
<p>Hey there 👋</p>
<b>{{.otp}}</b> is your one time password (OTP) for accessing {{.organization.name}}. Please keep your OTP confidential and it will expire in 1 minute.
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<div style="position: absolute; left: -9999px; top: -9999px; margin: 0px;"></div>
</body>
</html>
`
)

101
server/env/env.go vendored
View File

@@ -72,11 +72,15 @@ func InitAllEnv() error {
osLinkedInClientSecret := os.Getenv(constants.EnvKeyLinkedInClientSecret) osLinkedInClientSecret := os.Getenv(constants.EnvKeyLinkedInClientSecret)
osAppleClientID := os.Getenv(constants.EnvKeyAppleClientID) osAppleClientID := os.Getenv(constants.EnvKeyAppleClientID)
osAppleClientSecret := os.Getenv(constants.EnvKeyAppleClientSecret) osAppleClientSecret := os.Getenv(constants.EnvKeyAppleClientSecret)
osTwitterClientID := os.Getenv(constants.EnvKeyTwitterClientID)
osTwitterClientSecret := os.Getenv(constants.EnvKeyTwitterClientSecret)
osResetPasswordURL := os.Getenv(constants.EnvKeyResetPasswordURL) osResetPasswordURL := os.Getenv(constants.EnvKeyResetPasswordURL)
osOrganizationName := os.Getenv(constants.EnvKeyOrganizationName) osOrganizationName := os.Getenv(constants.EnvKeyOrganizationName)
osOrganizationLogo := os.Getenv(constants.EnvKeyOrganizationLogo) osOrganizationLogo := os.Getenv(constants.EnvKeyOrganizationLogo)
// os bool vars // os bool vars
osAppCookieSecure := os.Getenv(constants.EnvKeyAppCookieSecure)
osAdminCookieSecure := os.Getenv(constants.EnvKeyAdminCookieSecure)
osDisableBasicAuthentication := os.Getenv(constants.EnvKeyDisableBasicAuthentication) osDisableBasicAuthentication := os.Getenv(constants.EnvKeyDisableBasicAuthentication)
osDisableEmailVerification := os.Getenv(constants.EnvKeyDisableEmailVerification) osDisableEmailVerification := os.Getenv(constants.EnvKeyDisableEmailVerification)
osDisableMagicLinkLogin := os.Getenv(constants.EnvKeyDisableMagicLinkLogin) osDisableMagicLinkLogin := os.Getenv(constants.EnvKeyDisableMagicLinkLogin)
@@ -84,6 +88,8 @@ func InitAllEnv() error {
osDisableSignUp := os.Getenv(constants.EnvKeyDisableSignUp) osDisableSignUp := os.Getenv(constants.EnvKeyDisableSignUp)
osDisableRedisForEnv := os.Getenv(constants.EnvKeyDisableRedisForEnv) osDisableRedisForEnv := os.Getenv(constants.EnvKeyDisableRedisForEnv)
osDisableStrongPassword := os.Getenv(constants.EnvKeyDisableStrongPassword) osDisableStrongPassword := os.Getenv(constants.EnvKeyDisableStrongPassword)
osEnforceMultiFactorAuthentication := os.Getenv(constants.EnvKeyEnforceMultiFactorAuthentication)
osDisableMultiFactorAuthentication := os.Getenv(constants.EnvKeyDisableMultiFactorAuthentication)
// os slice vars // os slice vars
osAllowedOrigins := os.Getenv(constants.EnvKeyAllowedOrigins) osAllowedOrigins := os.Getenv(constants.EnvKeyAllowedOrigins)
@@ -353,31 +359,45 @@ func InitAllEnv() error {
if val, ok := envData[constants.EnvKeyLinkedInClientID]; !ok || val == "" { if val, ok := envData[constants.EnvKeyLinkedInClientID]; !ok || val == "" {
envData[constants.EnvKeyLinkedInClientID] = osLinkedInClientID envData[constants.EnvKeyLinkedInClientID] = osLinkedInClientID
} }
if osFacebookClientID != "" && envData[constants.EnvKeyLinkedInClientID] != osFacebookClientID { if osLinkedInClientID != "" && envData[constants.EnvKeyLinkedInClientID] != osLinkedInClientID {
envData[constants.EnvKeyLinkedInClientID] = osLinkedInClientID envData[constants.EnvKeyLinkedInClientID] = osLinkedInClientID
} }
if val, ok := envData[constants.EnvKeyLinkedInClientSecret]; !ok || val == "" { if val, ok := envData[constants.EnvKeyLinkedInClientSecret]; !ok || val == "" {
envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret
} }
if osFacebookClientSecret != "" && envData[constants.EnvKeyLinkedInClientSecret] != osFacebookClientSecret { if osLinkedInClientSecret != "" && envData[constants.EnvKeyLinkedInClientSecret] != osLinkedInClientSecret {
envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret
} }
if val, ok := envData[constants.EnvKeyAppleClientID]; !ok || val == "" { if val, ok := envData[constants.EnvKeyAppleClientID]; !ok || val == "" {
envData[constants.EnvKeyAppleClientID] = osAppleClientID envData[constants.EnvKeyAppleClientID] = osAppleClientID
} }
if osFacebookClientID != "" && envData[constants.EnvKeyAppleClientID] != osFacebookClientID { if osAppleClientID != "" && envData[constants.EnvKeyAppleClientID] != osAppleClientID {
envData[constants.EnvKeyAppleClientID] = osAppleClientID envData[constants.EnvKeyAppleClientID] = osAppleClientID
} }
if val, ok := envData[constants.EnvKeyAppleClientSecret]; !ok || val == "" { if val, ok := envData[constants.EnvKeyAppleClientSecret]; !ok || val == "" {
envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret
} }
if osFacebookClientSecret != "" && envData[constants.EnvKeyAppleClientSecret] != osFacebookClientSecret { if osAppleClientSecret != "" && envData[constants.EnvKeyAppleClientSecret] != osAppleClientSecret {
envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret
} }
if val, ok := envData[constants.EnvKeyTwitterClientID]; !ok || val == "" {
envData[constants.EnvKeyTwitterClientID] = osTwitterClientID
}
if osTwitterClientID != "" && envData[constants.EnvKeyTwitterClientID] != osTwitterClientID {
envData[constants.EnvKeyTwitterClientID] = osTwitterClientID
}
if val, ok := envData[constants.EnvKeyTwitterClientSecret]; !ok || val == "" {
envData[constants.EnvKeyTwitterClientSecret] = osTwitterClientSecret
}
if osTwitterClientSecret != "" && envData[constants.EnvKeyTwitterClientSecret] != osTwitterClientSecret {
envData[constants.EnvKeyTwitterClientSecret] = osTwitterClientSecret
}
if val, ok := envData[constants.EnvKeyResetPasswordURL]; !ok || val == "" { if val, ok := envData[constants.EnvKeyResetPasswordURL]; !ok || val == "" {
envData[constants.EnvKeyResetPasswordURL] = strings.TrimPrefix(osResetPasswordURL, "/") envData[constants.EnvKeyResetPasswordURL] = strings.TrimPrefix(osResetPasswordURL, "/")
} }
@@ -399,6 +419,40 @@ func InitAllEnv() error {
envData[constants.EnvKeyOrganizationLogo] = osOrganizationLogo envData[constants.EnvKeyOrganizationLogo] = osOrganizationLogo
} }
if _, ok := envData[constants.EnvKeyAppCookieSecure]; !ok {
if osAppCookieSecure == "" {
envData[constants.EnvKeyAppCookieSecure] = true
} else {
envData[constants.EnvKeyAppCookieSecure] = osAppCookieSecure == "true"
}
}
if osAppCookieSecure != "" {
boolValue, err := strconv.ParseBool(osAppCookieSecure)
if err != nil {
return err
}
if boolValue != envData[constants.EnvKeyAppCookieSecure].(bool) {
envData[constants.EnvKeyAppCookieSecure] = boolValue
}
}
if _, ok := envData[constants.EnvKeyAdminCookieSecure]; !ok {
if osAdminCookieSecure == "" {
envData[constants.EnvKeyAdminCookieSecure] = true
} else {
envData[constants.EnvKeyAdminCookieSecure] = osAdminCookieSecure == "true"
}
}
if osAdminCookieSecure != "" {
boolValue, err := strconv.ParseBool(osAdminCookieSecure)
if err != nil {
return err
}
if boolValue != envData[constants.EnvKeyAdminCookieSecure].(bool) {
envData[constants.EnvKeyAdminCookieSecure] = boolValue
}
}
if _, ok := envData[constants.EnvKeyDisableBasicAuthentication]; !ok { if _, ok := envData[constants.EnvKeyDisableBasicAuthentication]; !ok {
envData[constants.EnvKeyDisableBasicAuthentication] = osDisableBasicAuthentication == "true" envData[constants.EnvKeyDisableBasicAuthentication] = osDisableBasicAuthentication == "true"
} }
@@ -490,10 +544,49 @@ func InitAllEnv() error {
} }
} }
if _, ok := envData[constants.EnvKeyEnforceMultiFactorAuthentication]; !ok {
envData[constants.EnvKeyEnforceMultiFactorAuthentication] = osEnforceMultiFactorAuthentication == "true"
}
if osEnforceMultiFactorAuthentication != "" {
boolValue, err := strconv.ParseBool(osEnforceMultiFactorAuthentication)
if err != nil {
return err
}
if boolValue != envData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) {
envData[constants.EnvKeyEnforceMultiFactorAuthentication] = boolValue
}
}
if _, ok := envData[constants.EnvKeyDisableMultiFactorAuthentication]; !ok {
envData[constants.EnvKeyDisableMultiFactorAuthentication] = osDisableMultiFactorAuthentication == "true"
}
if osDisableMultiFactorAuthentication != "" {
boolValue, err := strconv.ParseBool(osDisableMultiFactorAuthentication)
if err != nil {
return err
}
if boolValue != envData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) {
envData[constants.EnvKeyDisableMultiFactorAuthentication] = boolValue
}
}
// no need to add nil check as its already done above // no need to add nil check as its already done above
if envData[constants.EnvKeySmtpHost] == "" || envData[constants.EnvKeySmtpUsername] == "" || envData[constants.EnvKeySmtpPassword] == "" || envData[constants.EnvKeySenderEmail] == "" && envData[constants.EnvKeySmtpPort] == "" { if envData[constants.EnvKeySmtpHost] == "" || envData[constants.EnvKeySmtpUsername] == "" || envData[constants.EnvKeySmtpPassword] == "" || envData[constants.EnvKeySenderEmail] == "" && envData[constants.EnvKeySmtpPort] == "" {
envData[constants.EnvKeyDisableEmailVerification] = true envData[constants.EnvKeyDisableEmailVerification] = true
envData[constants.EnvKeyDisableMagicLinkLogin] = true envData[constants.EnvKeyDisableMagicLinkLogin] = true
envData[constants.EnvKeyIsEmailServiceEnabled] = false
}
if envData[constants.EnvKeySmtpHost] != "" || envData[constants.EnvKeySmtpUsername] != "" || envData[constants.EnvKeySmtpPassword] != "" || envData[constants.EnvKeySenderEmail] != "" && envData[constants.EnvKeySmtpPort] != "" {
envData[constants.EnvKeyIsEmailServiceEnabled] = true
}
if envData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && !envData[constants.EnvKeyIsEmailServiceEnabled].(bool) {
return errors.New("to enable multi factor authentication, please enable email service")
}
if !envData[constants.EnvKeyIsEmailServiceEnabled].(bool) {
envData[constants.EnvKeyDisableMultiFactorAuthentication] = true
} }
if envData[constants.EnvKeyDisableEmailVerification].(bool) { if envData[constants.EnvKeyDisableEmailVerification].(bool) {

View File

@@ -201,7 +201,7 @@ func PersistEnv() error {
envValue := strings.TrimSpace(os.Getenv(key)) envValue := strings.TrimSpace(os.Getenv(key))
if envValue != "" { if envValue != "" {
switch key { switch key {
case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword: case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication, constants.EnvKeyDisableMultiFactorAuthentication:
if envValueBool, err := strconv.ParseBool(envValue); err == nil { if envValueBool, err := strconv.ParseBool(envValue); err == nil {
if value.(bool) != envValueBool { if value.(bool) != envValueBool {
storeData[key] = envValueBool storeData[key] = envValueBool
@@ -221,6 +221,8 @@ func PersistEnv() error {
// handle derivative cases like disabling email verification & magic login // handle derivative cases like disabling email verification & magic login
// in case SMTP is off but env is set to true // in case SMTP is off but env is set to true
if storeData[constants.EnvKeySmtpHost] == "" || storeData[constants.EnvKeySmtpUsername] == "" || storeData[constants.EnvKeySmtpPassword] == "" || storeData[constants.EnvKeySenderEmail] == "" && storeData[constants.EnvKeySmtpPort] == "" { if storeData[constants.EnvKeySmtpHost] == "" || storeData[constants.EnvKeySmtpUsername] == "" || storeData[constants.EnvKeySmtpPassword] == "" || storeData[constants.EnvKeySenderEmail] == "" && storeData[constants.EnvKeySmtpPort] == "" {
storeData[constants.EnvKeyIsEmailServiceEnabled] = false
if !storeData[constants.EnvKeyDisableEmailVerification].(bool) { if !storeData[constants.EnvKeyDisableEmailVerification].(bool) {
storeData[constants.EnvKeyDisableEmailVerification] = true storeData[constants.EnvKeyDisableEmailVerification] = true
hasChanged = true hasChanged = true

View File

@@ -9,7 +9,7 @@ require (
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
github.com/gocql/gocql v1.0.0 github.com/gocql/gocql v1.2.0
github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0

View File

@@ -110,8 +110,8 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gocql/gocql v1.0.0 h1:UnbTERpP72VZ/viKE1Q1gPtmLvyTZTvuAstvSRydw/c= github.com/gocql/gocql v1.2.0 h1:TZhsCd7fRuye4VyHr3WCvWwIQaZUmjsqnSIXK9FcVCE=
github.com/gocql/gocql v1.0.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= github.com/gocql/gocql v1.2.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,9 @@ package model
type AddEmailTemplateRequest struct { type AddEmailTemplateRequest struct {
EventName string `json:"event_name"` EventName string `json:"event_name"`
Subject string `json:"subject"`
Template string `json:"template"` Template string `json:"template"`
Design string `json:"design"`
} }
type AddWebhookRequest struct { type AddWebhookRequest struct {
@@ -23,12 +25,13 @@ type AdminSignupInput struct {
} }
type AuthResponse struct { type AuthResponse struct {
Message string `json:"message"` Message string `json:"message"`
AccessToken *string `json:"access_token"` ShouldShowOtpScreen *bool `json:"should_show_otp_screen"`
IDToken *string `json:"id_token"` AccessToken *string `json:"access_token"`
RefreshToken *string `json:"refresh_token"` IDToken *string `json:"id_token"`
ExpiresIn *int64 `json:"expires_in"` RefreshToken *string `json:"refresh_token"`
User *User `json:"user"` ExpiresIn *int64 `json:"expires_in"`
User *User `json:"user"`
} }
type DeleteEmailTemplateRequest struct { type DeleteEmailTemplateRequest struct {
@@ -43,64 +46,70 @@ type EmailTemplate struct {
ID string `json:"id"` ID string `json:"id"`
EventName string `json:"event_name"` EventName string `json:"event_name"`
Template string `json:"template"` Template string `json:"template"`
Design string `json:"design"`
Subject string `json:"subject"`
CreatedAt *int64 `json:"created_at"` CreatedAt *int64 `json:"created_at"`
UpdatedAt *int64 `json:"updated_at"` UpdatedAt *int64 `json:"updated_at"`
} }
type EmailTemplates struct { type EmailTemplates struct {
Pagination *Pagination `json:"pagination"` Pagination *Pagination `json:"pagination"`
EmailTemplates []*EmailTemplate `json:"EmailTemplates"` EmailTemplates []*EmailTemplate `json:"email_templates"`
} }
type Env struct { type Env struct {
AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"` AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"`
AdminSecret *string `json:"ADMIN_SECRET"` AdminSecret *string `json:"ADMIN_SECRET"`
DatabaseName *string `json:"DATABASE_NAME"` DatabaseName *string `json:"DATABASE_NAME"`
DatabaseURL *string `json:"DATABASE_URL"` DatabaseURL *string `json:"DATABASE_URL"`
DatabaseType *string `json:"DATABASE_TYPE"` DatabaseType *string `json:"DATABASE_TYPE"`
DatabaseUsername *string `json:"DATABASE_USERNAME"` DatabaseUsername *string `json:"DATABASE_USERNAME"`
DatabasePassword *string `json:"DATABASE_PASSWORD"` DatabasePassword *string `json:"DATABASE_PASSWORD"`
DatabaseHost *string `json:"DATABASE_HOST"` DatabaseHost *string `json:"DATABASE_HOST"`
DatabasePort *string `json:"DATABASE_PORT"` DatabasePort *string `json:"DATABASE_PORT"`
ClientID string `json:"CLIENT_ID"` ClientID string `json:"CLIENT_ID"`
ClientSecret string `json:"CLIENT_SECRET"` ClientSecret string `json:"CLIENT_SECRET"`
CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"` 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"`
SMTPPassword *string `json:"SMTP_PASSWORD"` SMTPPassword *string `json:"SMTP_PASSWORD"`
SenderEmail *string `json:"SENDER_EMAIL"` SenderEmail *string `json:"SENDER_EMAIL"`
JwtType *string `json:"JWT_TYPE"` JwtType *string `json:"JWT_TYPE"`
JwtSecret *string `json:"JWT_SECRET"` JwtSecret *string `json:"JWT_SECRET"`
JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"` JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"`
JwtPublicKey *string `json:"JWT_PUBLIC_KEY"` JwtPublicKey *string `json:"JWT_PUBLIC_KEY"`
AllowedOrigins []string `json:"ALLOWED_ORIGINS"` AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
AppURL *string `json:"APP_URL"` AppURL *string `json:"APP_URL"`
RedisURL *string `json:"REDIS_URL"` RedisURL *string `json:"REDIS_URL"`
ResetPasswordURL *string `json:"RESET_PASSWORD_URL"` ResetPasswordURL *string `json:"RESET_PASSWORD_URL"`
DisableEmailVerification bool `json:"DISABLE_EMAIL_VERIFICATION"` DisableEmailVerification bool `json:"DISABLE_EMAIL_VERIFICATION"`
DisableBasicAuthentication bool `json:"DISABLE_BASIC_AUTHENTICATION"` DisableBasicAuthentication bool `json:"DISABLE_BASIC_AUTHENTICATION"`
DisableMagicLinkLogin bool `json:"DISABLE_MAGIC_LINK_LOGIN"` DisableMagicLinkLogin bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
DisableLoginPage bool `json:"DISABLE_LOGIN_PAGE"` DisableLoginPage bool `json:"DISABLE_LOGIN_PAGE"`
DisableSignUp bool `json:"DISABLE_SIGN_UP"` DisableSignUp bool `json:"DISABLE_SIGN_UP"`
DisableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"` DisableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"`
DisableStrongPassword bool `json:"DISABLE_STRONG_PASSWORD"` DisableStrongPassword bool `json:"DISABLE_STRONG_PASSWORD"`
Roles []string `json:"ROLES"` DisableMultiFactorAuthentication bool `json:"DISABLE_MULTI_FACTOR_AUTHENTICATION"`
ProtectedRoles []string `json:"PROTECTED_ROLES"` EnforceMultiFactorAuthentication bool `json:"ENFORCE_MULTI_FACTOR_AUTHENTICATION"`
DefaultRoles []string `json:"DEFAULT_ROLES"` Roles []string `json:"ROLES"`
JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"` ProtectedRoles []string `json:"PROTECTED_ROLES"`
GoogleClientID *string `json:"GOOGLE_CLIENT_ID"` DefaultRoles []string `json:"DEFAULT_ROLES"`
GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"` JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"`
GithubClientID *string `json:"GITHUB_CLIENT_ID"` GoogleClientID *string `json:"GOOGLE_CLIENT_ID"`
GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"` GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"`
FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"` GithubClientID *string `json:"GITHUB_CLIENT_ID"`
FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"`
LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"`
LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"`
AppleClientID *string `json:"APPLE_CLIENT_ID"` LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"`
AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"` LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"` AppleClientID *string `json:"APPLE_CLIENT_ID"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"` AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"`
TwitterClientID *string `json:"TWITTER_CLIENT_ID"`
TwitterClientSecret *string `json:"TWITTER_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
} }
type Error struct { type Error struct {
@@ -157,11 +166,13 @@ type Meta struct {
IsGithubLoginEnabled bool `json:"is_github_login_enabled"` IsGithubLoginEnabled bool `json:"is_github_login_enabled"`
IsLinkedinLoginEnabled bool `json:"is_linkedin_login_enabled"` IsLinkedinLoginEnabled bool `json:"is_linkedin_login_enabled"`
IsAppleLoginEnabled bool `json:"is_apple_login_enabled"` IsAppleLoginEnabled bool `json:"is_apple_login_enabled"`
IsTwitterLoginEnabled bool `json:"is_twitter_login_enabled"`
IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"` IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"`
IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"` IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"`
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
IsSignUpEnabled bool `json:"is_sign_up_enabled"` IsSignUpEnabled bool `json:"is_sign_up_enabled"`
IsStrongPasswordEnabled bool `json:"is_strong_password_enabled"` IsStrongPasswordEnabled bool `json:"is_strong_password_enabled"`
IsMultiFactorAuthEnabled bool `json:"is_multi_factor_auth_enabled"`
} }
type OAuthRevokeInput struct { type OAuthRevokeInput struct {
@@ -173,15 +184,21 @@ type PaginatedInput struct {
} }
type Pagination struct { type Pagination struct {
Limit int64 `json:"limit"` Limit int64 `json:"limit"`
Page int64 `json:"page"` Page int64 `json:"page"`
Offset int64 `json:"offset"` Cursor *string `json:"cursor"`
Total int64 `json:"total"` Offset int64 `json:"offset"`
Total int64 `json:"total"`
} }
type PaginationInput struct { type PaginationInput struct {
Limit *int64 `json:"limit"` Limit *int64 `json:"limit"`
Page *int64 `json:"page"` Page *int64 `json:"page"`
Cursor *string `json:"cursor"`
}
type ResendOTPRequest struct {
Email string `json:"email"`
} }
type ResendVerifyEmailInput struct { type ResendVerifyEmailInput struct {
@@ -205,20 +222,21 @@ type SessionQueryInput struct {
} }
type SignUpInput struct { type SignUpInput struct {
Email string `json:"email"` Email string `json:"email"`
GivenName *string `json:"given_name"` GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"` FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"` MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"` Nickname *string `json:"nickname"`
Gender *string `json:"gender"` Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"` Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"` PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"` Picture *string `json:"picture"`
Password string `json:"password"` Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"` ConfirmPassword string `json:"confirm_password"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
Scope []string `json:"scope"` Scope []string `json:"scope"`
RedirectURI *string `json:"redirect_uri"` RedirectURI *string `json:"redirect_uri"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
} }
type TestEndpointRequest struct { type TestEndpointRequest struct {
@@ -240,78 +258,86 @@ type UpdateEmailTemplateRequest struct {
ID string `json:"id"` ID string `json:"id"`
EventName *string `json:"event_name"` EventName *string `json:"event_name"`
Template *string `json:"template"` Template *string `json:"template"`
Subject *string `json:"subject"`
Design *string `json:"design"`
} }
type UpdateEnvInput struct { type UpdateEnvInput struct {
AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"` AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"`
AdminSecret *string `json:"ADMIN_SECRET"` AdminSecret *string `json:"ADMIN_SECRET"`
CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"` CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"`
OldAdminSecret *string `json:"OLD_ADMIN_SECRET"` OldAdminSecret *string `json:"OLD_ADMIN_SECRET"`
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"`
SMTPPassword *string `json:"SMTP_PASSWORD"` SMTPPassword *string `json:"SMTP_PASSWORD"`
SenderEmail *string `json:"SENDER_EMAIL"` SenderEmail *string `json:"SENDER_EMAIL"`
JwtType *string `json:"JWT_TYPE"` JwtType *string `json:"JWT_TYPE"`
JwtSecret *string `json:"JWT_SECRET"` JwtSecret *string `json:"JWT_SECRET"`
JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"` JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"`
JwtPublicKey *string `json:"JWT_PUBLIC_KEY"` JwtPublicKey *string `json:"JWT_PUBLIC_KEY"`
AllowedOrigins []string `json:"ALLOWED_ORIGINS"` AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
AppURL *string `json:"APP_URL"` AppURL *string `json:"APP_URL"`
ResetPasswordURL *string `json:"RESET_PASSWORD_URL"` ResetPasswordURL *string `json:"RESET_PASSWORD_URL"`
DisableEmailVerification *bool `json:"DISABLE_EMAIL_VERIFICATION"` DisableEmailVerification *bool `json:"DISABLE_EMAIL_VERIFICATION"`
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"` DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"` DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"` DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
DisableSignUp *bool `json:"DISABLE_SIGN_UP"` DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
DisableRedisForEnv *bool `json:"DISABLE_REDIS_FOR_ENV"` DisableRedisForEnv *bool `json:"DISABLE_REDIS_FOR_ENV"`
DisableStrongPassword *bool `json:"DISABLE_STRONG_PASSWORD"` DisableStrongPassword *bool `json:"DISABLE_STRONG_PASSWORD"`
Roles []string `json:"ROLES"` DisableMultiFactorAuthentication *bool `json:"DISABLE_MULTI_FACTOR_AUTHENTICATION"`
ProtectedRoles []string `json:"PROTECTED_ROLES"` EnforceMultiFactorAuthentication *bool `json:"ENFORCE_MULTI_FACTOR_AUTHENTICATION"`
DefaultRoles []string `json:"DEFAULT_ROLES"` Roles []string `json:"ROLES"`
JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"` ProtectedRoles []string `json:"PROTECTED_ROLES"`
GoogleClientID *string `json:"GOOGLE_CLIENT_ID"` DefaultRoles []string `json:"DEFAULT_ROLES"`
GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"` JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"`
GithubClientID *string `json:"GITHUB_CLIENT_ID"` GoogleClientID *string `json:"GOOGLE_CLIENT_ID"`
GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"` GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"`
FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"` GithubClientID *string `json:"GITHUB_CLIENT_ID"`
FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"`
LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"`
LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"`
AppleClientID *string `json:"APPLE_CLIENT_ID"` LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"`
AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"` LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"` AppleClientID *string `json:"APPLE_CLIENT_ID"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"` AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"`
TwitterClientID *string `json:"TWITTER_CLIENT_ID"`
TwitterClientSecret *string `json:"TWITTER_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
} }
type UpdateProfileInput struct { type UpdateProfileInput struct {
OldPassword *string `json:"old_password"` OldPassword *string `json:"old_password"`
NewPassword *string `json:"new_password"` NewPassword *string `json:"new_password"`
ConfirmNewPassword *string `json:"confirm_new_password"` ConfirmNewPassword *string `json:"confirm_new_password"`
Email *string `json:"email"` Email *string `json:"email"`
GivenName *string `json:"given_name"` GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"` FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"` MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"` Nickname *string `json:"nickname"`
Gender *string `json:"gender"` Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"` Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"` PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"` Picture *string `json:"picture"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
} }
type UpdateUserInput struct { type UpdateUserInput struct {
ID string `json:"id"` ID string `json:"id"`
Email *string `json:"email"` Email *string `json:"email"`
EmailVerified *bool `json:"email_verified"` EmailVerified *bool `json:"email_verified"`
GivenName *string `json:"given_name"` GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"` FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"` MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"` Nickname *string `json:"nickname"`
Gender *string `json:"gender"` Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"` Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"` PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"` Picture *string `json:"picture"`
Roles []*string `json:"roles"` Roles []*string `json:"roles"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
} }
type UpdateWebhookRequest struct { type UpdateWebhookRequest struct {
@@ -323,24 +349,25 @@ type UpdateWebhookRequest struct {
} }
type User struct { type User struct {
ID string `json:"id"` ID string `json:"id"`
Email string `json:"email"` Email string `json:"email"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
SignupMethods string `json:"signup_methods"` SignupMethods string `json:"signup_methods"`
GivenName *string `json:"given_name"` GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"` FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"` MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"` Nickname *string `json:"nickname"`
PreferredUsername *string `json:"preferred_username"` PreferredUsername *string `json:"preferred_username"`
Gender *string `json:"gender"` Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"` Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"` PhoneNumber *string `json:"phone_number"`
PhoneNumberVerified *bool `json:"phone_number_verified"` PhoneNumberVerified *bool `json:"phone_number_verified"`
Picture *string `json:"picture"` Picture *string `json:"picture"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
CreatedAt *int64 `json:"created_at"` CreatedAt *int64 `json:"created_at"`
UpdatedAt *int64 `json:"updated_at"` UpdatedAt *int64 `json:"updated_at"`
RevokedTimestamp *int64 `json:"revoked_timestamp"` RevokedTimestamp *int64 `json:"revoked_timestamp"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
} }
type Users struct { type Users struct {
@@ -379,6 +406,11 @@ type VerifyEmailInput struct {
Token string `json:"token"` Token string `json:"token"`
} }
type VerifyOTPRequest struct {
Email string `json:"email"`
Otp string `json:"otp"`
}
type Webhook struct { type Webhook struct {
ID string `json:"id"` ID string `json:"id"`
EventName *string `json:"event_name"` EventName *string `json:"event_name"`

View File

@@ -6,460 +6,494 @@ scalar Map
scalar Any scalar Any
type Pagination { type Pagination {
limit: Int64! limit: Int64!
page: Int64! page: Int64!
offset: Int64! cursor: String
total: Int64! offset: Int64!
total: Int64!
} }
type Meta { type Meta {
version: String! version: String!
client_id: String! client_id: String!
is_google_login_enabled: Boolean! is_google_login_enabled: Boolean!
is_facebook_login_enabled: Boolean! is_facebook_login_enabled: Boolean!
is_github_login_enabled: Boolean! is_github_login_enabled: Boolean!
is_linkedin_login_enabled: Boolean! is_linkedin_login_enabled: Boolean!
is_apple_login_enabled: Boolean! is_apple_login_enabled: Boolean!
is_email_verification_enabled: Boolean! is_twitter_login_enabled: Boolean!
is_basic_authentication_enabled: Boolean! is_email_verification_enabled: Boolean!
is_magic_link_login_enabled: Boolean! is_basic_authentication_enabled: Boolean!
is_sign_up_enabled: Boolean! is_magic_link_login_enabled: Boolean!
is_strong_password_enabled: Boolean! is_sign_up_enabled: Boolean!
is_strong_password_enabled: Boolean!
is_multi_factor_auth_enabled: Boolean!
} }
type User { type User {
id: ID! id: ID!
email: String! email: String!
email_verified: Boolean! email_verified: Boolean!
signup_methods: String! signup_methods: String!
given_name: String given_name: String
family_name: String family_name: String
middle_name: String middle_name: String
nickname: String nickname: String
# defaults to email # defaults to email
preferred_username: String preferred_username: String
gender: String gender: String
birthdate: String birthdate: String
phone_number: String phone_number: String
phone_number_verified: Boolean phone_number_verified: Boolean
picture: String picture: String
roles: [String!]! roles: [String!]!
created_at: Int64 created_at: Int64
updated_at: Int64 updated_at: Int64
revoked_timestamp: Int64 revoked_timestamp: Int64
is_multi_factor_auth_enabled: Boolean
} }
type Users { type Users {
pagination: Pagination! pagination: Pagination!
users: [User!]! users: [User!]!
} }
type VerificationRequest { type VerificationRequest {
id: ID! id: ID!
identifier: String identifier: String
token: String token: String
email: String email: String
expires: Int64 expires: Int64
created_at: Int64 created_at: Int64
updated_at: Int64 updated_at: Int64
nonce: String nonce: String
redirect_uri: String redirect_uri: String
} }
type VerificationRequests { type VerificationRequests {
pagination: Pagination! pagination: Pagination!
verification_requests: [VerificationRequest!]! verification_requests: [VerificationRequest!]!
} }
type Error { type Error {
message: String! message: String!
reason: String! reason: String!
} }
type AuthResponse { type AuthResponse {
message: String! message: String!
access_token: String should_show_otp_screen: Boolean
id_token: String access_token: String
refresh_token: String id_token: String
expires_in: Int64 refresh_token: String
user: User expires_in: Int64
user: User
} }
type Response { type Response {
message: String! message: String!
} }
type Env { type Env {
ACCESS_TOKEN_EXPIRY_TIME: String ACCESS_TOKEN_EXPIRY_TIME: String
ADMIN_SECRET: String ADMIN_SECRET: String
DATABASE_NAME: String DATABASE_NAME: String
DATABASE_URL: String DATABASE_URL: String
DATABASE_TYPE: String DATABASE_TYPE: String
DATABASE_USERNAME: String DATABASE_USERNAME: String
DATABASE_PASSWORD: String DATABASE_PASSWORD: String
DATABASE_HOST: String DATABASE_HOST: String
DATABASE_PORT: String DATABASE_PORT: String
CLIENT_ID: String! CLIENT_ID: String!
CLIENT_SECRET: String! CLIENT_SECRET: String!
CUSTOM_ACCESS_TOKEN_SCRIPT: String CUSTOM_ACCESS_TOKEN_SCRIPT: String
SMTP_HOST: String SMTP_HOST: String
SMTP_PORT: String SMTP_PORT: String
SMTP_USERNAME: String SMTP_USERNAME: String
SMTP_PASSWORD: String SMTP_PASSWORD: String
SENDER_EMAIL: String SENDER_EMAIL: String
JWT_TYPE: String JWT_TYPE: String
JWT_SECRET: String JWT_SECRET: String
JWT_PRIVATE_KEY: String JWT_PRIVATE_KEY: String
JWT_PUBLIC_KEY: String JWT_PUBLIC_KEY: String
ALLOWED_ORIGINS: [String!] ALLOWED_ORIGINS: [String!]
APP_URL: String APP_URL: String
REDIS_URL: String REDIS_URL: String
RESET_PASSWORD_URL: String RESET_PASSWORD_URL: String
DISABLE_EMAIL_VERIFICATION: Boolean! DISABLE_EMAIL_VERIFICATION: Boolean!
DISABLE_BASIC_AUTHENTICATION: Boolean! DISABLE_BASIC_AUTHENTICATION: Boolean!
DISABLE_MAGIC_LINK_LOGIN: Boolean! DISABLE_MAGIC_LINK_LOGIN: Boolean!
DISABLE_LOGIN_PAGE: Boolean! DISABLE_LOGIN_PAGE: Boolean!
DISABLE_SIGN_UP: Boolean! DISABLE_SIGN_UP: Boolean!
DISABLE_REDIS_FOR_ENV: Boolean! DISABLE_REDIS_FOR_ENV: Boolean!
DISABLE_STRONG_PASSWORD: Boolean! DISABLE_STRONG_PASSWORD: Boolean!
ROLES: [String!] DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean!
PROTECTED_ROLES: [String!] ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean!
DEFAULT_ROLES: [String!] ROLES: [String!]
JWT_ROLE_CLAIM: String PROTECTED_ROLES: [String!]
GOOGLE_CLIENT_ID: String DEFAULT_ROLES: [String!]
GOOGLE_CLIENT_SECRET: String JWT_ROLE_CLAIM: String
GITHUB_CLIENT_ID: String GOOGLE_CLIENT_ID: String
GITHUB_CLIENT_SECRET: String GOOGLE_CLIENT_SECRET: String
FACEBOOK_CLIENT_ID: String GITHUB_CLIENT_ID: String
FACEBOOK_CLIENT_SECRET: String GITHUB_CLIENT_SECRET: String
LINKEDIN_CLIENT_ID: String FACEBOOK_CLIENT_ID: String
LINKEDIN_CLIENT_SECRET: String FACEBOOK_CLIENT_SECRET: String
APPLE_CLIENT_ID: String LINKEDIN_CLIENT_ID: String
APPLE_CLIENT_SECRET: String LINKEDIN_CLIENT_SECRET: String
ORGANIZATION_NAME: String APPLE_CLIENT_ID: String
ORGANIZATION_LOGO: String APPLE_CLIENT_SECRET: String
TWITTER_CLIENT_ID: String
TWITTER_CLIENT_SECRET: String
ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String
} }
type ValidateJWTTokenResponse { type ValidateJWTTokenResponse {
is_valid: Boolean! is_valid: Boolean!
} }
type GenerateJWTKeysResponse { type GenerateJWTKeysResponse {
secret: String secret: String
public_key: String public_key: String
private_key: String private_key: String
} }
type Webhook { type Webhook {
id: ID! id: ID!
event_name: String event_name: String
endpoint: String endpoint: String
enabled: Boolean enabled: Boolean
headers: Map headers: Map
created_at: Int64 created_at: Int64
updated_at: Int64 updated_at: Int64
} }
type Webhooks { type Webhooks {
pagination: Pagination! pagination: Pagination!
webhooks: [Webhook!]! webhooks: [Webhook!]!
} }
type WebhookLog { type WebhookLog {
id: ID! id: ID!
http_status: Int64 http_status: Int64
response: String response: String
request: String request: String
webhook_id: ID webhook_id: ID
created_at: Int64 created_at: Int64
updated_at: Int64 updated_at: Int64
} }
type TestEndpointResponse { type TestEndpointResponse {
http_status: Int64 http_status: Int64
response: String response: String
} }
type WebhookLogs { type WebhookLogs {
pagination: Pagination! pagination: Pagination!
webhook_logs: [WebhookLog!]! webhook_logs: [WebhookLog!]!
} }
type EmailTemplate { type EmailTemplate {
id: ID! id: ID!
event_name: String! event_name: String!
template: String! template: String!
created_at: Int64 design: String!
updated_at: Int64 subject: String!
created_at: Int64
updated_at: Int64
} }
type EmailTemplates { type EmailTemplates {
pagination: Pagination! pagination: Pagination!
EmailTemplates: [EmailTemplate!]! email_templates: [EmailTemplate!]!
} }
input UpdateEnvInput { input UpdateEnvInput {
ACCESS_TOKEN_EXPIRY_TIME: String ACCESS_TOKEN_EXPIRY_TIME: String
ADMIN_SECRET: String ADMIN_SECRET: String
CUSTOM_ACCESS_TOKEN_SCRIPT: String CUSTOM_ACCESS_TOKEN_SCRIPT: String
OLD_ADMIN_SECRET: String OLD_ADMIN_SECRET: String
SMTP_HOST: String SMTP_HOST: String
SMTP_PORT: String SMTP_PORT: String
SMTP_USERNAME: String SMTP_USERNAME: String
SMTP_PASSWORD: String SMTP_PASSWORD: String
SENDER_EMAIL: String SENDER_EMAIL: String
JWT_TYPE: String JWT_TYPE: String
JWT_SECRET: String JWT_SECRET: String
JWT_PRIVATE_KEY: String JWT_PRIVATE_KEY: String
JWT_PUBLIC_KEY: String JWT_PUBLIC_KEY: String
ALLOWED_ORIGINS: [String!] ALLOWED_ORIGINS: [String!]
APP_URL: String APP_URL: String
RESET_PASSWORD_URL: String RESET_PASSWORD_URL: String
DISABLE_EMAIL_VERIFICATION: Boolean DISABLE_EMAIL_VERIFICATION: Boolean
DISABLE_BASIC_AUTHENTICATION: Boolean DISABLE_BASIC_AUTHENTICATION: Boolean
DISABLE_MAGIC_LINK_LOGIN: Boolean DISABLE_MAGIC_LINK_LOGIN: Boolean
DISABLE_LOGIN_PAGE: Boolean DISABLE_LOGIN_PAGE: Boolean
DISABLE_SIGN_UP: Boolean DISABLE_SIGN_UP: Boolean
DISABLE_REDIS_FOR_ENV: Boolean DISABLE_REDIS_FOR_ENV: Boolean
DISABLE_STRONG_PASSWORD: Boolean DISABLE_STRONG_PASSWORD: Boolean
ROLES: [String!] DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean
PROTECTED_ROLES: [String!] ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean
DEFAULT_ROLES: [String!] ROLES: [String!]
JWT_ROLE_CLAIM: String PROTECTED_ROLES: [String!]
GOOGLE_CLIENT_ID: String DEFAULT_ROLES: [String!]
GOOGLE_CLIENT_SECRET: String JWT_ROLE_CLAIM: String
GITHUB_CLIENT_ID: String GOOGLE_CLIENT_ID: String
GITHUB_CLIENT_SECRET: String GOOGLE_CLIENT_SECRET: String
FACEBOOK_CLIENT_ID: String GITHUB_CLIENT_ID: String
FACEBOOK_CLIENT_SECRET: String GITHUB_CLIENT_SECRET: String
LINKEDIN_CLIENT_ID: String FACEBOOK_CLIENT_ID: String
LINKEDIN_CLIENT_SECRET: String FACEBOOK_CLIENT_SECRET: String
APPLE_CLIENT_ID: String LINKEDIN_CLIENT_ID: String
APPLE_CLIENT_SECRET: String LINKEDIN_CLIENT_SECRET: String
ORGANIZATION_NAME: String APPLE_CLIENT_ID: String
ORGANIZATION_LOGO: String APPLE_CLIENT_SECRET: String
TWITTER_CLIENT_ID: String
TWITTER_CLIENT_SECRET: String
ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String
} }
input AdminLoginInput { input AdminLoginInput {
admin_secret: String! admin_secret: String!
} }
input AdminSignupInput { input AdminSignupInput {
admin_secret: String! admin_secret: String!
} }
input SignUpInput { input SignUpInput {
email: String! email: String!
given_name: String given_name: String
family_name: String family_name: String
middle_name: String middle_name: String
nickname: String nickname: String
gender: String gender: String
birthdate: String birthdate: String
phone_number: String phone_number: String
picture: String picture: String
password: String! password: String!
confirm_password: String! confirm_password: String!
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
redirect_uri: String redirect_uri: String
is_multi_factor_auth_enabled: Boolean
} }
input LoginInput { input LoginInput {
email: String! email: String!
password: String! password: String!
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
} }
input VerifyEmailInput { input VerifyEmailInput {
token: String! token: String!
} }
input ResendVerifyEmailInput { input ResendVerifyEmailInput {
email: String! email: String!
identifier: String! identifier: String!
} }
input UpdateProfileInput { input UpdateProfileInput {
old_password: String old_password: String
new_password: String new_password: String
confirm_new_password: String confirm_new_password: String
email: String email: String
given_name: String given_name: String
family_name: String family_name: String
middle_name: String middle_name: String
nickname: String nickname: String
gender: String gender: String
birthdate: String birthdate: String
phone_number: String phone_number: String
picture: String picture: String
is_multi_factor_auth_enabled: Boolean
} }
input UpdateUserInput { input UpdateUserInput {
id: ID! id: ID!
email: String email: String
email_verified: Boolean email_verified: Boolean
given_name: String given_name: String
family_name: String family_name: String
middle_name: String middle_name: String
nickname: String nickname: String
gender: String gender: String
birthdate: String birthdate: String
phone_number: String phone_number: String
picture: String picture: String
roles: [String] roles: [String]
is_multi_factor_auth_enabled: Boolean
} }
input ForgotPasswordInput { input ForgotPasswordInput {
email: String! email: String!
state: String state: String
redirect_uri: String redirect_uri: String
} }
input ResetPasswordInput { input ResetPasswordInput {
token: String! token: String!
password: String! password: String!
confirm_password: String! confirm_password: String!
} }
input DeleteUserInput { input DeleteUserInput {
email: String! email: String!
} }
input MagicLinkLoginInput { input MagicLinkLoginInput {
email: String! email: String!
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
state: String state: String
redirect_uri: String redirect_uri: String
} }
input SessionQueryInput { input SessionQueryInput {
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
} }
input PaginationInput { input PaginationInput {
limit: Int64 limit: Int64
page: Int64 page: Int64
cursor: String
} }
input PaginatedInput { input PaginatedInput {
pagination: PaginationInput pagination: PaginationInput
} }
input OAuthRevokeInput { input OAuthRevokeInput {
refresh_token: String! refresh_token: String!
} }
input InviteMemberInput { input InviteMemberInput {
emails: [String!]! emails: [String!]!
redirect_uri: String redirect_uri: String
} }
input UpdateAccessInput { input UpdateAccessInput {
user_id: String! user_id: String!
} }
input ValidateJWTTokenInput { input ValidateJWTTokenInput {
token_type: String! token_type: String!
token: String! token: String!
roles: [String!] roles: [String!]
} }
input GenerateJWTKeysInput { input GenerateJWTKeysInput {
type: String! type: String!
} }
input ListWebhookLogRequest { input ListWebhookLogRequest {
pagination: PaginationInput pagination: PaginationInput
webhook_id: String webhook_id: String
} }
input AddWebhookRequest { input AddWebhookRequest {
event_name: String! event_name: String!
endpoint: String! endpoint: String!
enabled: Boolean! enabled: Boolean!
headers: Map headers: Map
} }
input UpdateWebhookRequest { input UpdateWebhookRequest {
id: ID! id: ID!
event_name: String event_name: String
endpoint: String endpoint: String
enabled: Boolean enabled: Boolean
headers: Map headers: Map
} }
input WebhookRequest { input WebhookRequest {
id: ID! id: ID!
} }
input TestEndpointRequest { input TestEndpointRequest {
endpoint: String! endpoint: String!
event_name: String! event_name: String!
headers: Map headers: Map
} }
input AddEmailTemplateRequest { input AddEmailTemplateRequest {
event_name: String! event_name: String!
template: String! subject: String!
template: String!
design: String!
} }
input UpdateEmailTemplateRequest { input UpdateEmailTemplateRequest {
id: ID! id: ID!
event_name: String event_name: String
template: String template: String
subject: String
design: String
} }
input DeleteEmailTemplateRequest { input DeleteEmailTemplateRequest {
id: ID! id: ID!
}
input VerifyOTPRequest {
email: String!
otp: String!
}
input ResendOTPRequest {
email: String!
} }
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
magic_link_login(params: MagicLinkLoginInput!): Response! magic_link_login(params: MagicLinkLoginInput!): Response!
logout: Response! logout: Response!
update_profile(params: UpdateProfileInput!): Response! update_profile(params: UpdateProfileInput!): Response!
verify_email(params: VerifyEmailInput!): AuthResponse! verify_email(params: VerifyEmailInput!): AuthResponse!
resend_verify_email(params: ResendVerifyEmailInput!): Response! resend_verify_email(params: ResendVerifyEmailInput!): Response!
forgot_password(params: ForgotPasswordInput!): Response! forgot_password(params: ForgotPasswordInput!): Response!
reset_password(params: ResetPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response!
revoke(params: OAuthRevokeInput!): Response! revoke(params: OAuthRevokeInput!): Response!
# admin only apis verify_otp(params: VerifyOTPRequest!): AuthResponse!
_delete_user(params: DeleteUserInput!): Response! resend_otp(params: ResendOTPRequest!): Response!
_update_user(params: UpdateUserInput!): User! # admin only apis
_admin_signup(params: AdminSignupInput!): Response! _delete_user(params: DeleteUserInput!): Response!
_admin_login(params: AdminLoginInput!): Response! _update_user(params: UpdateUserInput!): User!
_admin_logout: Response! _admin_signup(params: AdminSignupInput!): Response!
_update_env(params: UpdateEnvInput!): Response! _admin_login(params: AdminLoginInput!): Response!
_invite_members(params: InviteMemberInput!): Response! _admin_logout: Response!
_revoke_access(param: UpdateAccessInput!): Response! _update_env(params: UpdateEnvInput!): Response!
_enable_access(param: UpdateAccessInput!): Response! _invite_members(params: InviteMemberInput!): Response!
_generate_jwt_keys(params: GenerateJWTKeysInput!): GenerateJWTKeysResponse! _revoke_access(param: UpdateAccessInput!): Response!
_add_webhook(params: AddWebhookRequest!): Response! _enable_access(param: UpdateAccessInput!): Response!
_update_webhook(params: UpdateWebhookRequest!): Response! _generate_jwt_keys(params: GenerateJWTKeysInput!): GenerateJWTKeysResponse!
_delete_webhook(params: WebhookRequest!): Response! _add_webhook(params: AddWebhookRequest!): Response!
_test_endpoint(params: TestEndpointRequest!): TestEndpointResponse! _update_webhook(params: UpdateWebhookRequest!): Response!
_add_email_template(params: AddEmailTemplateRequest!): Response! _delete_webhook(params: WebhookRequest!): Response!
_update_email_template(params: UpdateEmailTemplateRequest!): Response! _test_endpoint(params: TestEndpointRequest!): TestEndpointResponse!
_delete_email_template(params: DeleteEmailTemplateRequest!): Response! _add_email_template(params: AddEmailTemplateRequest!): Response!
_update_email_template(params: UpdateEmailTemplateRequest!): Response!
_delete_email_template(params: DeleteEmailTemplateRequest!): Response!
} }
type Query { type Query {
meta: Meta! meta: Meta!
session(params: SessionQueryInput): AuthResponse! session(params: SessionQueryInput): AuthResponse!
profile: User! profile: User!
validate_jwt_token(params: ValidateJWTTokenInput!): ValidateJWTTokenResponse! validate_jwt_token(params: ValidateJWTTokenInput!): ValidateJWTTokenResponse!
# admin only apis # admin only apis
_users(params: PaginatedInput): Users! _users(params: PaginatedInput): Users!
_verification_requests(params: PaginatedInput): VerificationRequests! _verification_requests(params: PaginatedInput): VerificationRequests!
_admin_session: Response! _admin_session: Response!
_env: Env! _env: Env!
_webhook(params: WebhookRequest!): Webhook! _webhook(params: WebhookRequest!): Webhook!
_webhooks(params: PaginatedInput): Webhooks! _webhooks(params: PaginatedInput): Webhooks!
_webhook_logs(params: ListWebhookLogRequest): WebhookLogs! _webhook_logs(params: ListWebhookLogRequest): WebhookLogs!
_email_templates(params: PaginatedInput): EmailTemplates! _email_templates(params: PaginatedInput): EmailTemplates!
} }

View File

@@ -51,6 +51,14 @@ func (r *mutationResolver) Revoke(ctx context.Context, params model.OAuthRevokeI
return resolvers.RevokeResolver(ctx, params) return resolvers.RevokeResolver(ctx, params)
} }
func (r *mutationResolver) VerifyOtp(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) {
return resolvers.VerifyOtpResolver(ctx, params)
}
func (r *mutationResolver) ResendOtp(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) {
return resolvers.ResendOTPResolver(ctx, params)
}
func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) { func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) {
return resolvers.DeleteUserResolver(ctx, params) return resolvers.DeleteUserResolver(ctx, params)
} }

View File

@@ -248,7 +248,7 @@ func AuthorizeHandler() gin.HandlerFunc {
return return
} }
memorystore.Provider.SetUserSession(user.ID, constants.TokenTypeSessionToken+"_"+newSessionTokenData.Nonce, newSessionToken) memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+newSessionTokenData.Nonce, newSessionToken)
cookie.SetSession(gc, newSessionToken) cookie.SetSession(gc, newSessionToken)
code := uuid.New().String() code := uuid.New().String()
memorystore.Provider.SetState(codeChallenge, code+"@"+newSessionToken) memorystore.Provider.SetState(codeChallenge, code+"@"+newSessionToken)

View File

@@ -67,6 +67,8 @@ func OAuthCallbackHandler() gin.HandlerFunc {
user, err = processLinkedInUserInfo(code) user, err = processLinkedInUserInfo(code)
case constants.AuthRecipeMethodApple: case constants.AuthRecipeMethodApple:
user, err = processAppleUserInfo(code) user, err = processAppleUserInfo(code)
case constants.AuthRecipeMethodTwitter:
user, err = processTwitterUserInfo(code, sessionState)
default: default:
log.Info("Invalid oauth provider") log.Info("Invalid oauth provider")
err = fmt.Errorf(`invalid oauth provider`) err = fmt.Errorf(`invalid oauth provider`)
@@ -285,9 +287,9 @@ func processGithubUserInfo(code string) (models.User, error) {
log.Debug("Failed to create github user info request: ", err) log.Debug("Failed to create github user info request: ", err)
return user, fmt.Errorf("error creating github user info request: %s", err.Error()) return user, fmt.Errorf("error creating github user info request: %s", err.Error())
} }
req.Header = http.Header{ req.Header.Set(
"Authorization": []string{fmt.Sprintf("token %s", oauth2Token.AccessToken)}, "Authorization", fmt.Sprintf("token %s", oauth2Token.AccessToken),
} )
response, err := client.Do(req) response, err := client.Do(req)
if err != nil { if err != nil {
@@ -329,14 +331,14 @@ func processGithubUserInfo(code string) (models.User, error) {
} }
// fetch using /users/email endpoint // fetch using /users/email endpoint
req, err := http.NewRequest("GET", constants.GithubUserEmails, nil) req, err := http.NewRequest(http.MethodGet, constants.GithubUserEmails, nil)
if err != nil { if err != nil {
log.Debug("Failed to create github emails request: ", err) log.Debug("Failed to create github emails request: ", err)
return user, fmt.Errorf("error creating github user info request: %s", err.Error()) return user, fmt.Errorf("error creating github user info request: %s", err.Error())
} }
req.Header = http.Header{ req.Header.Set(
"Authorization": []string{fmt.Sprintf("token %s", oauth2Token.AccessToken)}, "Authorization", fmt.Sprintf("token %s", oauth2Token.AccessToken),
} )
response, err := client.Do(req) response, err := client.Do(req)
if err != nil { if err != nil {
@@ -564,3 +566,70 @@ func processAppleUserInfo(code string) (models.User, error) {
return user, err return user, err
} }
func processTwitterUserInfo(code, verifier string) (models.User, error) {
user := models.User{}
oauth2Token, err := oauth.OAuthProviders.TwitterConfig.Exchange(oauth2.NoContext, code, oauth2.SetAuthURLParam("code_verifier", verifier))
if err != nil {
log.Debug("Failed to exchange code for token: ", err)
return user, fmt.Errorf("invalid twitter exchange code: %s", err.Error())
}
client := http.Client{}
req, err := http.NewRequest("GET", constants.TwitterUserInfoURL, nil)
if err != nil {
log.Debug("Failed to create Twitter user info request: ", err)
return user, fmt.Errorf("error creating Twitter user info request: %s", err.Error())
}
req.Header = http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", oauth2Token.AccessToken)},
}
response, err := client.Do(req)
if err != nil {
log.Debug("Failed to request Twitter user info: ", err)
return user, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Debug("Failed to read Twitter user info response body: ", err)
return user, fmt.Errorf("failed to read Twitter response body: %s", err.Error())
}
if response.StatusCode >= 400 {
log.Debug("Failed to request Twitter user info: ", string(body))
return user, fmt.Errorf("failed to request Twitter user info: %s", string(body))
}
responseRawData := make(map[string]interface{})
json.Unmarshal(body, &responseRawData)
userRawData := responseRawData["data"].(map[string]interface{})
// log.Info(userRawData)
// Twitter API does not return E-Mail adresses by default. For that case special privileges have
// to be granted on a per-App basis. See https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials
// Currently Twitter API only provides the full name of a user. To fill givenName and familyName
// the full name will be split at the first whitespace. This approach will not be valid for all name combinations
nameArr := strings.SplitAfterN(userRawData["name"].(string), " ", 2)
firstName := nameArr[0]
lastName := ""
if len(nameArr) == 2 {
lastName = nameArr[1]
}
nickname := userRawData["username"].(string)
profilePicture := userRawData["profile_image_url"].(string)
user = models.User{
GivenName: &firstName,
FamilyName: &lastName,
Picture: &profilePicture,
Nickname: &nickname,
}
return user, nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/oauth" "github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
) )
@@ -95,7 +96,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
} }
oauthStateString := state + "___" + redirectURI + "___" + roles + "___" + strings.Join(scope, ",") oauthStateString := state + "___" + redirectURI + "___" + roles + "___" + strings.Join(scope, " ")
provider := c.Param("oauth_provider") provider := c.Param("oauth_provider")
isProviderConfigured := true isProviderConfigured := true
@@ -169,6 +170,26 @@ func OAuthLoginHandler() gin.HandlerFunc {
oauth.OAuthProviders.LinkedInConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodLinkedIn oauth.OAuthProviders.LinkedInConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodLinkedIn
url := oauth.OAuthProviders.LinkedInConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.LinkedInConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case constants.AuthRecipeMethodTwitter:
if oauth.OAuthProviders.TwitterConfig == nil {
log.Debug("Twitter OAuth provider is not configured")
isProviderConfigured = false
break
}
verifier, challenge := utils.GenerateCodeChallenge()
err := memorystore.Provider.SetState(oauthStateString, verifier)
if err != nil {
log.Debug("Error setting state: ", err)
c.JSON(500, gin.H{
"error": "internal server error",
})
return
}
oauth.OAuthProviders.TwitterConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodTwitter
url := oauth.OAuthProviders.TwitterConfig.AuthCodeURL(oauthStateString, oauth2.SetAuthURLParam("code_challenge", challenge), oauth2.SetAuthURLParam("code_challenge_method", "S256"))
c.Redirect(http.StatusTemporaryRedirect, url)
case constants.AuthRecipeMethodApple: case constants.AuthRecipeMethodApple:
if oauth.OAuthProviders.AppleConfig == nil { if oauth.OAuthProviders.AppleConfig == nil {
log.Debug("Apple OAuth provider is not configured") log.Debug("Apple OAuth provider is not configured")

View File

@@ -76,7 +76,6 @@ func TokenHandler() gin.HandlerFunc {
sessionKey := "" sessionKey := ""
if isAuthorizationCodeGrant { if isAuthorizationCodeGrant {
if codeVerifier == "" { if codeVerifier == "" {
log.Debug("Code verifier is empty") log.Debug("Code verifier is empty")
gc.JSON(http.StatusBadRequest, gin.H{ gc.JSON(http.StatusBadRequest, gin.H{
@@ -134,15 +133,18 @@ func TokenHandler() gin.HandlerFunc {
}) })
return return
} }
userID = claims.Subject userID = claims.Subject
roles = claims.Roles roles = claims.Roles
scope = claims.Scope scope = claims.Scope
loginMethod = claims.LoginMethod loginMethod = claims.LoginMethod
// rollover the session for security // rollover the session for security
sessionKey = userID sessionKey = userID
if loginMethod != "" { if loginMethod != "" {
sessionKey = loginMethod + ":" + userID sessionKey = loginMethod + ":" + userID
} }
go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce) go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce)
} else { } else {
// validate refresh token // validate refresh token

View File

@@ -109,6 +109,7 @@ func main() {
router := routes.InitRouter(log) router := routes.InitRouter(log)
log.Info("Starting Authorizer: ", VERSION) log.Info("Starting Authorizer: ", VERSION)
port, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyPort) port, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyPort)
log.Info("Authorizer running at PORT: ", port)
if err != nil { if err != nil {
log.Info("Error while getting port from env using default port 8080: ", err) log.Info("Error while getting port from env using default port 8080: ", err)
port = "8080" port = "8080"

View File

@@ -25,12 +25,15 @@ func InitMemStore() error {
constants.EnvKeyOrganizationLogo: "https://www.authorizer.dev/images/logo.png", constants.EnvKeyOrganizationLogo: "https://www.authorizer.dev/images/logo.png",
// boolean envs // boolean envs
constants.EnvKeyDisableBasicAuthentication: false, constants.EnvKeyDisableBasicAuthentication: false,
constants.EnvKeyDisableMagicLinkLogin: false, constants.EnvKeyDisableMagicLinkLogin: false,
constants.EnvKeyDisableEmailVerification: false, constants.EnvKeyDisableEmailVerification: false,
constants.EnvKeyDisableLoginPage: false, constants.EnvKeyDisableLoginPage: false,
constants.EnvKeyDisableSignUp: false, constants.EnvKeyDisableSignUp: false,
constants.EnvKeyDisableStrongPassword: false, constants.EnvKeyDisableStrongPassword: false,
constants.EnvKeyIsEmailServiceEnabled: false,
constants.EnvKeyEnforceMultiFactorAuthentication: false,
constants.EnvKeyDisableMultiFactorAuthentication: false,
} }
requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv() requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv()

View File

@@ -7,7 +7,7 @@ import (
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
) )
// SetUserSession sets the user session // SetUserSession sets the user session for given user identifier in form recipe:user_id
func (c *provider) SetUserSession(userId, key, token string) error { func (c *provider) SetUserSession(userId, key, token string) error {
c.sessionStore.Set(userId, key, token) c.sessionStore.Set(userId, key, token)
return nil return nil
@@ -34,6 +34,7 @@ func (c *provider) DeleteAllUserSessions(userId string) error {
constants.AuthRecipeMethodGithub, constants.AuthRecipeMethodGithub,
constants.AuthRecipeMethodGoogle, constants.AuthRecipeMethodGoogle,
constants.AuthRecipeMethodLinkedIn, constants.AuthRecipeMethodLinkedIn,
constants.AuthRecipeMethodTwitter,
} }
for _, namespace := range namespaces { for _, namespace := range namespaces {

View File

@@ -39,6 +39,7 @@ func (s *SessionStore) Set(key string, subKey, value string) {
func (s *SessionStore) RemoveAll(key string) { func (s *SessionStore) RemoveAll(key string) {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
delete(s.store, key) delete(s.store, key)
} }
@@ -53,6 +54,9 @@ func (s *SessionStore) Remove(key, subKey string) {
// Get all the values for given key // Get all the values for given key
func (s *SessionStore) GetAll(key string) map[string]string { func (s *SessionStore) GetAll(key string) map[string]string {
s.mutex.Lock()
defer s.mutex.Unlock()
if _, ok := s.store[key]; !ok { if _, ok := s.store[key]; !ok {
s.store[key] = make(map[string]string) s.store[key] = make(map[string]string)
} }
@@ -63,6 +67,7 @@ func (s *SessionStore) GetAll(key string) map[string]string {
func (s *SessionStore) RemoveByNamespace(namespace string) error { func (s *SessionStore) RemoveByNamespace(namespace string) error {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
for key := range s.store { for key := range s.store {
if strings.Contains(key, namespace+":") { if strings.Contains(key, namespace+":") {
delete(s.store, key) delete(s.store, key)

View File

@@ -2,7 +2,7 @@ package providers
// Provider defines current memory store provider // Provider defines current memory store provider
type Provider interface { type Provider interface {
// SetUserSession sets the user session // SetUserSession sets the user session for given user identifier in form recipe:user_id
SetUserSession(userId, key, token string) error SetUserSession(userId, key, token string) error
// GetAllUserSessions returns all the user sessions from the session store // GetAllUserSessions returns all the user sessions from the session store
GetAllUserSessions(userId string) (map[string]string, error) GetAllUserSessions(userId string) (map[string]string, error)

View File

@@ -14,7 +14,7 @@ var (
envStorePrefix = "authorizer_env" envStorePrefix = "authorizer_env"
) )
// SetUserSession sets the user session in redis store. // SetUserSession sets the user session for given user identifier in form recipe:user_id
func (c *provider) SetUserSession(userId, key, token string) error { func (c *provider) SetUserSession(userId, key, token string) error {
err := c.store.HSet(c.ctx, userId, key, token).Err() err := c.store.HSet(c.ctx, userId, key, token).Err()
if err != nil { if err != nil {
@@ -71,6 +71,7 @@ func (c *provider) DeleteAllUserSessions(userID string) error {
constants.AuthRecipeMethodGithub, constants.AuthRecipeMethodGithub,
constants.AuthRecipeMethodGoogle, constants.AuthRecipeMethodGoogle,
constants.AuthRecipeMethodLinkedIn, constants.AuthRecipeMethodLinkedIn,
constants.AuthRecipeMethodTwitter,
} }
for _, namespace := range namespaces { for _, namespace := range namespaces {
err := c.store.Del(c.ctx, namespace+":"+userID).Err() err := c.store.Del(c.ctx, namespace+":"+userID).Err()
@@ -160,7 +161,7 @@ func (c *provider) GetEnvStore() (map[string]interface{}, error) {
return nil, err return nil, err
} }
for key, value := range data { for key, value := range data {
if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword { if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled || key == constants.EnvKeyEnforceMultiFactorAuthentication || key == constants.EnvKeyDisableMultiFactorAuthentication {
boolValue, err := strconv.ParseBool(value) boolValue, err := strconv.ParseBool(value)
if err != nil { if err != nil {
return res, err return res, err

View File

@@ -20,6 +20,7 @@ type OAuthProvider struct {
FacebookConfig *oauth2.Config FacebookConfig *oauth2.Config
LinkedInConfig *oauth2.Config LinkedInConfig *oauth2.Config
AppleConfig *oauth2.Config AppleConfig *oauth2.Config
TwitterConfig *oauth2.Config
} }
// OIDCProviders is a struct that contains reference all the OpenID providers // OIDCProviders is a struct that contains reference all the OpenID providers
@@ -74,6 +75,7 @@ func InitOAuth() error {
ClientSecret: githubClientSecret, ClientSecret: githubClientSecret,
RedirectURL: "/oauth_callback/github", RedirectURL: "/oauth_callback/github",
Endpoint: githubOAuth2.Endpoint, Endpoint: githubOAuth2.Endpoint,
Scopes: []string{"read:user", "user:email"},
} }
} }
@@ -133,5 +135,28 @@ func InitOAuth() error {
} }
} }
twitterClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitterClientID)
if err != nil {
twitterClientID = ""
}
twitterClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitterClientSecret)
if err != nil {
twitterClientSecret = ""
}
if twitterClientID != "" && twitterClientSecret != "" {
OAuthProviders.TwitterConfig = &oauth2.Config{
ClientID: twitterClientID,
ClientSecret: twitterClientSecret,
RedirectURL: "/oauth_callback/twitter",
Endpoint: oauth2.Endpoint{
// Endpoint is currently not yet part of oauth2-package. See https://go-review.googlesource.com/c/oauth2/+/350889 for status
AuthURL: "https://twitter.com/i/oauth2/authorize",
TokenURL: "https://api.twitter.com/2/oauth2/token",
AuthStyle: oauth2.AuthStyleInHeader,
},
Scopes: []string{"tweet.read", "users.read"},
}
}
return nil return nil
} }

View File

@@ -14,8 +14,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// TODO add template validator
// AddEmailTemplateResolver resolver for add email template mutation // AddEmailTemplateResolver resolver for add email template mutation
func AddEmailTemplateResolver(ctx context.Context, params model.AddEmailTemplateRequest) (*model.Response, error) { func AddEmailTemplateResolver(ctx context.Context, params model.AddEmailTemplateRequest) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx) gc, err := utils.GinContextFromContext(ctx)
@@ -34,13 +32,23 @@ func AddEmailTemplateResolver(ctx context.Context, params model.AddEmailTemplate
return nil, fmt.Errorf("invalid event name %s", params.EventName) return nil, fmt.Errorf("invalid event name %s", params.EventName)
} }
if strings.TrimSpace(params.Subject) == "" {
return nil, fmt.Errorf("empty subject not allowed")
}
if strings.TrimSpace(params.Template) == "" { if strings.TrimSpace(params.Template) == "" {
return nil, fmt.Errorf("empty template not allowed") return nil, fmt.Errorf("empty template not allowed")
} }
if strings.TrimSpace(params.Design) == "" {
return nil, fmt.Errorf("empty design not allowed")
}
_, err = db.Provider.AddEmailTemplate(ctx, models.EmailTemplate{ _, err = db.Provider.AddEmailTemplate(ctx, models.EmailTemplate{
EventName: params.EventName, EventName: params.EventName,
Template: params.Template, Template: params.Template,
Subject: params.Subject,
Design: params.Design,
}) })
if err != nil { if err != nil {
log.Debug("Failed to add email template: ", err) log.Debug("Failed to add email template: ", err)

View File

@@ -143,6 +143,13 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
if val, ok := store[constants.EnvKeyAppleClientSecret]; ok { if val, ok := store[constants.EnvKeyAppleClientSecret]; ok {
res.AppleClientSecret = refs.NewStringRef(val.(string)) res.AppleClientSecret = refs.NewStringRef(val.(string))
} }
if val, ok := store[constants.EnvKeyTwitterClientID]; ok {
res.TwitterClientID = refs.NewStringRef(val.(string))
}
if val, ok := store[constants.EnvKeyTwitterClientSecret]; ok {
res.TwitterClientSecret = refs.NewStringRef(val.(string))
}
if val, ok := store[constants.EnvKeyOrganizationName]; ok { if val, ok := store[constants.EnvKeyOrganizationName]; ok {
res.OrganizationName = refs.NewStringRef(val.(string)) res.OrganizationName = refs.NewStringRef(val.(string))
} }
@@ -170,6 +177,8 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
res.DisableLoginPage = store[constants.EnvKeyDisableLoginPage].(bool) res.DisableLoginPage = store[constants.EnvKeyDisableLoginPage].(bool)
res.DisableSignUp = store[constants.EnvKeyDisableSignUp].(bool) res.DisableSignUp = store[constants.EnvKeyDisableSignUp].(bool)
res.DisableStrongPassword = store[constants.EnvKeyDisableStrongPassword].(bool) res.DisableStrongPassword = store[constants.EnvKeyDisableStrongPassword].(bool)
res.EnforceMultiFactorAuthentication = store[constants.EnvKeyEnforceMultiFactorAuthentication].(bool)
res.DisableMultiFactorAuthentication = store[constants.EnvKeyDisableMultiFactorAuthentication].(bool)
return res, nil return res, nil
} }

View File

@@ -15,6 +15,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@@ -49,7 +50,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
log := log.WithFields(log.Fields{ log := log.WithFields(log.Fields{
"email": params.Email, "email": params.Email,
}) })
_, err = db.Provider.GetUserByEmail(ctx, params.Email) user, err := db.Provider.GetUserByEmail(ctx, params.Email)
if err != nil { if err != nil {
log.Debug("User not found: ", err) log.Debug("User not found: ", err)
return res, fmt.Errorf(`user with this email not found`) return res, fmt.Errorf(`user with this email not found`)
@@ -61,9 +62,9 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
log.Debug("Failed to generate nonce: ", err) log.Debug("Failed to generate nonce: ", err)
return res, err return res, err
} }
redirectURL := parsers.GetAppURL(gc) + "/reset-password" redirectURL := parsers.GetAppURL(gc)
if params.RedirectURI != nil { if strings.TrimSpace(refs.StringValue(params.RedirectURI)) != "" {
redirectURL = *params.RedirectURI redirectURL = refs.StringValue(params.RedirectURI)
} }
verificationToken, err := token.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL) verificationToken, err := token.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL)
@@ -84,8 +85,12 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
return res, err return res, err
} }
// exec it as go routin so that we can reduce the api latency // exec it as go routine so that we can reduce the api latency
go email.SendForgotPasswordMail(params.Email, verificationToken, hostname) go email.SendEmail([]string{params.Email}, constants.VerificationTypeForgotPassword, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"verification_url": utils.GetForgotPasswordURL(verificationToken, hostname),
})
res = &model.Response{ res = &model.Response{
Message: `Please check your inbox! We have sent a password reset link.`, Message: `Please check your inbox! We have sent a password reset link.`,

View File

@@ -16,6 +16,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@@ -35,13 +36,13 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput)
} }
// this feature is only allowed if email server is configured // this feature is only allowed if email server is configured
isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) EnvKeyIsEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil { if err != nil {
log.Debug("Error getting email verification disabled: ", err) log.Debug("Error getting email verification disabled: ", err)
isEmailVerificationDisabled = true EnvKeyIsEmailServiceEnabled = false
} }
if isEmailVerificationDisabled { if !EnvKeyIsEmailServiceEnabled {
log.Debug("Email server is not configured") log.Debug("Email server is not configured")
return nil, errors.New("email sending is disabled") return nil, errors.New("email sending is disabled")
} }
@@ -114,7 +115,7 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput)
return nil, err return nil, err
} }
verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL) verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeInviteMember, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
log.Debug("Failed to create verification token: ", err) log.Debug("Failed to create verification token: ", err)
} }
@@ -134,8 +135,17 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput)
} else { } else {
// use basic authentication if that option is on // use basic authentication if that option is on
user.SignupMethods = constants.AuthRecipeMethodBasicAuth user.SignupMethods = constants.AuthRecipeMethodBasicAuth
verificationRequest.Identifier = constants.VerificationTypeForgotPassword verificationRequest.Identifier = constants.VerificationTypeInviteMember
isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication)
if err != nil {
log.Debug("MFA service not enabled: ", err)
isMFAEnforced = false
}
if isMFAEnforced {
user.IsMultiFactorAuthEnabled = refs.NewBoolRef(true)
}
verifyEmailURL = appURL + "/setup-password" verifyEmailURL = appURL + "/setup-password"
} }
@@ -152,7 +162,12 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput)
return nil, err return nil, err
} }
go emailservice.InviteEmail(email, verificationToken, verifyEmailURL, redirectURL) // exec it as go routine so that we can reduce the api latency
go emailservice.SendEmail([]string{user.Email}, constants.VerificationTypeInviteMember, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"verification_url": utils.GetInviteVerificationURL(verifyEmailURL, verificationToken, redirectURL),
})
} }
return &model.Response{ return &model.Response{

View File

@@ -13,8 +13,10 @@ import (
"github.com/authorizerdev/authorizer/server/cookie" "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/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@@ -48,7 +50,7 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
user, err := db.Provider.GetUserByEmail(ctx, params.Email) user, err := db.Provider.GetUserByEmail(ctx, params.Email)
if err != nil { if err != nil {
log.Debug("Failed to get user by email: ", err) log.Debug("Failed to get user by email: ", err)
return res, fmt.Errorf(`user with this email not found`) return res, fmt.Errorf(`bad user credentials`)
} }
if user.RevokedTimestamp != nil { if user.RevokedTimestamp != nil {
@@ -70,7 +72,7 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
if err != nil { if err != nil {
log.Debug("Failed to compare password: ", err) log.Debug("Failed to compare password: ", err)
return res, fmt.Errorf(`invalid password`) return res, fmt.Errorf(`bad user credentials`)
} }
defaultRolesString, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyDefaultRoles) defaultRolesString, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyDefaultRoles)
@@ -97,6 +99,47 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
scope = params.Scope scope = params.Scope
} }
isEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEmailServiceEnabled {
log.Debug("Email service not enabled: ", err)
}
isMFADisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMultiFactorAuthentication)
if err != nil || !isEmailServiceEnabled {
log.Debug("MFA service not enabled: ", err)
}
// If email service is not enabled continue the process in any way
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && isEmailServiceEnabled && !isMFADisabled {
otp := utils.GenerateOTP()
otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
Email: user.Email,
Otp: otp,
ExpiresAt: time.Now().Add(1 * time.Minute).Unix(),
})
if err != nil {
log.Debug("Failed to add otp: ", err)
return nil, err
}
go func() {
// exec it as go routine so that we can reduce the api latency
go email.SendEmail([]string{params.Email}, constants.VerificationTypeOTP, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"otp": otpData.Otp,
})
if err != nil {
log.Debug("Failed to send otp email: ", err)
}
}()
return &model.AuthResponse{
Message: "Please check the OTP in your inbox",
ShouldShowOtpScreen: refs.NewBoolRef(true),
}, nil
}
authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth) authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth)
if err != nil { if err != nil {
log.Debug("Failed to create auth token", err) log.Debug("Failed to create auth token", err)

View File

@@ -219,8 +219,12 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
return res, err return res, err
} }
// exec it as go routing so that we can reduce the api latency // exec it as go routine so that we can reduce the api latency
go email.SendVerificationMail(params.Email, verificationToken, hostname) go email.SendEmail([]string{params.Email}, constants.VerificationTypeMagicLinkLogin, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"verification_url": utils.GetEmailVerificationURL(verificationToken, hostname),
})
} }
res = &model.Response{ res = &model.Response{

View File

@@ -77,6 +77,18 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) {
githubClientSecret = "" githubClientSecret = ""
} }
twitterClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitterClientID)
if err != nil {
log.Debug("Failed to get Twitter Client ID from environment variable", err)
twitterClientID = ""
}
twitterClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitterClientSecret)
if err != nil {
log.Debug("Failed to get Twitter Client Secret from environment variable", err)
twitterClientSecret = ""
}
isBasicAuthDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) isBasicAuthDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication)
if err != nil { if err != nil {
log.Debug("Failed to get Disable Basic Authentication from environment variable", err) log.Debug("Failed to get Disable Basic Authentication from environment variable", err)
@@ -107,6 +119,12 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) {
isSignUpDisabled = true isSignUpDisabled = true
} }
isMultiFactorAuthenticationEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMultiFactorAuthentication)
if err != nil {
log.Debug("Failed to get Disable Multi Factor Authentication from environment variable", err)
isSignUpDisabled = true
}
metaInfo := model.Meta{ metaInfo := model.Meta{
Version: constants.VERSION, Version: constants.VERSION,
ClientID: clientID, ClientID: clientID,
@@ -115,11 +133,13 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) {
IsFacebookLoginEnabled: facebookClientID != "" && facebookClientSecret != "", IsFacebookLoginEnabled: facebookClientID != "" && facebookClientSecret != "",
IsLinkedinLoginEnabled: linkedClientID != "" && linkedInClientSecret != "", IsLinkedinLoginEnabled: linkedClientID != "" && linkedInClientSecret != "",
IsAppleLoginEnabled: appleClientID != "" && appleClientSecret != "", IsAppleLoginEnabled: appleClientID != "" && appleClientSecret != "",
IsTwitterLoginEnabled: twitterClientID != "" && twitterClientSecret != "",
IsBasicAuthenticationEnabled: !isBasicAuthDisabled, IsBasicAuthenticationEnabled: !isBasicAuthDisabled,
IsEmailVerificationEnabled: !isEmailVerificationDisabled, IsEmailVerificationEnabled: !isEmailVerificationDisabled,
IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled, IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled,
IsSignUpEnabled: !isSignUpDisabled, IsSignUpEnabled: !isSignUpDisabled,
IsStrongPasswordEnabled: !isStrongPasswordDisabled, IsStrongPasswordEnabled: !isStrongPasswordDisabled,
IsMultiFactorAuthEnabled: !isMultiFactorAuthenticationEnabled,
} }
return &metaInfo, nil return &metaInfo, nil
} }

View File

@@ -0,0 +1,101 @@
package resolvers
import (
"context"
"errors"
"fmt"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/utils"
)
// ResendOTPResolver is a resolver for resend otp mutation
func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) {
log := log.WithFields(log.Fields{
"email": params.Email,
})
params.Email = strings.ToLower(params.Email)
user, err := db.Provider.GetUserByEmail(ctx, params.Email)
if err != nil {
log.Debug("Failed to get user by email: ", err)
return nil, fmt.Errorf(`user with this email not found`)
}
if user.RevokedTimestamp != nil {
log.Debug("User access is revoked")
return nil, fmt.Errorf(`user access has been revoked`)
}
if user.EmailVerifiedAt == nil {
log.Debug("User email is not verified")
return nil, fmt.Errorf(`email not verified`)
}
if !refs.BoolValue(user.IsMultiFactorAuthEnabled) {
log.Debug("User multi factor authentication is not enabled")
return nil, fmt.Errorf(`multi factor authentication not enabled`)
}
isEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEmailServiceEnabled {
log.Debug("Email service not enabled: ", err)
return nil, errors.New("email service not enabled")
}
isMFADisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMultiFactorAuthentication)
if err != nil || isMFADisabled {
log.Debug("MFA service not enabled: ", err)
return nil, errors.New("multi factor authentication is disabled for this instance")
}
// get otp by email
otpData, err := db.Provider.GetOTPByEmail(ctx, params.Email)
if err != nil {
log.Debug("Failed to get otp for given email: ", err)
return nil, err
}
if otpData == nil {
log.Debug("No otp found for given email: ", params.Email)
return &model.Response{
Message: "Failed to get for given email",
}, errors.New("failed to get otp for given email")
}
otp := utils.GenerateOTP()
otpData, err = db.Provider.UpsertOTP(ctx, &models.OTP{
Email: user.Email,
Otp: otp,
ExpiresAt: time.Now().Add(1 * time.Minute).Unix(),
})
if err != nil {
log.Debug("Error generating new otp: ", err)
return nil, err
}
go func() {
// exec it as go routine so that we can reduce the api latency
go email.SendEmail([]string{params.Email}, constants.VerificationTypeOTP, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"otp": otp,
})
if err != nil {
log.Debug("Error sending otp email: ", otp)
}
}()
return &model.Response{
Message: `OTP has been sent. Please check your inbox`,
}, nil
}

View File

@@ -39,6 +39,11 @@ func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEma
return res, fmt.Errorf("invalid identifier") return res, fmt.Errorf("invalid identifier")
} }
user, err := db.Provider.GetUserByEmail(ctx, params.Email)
if err != nil {
return res, fmt.Errorf("invalid user")
}
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, params.Email, params.Identifier) verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, params.Email, params.Identifier)
if err != nil { if err != nil {
log.Debug("Failed to get verification request: ", err) log.Debug("Failed to get verification request: ", err)
@@ -74,8 +79,12 @@ func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEma
log.Debug("Failed to add verification request: ", err) log.Debug("Failed to add verification request: ", err)
} }
// exec it as go routin so that we can reduce the api latency // exec it as go routine so that we can reduce the api latency
go email.SendVerificationMail(params.Email, verificationToken, hostname) go email.SendEmail([]string{params.Email}, params.Identifier, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"verification_url": utils.GetEmailVerificationURL(verificationToken, hostname),
})
res = &model.Response{ res = &model.Response{
Message: `Verification email has been sent. Please check your inbox`, Message: `Verification email has been sent. Please check your inbox`,

View File

@@ -14,6 +14,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@@ -84,6 +85,16 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput)
signupMethod := user.SignupMethods signupMethod := user.SignupMethods
if !strings.Contains(signupMethod, constants.AuthRecipeMethodBasicAuth) { if !strings.Contains(signupMethod, constants.AuthRecipeMethodBasicAuth) {
signupMethod = signupMethod + "," + constants.AuthRecipeMethodBasicAuth signupMethod = signupMethod + "," + constants.AuthRecipeMethodBasicAuth
isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication)
if err != nil {
log.Debug("MFA service not enabled: ", err)
isMFAEnforced = false
}
if isMFAEnforced {
user.IsMultiFactorAuthEnabled = refs.NewBoolRef(true)
}
} }
user.SignupMethods = signupMethod user.SignupMethods = signupMethod

View File

@@ -17,6 +17,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@@ -157,6 +158,20 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
user.Picture = params.Picture user.Picture = params.Picture
} }
if params.IsMultiFactorAuthEnabled != nil {
user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled
}
isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication)
if err != nil {
log.Debug("MFA service not enabled: ", err)
isMFAEnforced = false
}
if isMFAEnforced {
user.IsMultiFactorAuthEnabled = refs.NewBoolRef(true)
}
user.SignupMethods = constants.AuthRecipeMethodBasicAuth user.SignupMethods = constants.AuthRecipeMethodBasicAuth
isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification)
if err != nil { if err != nil {
@@ -206,9 +221,14 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
return res, err return res, err
} }
// exec it as go routin so that we can reduce the api latency // exec it as go routine so that we can reduce the api latency
go func() { go func() {
email.SendVerificationMail(params.Email, verificationToken, hostname) // exec it as go routine so that we can reduce the api latency
email.SendEmail([]string{params.Email}, constants.VerificationTypeBasicAuthSignup, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"verification_url": utils.GetEmailVerificationURL(verificationToken, hostname),
})
utils.RegisterEvent(ctx, constants.UserCreatedWebhookEvent, constants.AuthRecipeMethodBasicAuth, user) utils.RegisterEvent(ctx, constants.UserCreatedWebhookEvent, constants.AuthRecipeMethodBasicAuth, user)
}() }()

View File

@@ -15,8 +15,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// TODO add template validator
// UpdateEmailTemplateResolver resolver for update email template mutation // UpdateEmailTemplateResolver resolver for update email template mutation
func UpdateEmailTemplateResolver(ctx context.Context, params model.UpdateEmailTemplateRequest) (*model.Response, error) { func UpdateEmailTemplateResolver(ctx context.Context, params model.UpdateEmailTemplateRequest) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx) gc, err := utils.GinContextFromContext(ctx)
@@ -51,6 +49,14 @@ func UpdateEmailTemplateResolver(ctx context.Context, params model.UpdateEmailTe
emailTemplateDetails.EventName = refs.StringValue(params.EventName) emailTemplateDetails.EventName = refs.StringValue(params.EventName)
} }
if params.Subject != nil && emailTemplateDetails.Subject != refs.StringValue(params.Subject) {
if strings.TrimSpace(refs.StringValue(params.Subject)) == "" {
log.Debug("empty subject not allowed")
return nil, fmt.Errorf("empty subject not allowed")
}
emailTemplateDetails.Subject = refs.StringValue(params.Subject)
}
if params.Template != nil && emailTemplateDetails.Template != refs.StringValue(params.Template) { if params.Template != nil && emailTemplateDetails.Template != refs.StringValue(params.Template) {
if strings.TrimSpace(refs.StringValue(params.Template)) == "" { if strings.TrimSpace(refs.StringValue(params.Template)) == "" {
log.Debug("empty template not allowed") log.Debug("empty template not allowed")
@@ -59,6 +65,14 @@ func UpdateEmailTemplateResolver(ctx context.Context, params model.UpdateEmailTe
emailTemplateDetails.Template = refs.StringValue(params.Template) emailTemplateDetails.Template = refs.StringValue(params.Template)
} }
if params.Design != nil && emailTemplateDetails.Design != refs.StringValue(params.Design) {
if strings.TrimSpace(refs.StringValue(params.Design)) == "" {
log.Debug("empty design not allowed")
return nil, fmt.Errorf("empty design not allowed")
}
emailTemplateDetails.Design = refs.StringValue(params.Design)
}
_, err = db.Provider.UpdateEmailTemplate(ctx, emailTemplateDetails) _, err = db.Provider.UpdateEmailTemplate(ctx, emailTemplateDetails)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -31,6 +31,7 @@ func clearSessionIfRequired(currentData, updatedData map[string]interface{}) {
isCurrentGoogleLoginEnabled := currentData[constants.EnvKeyGoogleClientID] != nil && currentData[constants.EnvKeyGoogleClientSecret] != nil && currentData[constants.EnvKeyGoogleClientID].(string) != "" && currentData[constants.EnvKeyGoogleClientSecret].(string) != "" isCurrentGoogleLoginEnabled := currentData[constants.EnvKeyGoogleClientID] != nil && currentData[constants.EnvKeyGoogleClientSecret] != nil && currentData[constants.EnvKeyGoogleClientID].(string) != "" && currentData[constants.EnvKeyGoogleClientSecret].(string) != ""
isCurrentGithubLoginEnabled := currentData[constants.EnvKeyGithubClientID] != nil && currentData[constants.EnvKeyGithubClientSecret] != nil && currentData[constants.EnvKeyGithubClientID].(string) != "" && currentData[constants.EnvKeyGithubClientSecret].(string) != "" isCurrentGithubLoginEnabled := currentData[constants.EnvKeyGithubClientID] != nil && currentData[constants.EnvKeyGithubClientSecret] != nil && currentData[constants.EnvKeyGithubClientID].(string) != "" && currentData[constants.EnvKeyGithubClientSecret].(string) != ""
isCurrentLinkedInLoginEnabled := currentData[constants.EnvKeyLinkedInClientID] != nil && currentData[constants.EnvKeyLinkedInClientSecret] != nil && currentData[constants.EnvKeyLinkedInClientID].(string) != "" && currentData[constants.EnvKeyLinkedInClientSecret].(string) != "" isCurrentLinkedInLoginEnabled := currentData[constants.EnvKeyLinkedInClientID] != nil && currentData[constants.EnvKeyLinkedInClientSecret] != nil && currentData[constants.EnvKeyLinkedInClientID].(string) != "" && currentData[constants.EnvKeyLinkedInClientSecret].(string) != ""
isCurrentTwitterLoginEnabled := currentData[constants.EnvKeyTwitterClientID] != nil && currentData[constants.EnvKeyTwitterClientSecret] != nil && currentData[constants.EnvKeyTwitterClientID].(string) != "" && currentData[constants.EnvKeyTwitterClientSecret].(string) != ""
isUpdatedBasicAuthEnabled := !updatedData[constants.EnvKeyDisableBasicAuthentication].(bool) isUpdatedBasicAuthEnabled := !updatedData[constants.EnvKeyDisableBasicAuthentication].(bool)
isUpdatedMagicLinkLoginEnabled := !updatedData[constants.EnvKeyDisableMagicLinkLogin].(bool) isUpdatedMagicLinkLoginEnabled := !updatedData[constants.EnvKeyDisableMagicLinkLogin].(bool)
@@ -39,6 +40,7 @@ func clearSessionIfRequired(currentData, updatedData map[string]interface{}) {
isUpdatedGoogleLoginEnabled := updatedData[constants.EnvKeyGoogleClientID] != nil && updatedData[constants.EnvKeyGoogleClientSecret] != nil && updatedData[constants.EnvKeyGoogleClientID].(string) != "" && updatedData[constants.EnvKeyGoogleClientSecret].(string) != "" isUpdatedGoogleLoginEnabled := updatedData[constants.EnvKeyGoogleClientID] != nil && updatedData[constants.EnvKeyGoogleClientSecret] != nil && updatedData[constants.EnvKeyGoogleClientID].(string) != "" && updatedData[constants.EnvKeyGoogleClientSecret].(string) != ""
isUpdatedGithubLoginEnabled := updatedData[constants.EnvKeyGithubClientID] != nil && updatedData[constants.EnvKeyGithubClientSecret] != nil && updatedData[constants.EnvKeyGithubClientID].(string) != "" && updatedData[constants.EnvKeyGithubClientSecret].(string) != "" isUpdatedGithubLoginEnabled := updatedData[constants.EnvKeyGithubClientID] != nil && updatedData[constants.EnvKeyGithubClientSecret] != nil && updatedData[constants.EnvKeyGithubClientID].(string) != "" && updatedData[constants.EnvKeyGithubClientSecret].(string) != ""
isUpdatedLinkedInLoginEnabled := updatedData[constants.EnvKeyLinkedInClientID] != nil && updatedData[constants.EnvKeyLinkedInClientSecret] != nil && updatedData[constants.EnvKeyLinkedInClientID].(string) != "" && updatedData[constants.EnvKeyLinkedInClientSecret].(string) != "" isUpdatedLinkedInLoginEnabled := updatedData[constants.EnvKeyLinkedInClientID] != nil && updatedData[constants.EnvKeyLinkedInClientSecret] != nil && updatedData[constants.EnvKeyLinkedInClientID].(string) != "" && updatedData[constants.EnvKeyLinkedInClientSecret].(string) != ""
isUpdatedTwitterLoginEnabled := updatedData[constants.EnvKeyTwitterClientID] != nil && updatedData[constants.EnvKeyTwitterClientSecret] != nil && updatedData[constants.EnvKeyTwitterClientID].(string) != "" && updatedData[constants.EnvKeyTwitterClientSecret].(string) != ""
if isCurrentBasicAuthEnabled && !isUpdatedBasicAuthEnabled { if isCurrentBasicAuthEnabled && !isUpdatedBasicAuthEnabled {
memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodBasicAuth) memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodBasicAuth)
@@ -67,6 +69,10 @@ func clearSessionIfRequired(currentData, updatedData map[string]interface{}) {
if isCurrentLinkedInLoginEnabled && !isUpdatedLinkedInLoginEnabled { if isCurrentLinkedInLoginEnabled && !isUpdatedLinkedInLoginEnabled {
memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodLinkedIn) memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodLinkedIn)
} }
if isCurrentTwitterLoginEnabled && !isUpdatedTwitterLoginEnabled {
memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodTwitter)
}
} }
// UpdateEnvResolver is a resolver for update config mutation // UpdateEnvResolver is a resolver for update config mutation
@@ -234,6 +240,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
// handle derivative cases like disabling email verification & magic login // handle derivative cases like disabling email verification & magic login
// in case SMTP is off but env is set to true // in case SMTP is off but env is set to true
if updatedData[constants.EnvKeySmtpHost] == "" || updatedData[constants.EnvKeySmtpUsername] == "" || updatedData[constants.EnvKeySmtpPassword] == "" || updatedData[constants.EnvKeySenderEmail] == "" && updatedData[constants.EnvKeySmtpPort] == "" { if updatedData[constants.EnvKeySmtpHost] == "" || updatedData[constants.EnvKeySmtpUsername] == "" || updatedData[constants.EnvKeySmtpPassword] == "" || updatedData[constants.EnvKeySenderEmail] == "" && updatedData[constants.EnvKeySmtpPort] == "" {
updatedData[constants.EnvKeyIsEmailServiceEnabled] = false
updatedData[constants.EnvKeyDisableMultiFactorAuthentication] = true
if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) { if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) {
updatedData[constants.EnvKeyDisableEmailVerification] = true updatedData[constants.EnvKeyDisableEmailVerification] = true
} }
@@ -243,6 +251,16 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
} }
} }
if updatedData[constants.EnvKeySmtpHost] != "" || updatedData[constants.EnvKeySmtpUsername] != "" || updatedData[constants.EnvKeySmtpPassword] != "" || updatedData[constants.EnvKeySenderEmail] != "" && updatedData[constants.EnvKeySmtpPort] != "" {
updatedData[constants.EnvKeyIsEmailServiceEnabled] = true
}
if !currentData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && updatedData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && !updatedData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) {
go db.Provider.UpdateUsers(ctx, map[string]interface{}{
"is_multi_factor_auth_enabled": true,
}, nil)
}
// check the roles change // check the roles change
if len(params.Roles) > 0 { if len(params.Roles) > 0 {
if len(params.DefaultRoles) > 0 { if len(params.DefaultRoles) > 0 {
@@ -265,8 +283,6 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
} }
} }
go clearSessionIfRequired(currentData, updatedData)
// Update local store // Update local store
memorystore.Provider.UpdateEnvStore(updatedData) memorystore.Provider.UpdateEnvStore(updatedData)
jwk, err := crypto.GenerateJWKBasedOnEnv() jwk, err := crypto.GenerateJWKBasedOnEnv()
@@ -320,6 +336,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
return res, err return res, err
} }
go clearSessionIfRequired(currentData, updatedData)
res = &model.Response{ res = &model.Response{
Message: "configurations updated successfully", Message: "configurations updated successfully",
} }

View File

@@ -2,6 +2,7 @@ package resolvers
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@@ -46,7 +47,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
} }
// 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 && params.NewPassword == nil && params.ConfirmNewPassword == 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 && params.NewPassword == nil && params.ConfirmNewPassword == nil && params.IsMultiFactorAuthEnabled == nil {
log.Debug("All params are empty") log.Debug("All params are empty")
return res, fmt.Errorf("please enter at least one param to update") return res, fmt.Errorf("please enter at least one param to update")
} }
@@ -94,6 +95,29 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
user.Picture = params.Picture user.Picture = params.Picture
} }
if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) {
if refs.BoolValue(params.IsMultiFactorAuthEnabled) {
isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEnvServiceEnabled {
log.Debug("Email service not enabled:")
return nil, errors.New("email service not enabled, so cannot enable multi factor authentication")
}
}
isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication)
if err != nil {
log.Debug("MFA service not enabled: ", err)
isMFAEnforced = false
}
if isMFAEnforced && !refs.BoolValue(params.IsMultiFactorAuthEnabled) {
log.Debug("Cannot disable mfa service as it is enforced:")
return nil, errors.New("cannot disable multi factor authentication as it is enforced by organization")
}
user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled
}
isPasswordChanging := false isPasswordChanging := false
if params.NewPassword != nil && params.ConfirmNewPassword == nil { if params.NewPassword != nil && params.ConfirmNewPassword == nil {
isPasswordChanging = true isPasswordChanging = true
@@ -220,8 +244,12 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
return res, err return res, err
} }
// exec it as go routin so that we can reduce the api latency // exec it as go routine so that we can reduce the api latency
go email.SendVerificationMail(newEmail, verificationToken, hostname) go email.SendEmail([]string{user.Email}, verificationType, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"verification_url": utils.GetEmailVerificationURL(verificationToken, hostname),
})
} }
} }

View File

@@ -2,6 +2,7 @@ package resolvers
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@@ -15,6 +16,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@@ -45,7 +47,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
"user_id": params.ID, "user_id": params.ID,
}) })
if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil && params.Roles == nil { if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil && params.Roles == nil && params.IsMultiFactorAuthEnabled == nil {
log.Debug("No params to update") log.Debug("No params to update")
return res, fmt.Errorf("please enter atleast one param to update") return res, fmt.Errorf("please enter atleast one param to update")
} }
@@ -56,38 +58,49 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
return res, fmt.Errorf(`User not found`) return res, fmt.Errorf(`User not found`)
} }
if params.GivenName != nil && user.GivenName != params.GivenName { if params.GivenName != nil && refs.StringValue(user.GivenName) != refs.StringValue(params.GivenName) {
user.GivenName = params.GivenName user.GivenName = params.GivenName
} }
if params.FamilyName != nil && user.FamilyName != params.FamilyName { if params.FamilyName != nil && refs.StringValue(user.FamilyName) != refs.StringValue(params.FamilyName) {
user.FamilyName = params.FamilyName user.FamilyName = params.FamilyName
} }
if params.MiddleName != nil && user.MiddleName != params.MiddleName { if params.MiddleName != nil && refs.StringValue(user.MiddleName) != refs.StringValue(params.MiddleName) {
user.MiddleName = params.MiddleName user.MiddleName = params.MiddleName
} }
if params.Nickname != nil && user.Nickname != params.Nickname { if params.Nickname != nil && refs.StringValue(user.Nickname) != refs.StringValue(params.Nickname) {
user.Nickname = params.Nickname user.Nickname = params.Nickname
} }
if params.Birthdate != nil && user.Birthdate != params.Birthdate { if params.Birthdate != nil && refs.StringValue(user.Birthdate) != refs.StringValue(params.Birthdate) {
user.Birthdate = params.Birthdate user.Birthdate = params.Birthdate
} }
if params.Gender != nil && user.Gender != params.Gender { if params.Gender != nil && refs.StringValue(user.Gender) != refs.StringValue(params.Gender) {
user.Gender = params.Gender user.Gender = params.Gender
} }
if params.PhoneNumber != nil && user.PhoneNumber != params.PhoneNumber { if params.PhoneNumber != nil && refs.StringValue(user.PhoneNumber) != refs.StringValue(params.PhoneNumber) {
user.PhoneNumber = params.PhoneNumber user.PhoneNumber = params.PhoneNumber
} }
if params.Picture != nil && user.Picture != params.Picture { if params.Picture != nil && refs.StringValue(user.Picture) != refs.StringValue(params.Picture) {
user.Picture = params.Picture user.Picture = params.Picture
} }
if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) {
user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled
if refs.BoolValue(params.IsMultiFactorAuthEnabled) {
isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEnvServiceEnabled {
log.Debug("Email service not enabled:")
return nil, errors.New("email service not enabled, so cannot enable multi factor authentication")
}
}
}
if params.EmailVerified != nil { if params.EmailVerified != nil {
if *params.EmailVerified { if *params.EmailVerified {
now := time.Now().Unix() now := time.Now().Unix()
@@ -143,8 +156,12 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
return res, err return res, err
} }
// exec it as go routin so that we can reduce the api latency // exec it as go routine so that we can reduce the api latency
go email.SendVerificationMail(newEmail, verificationToken, hostname) go email.SendEmail([]string{user.Email}, constants.VerificationTypeBasicAuthSignup, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"verification_url": utils.GetEmailVerificationURL(verificationToken, hostname),
})
} }

View File

@@ -0,0 +1,104 @@
package resolvers
import (
"context"
"fmt"
"strings"
"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/models"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
log "github.com/sirupsen/logrus"
)
// VerifyOtpResolver resolver for verify otp mutation
func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) {
var res *model.AuthResponse
gc, err := utils.GinContextFromContext(ctx)
if err != nil {
log.Debug("Failed to get GinContext: ", err)
return res, err
}
otp, err := db.Provider.GetOTPByEmail(ctx, params.Email)
if err != nil {
log.Debug("Failed to get otp request by email: ", err)
return res, fmt.Errorf(`invalid email: %s`, err.Error())
}
if params.Otp != otp.Otp {
log.Debug("Failed to verify otp request: Incorrect value")
return res, fmt.Errorf(`invalid otp`)
}
expiresIn := otp.ExpiresAt - time.Now().Unix()
if expiresIn < 0 {
log.Debug("Failed to verify otp request: Timeout")
return res, fmt.Errorf("otp expired")
}
user, err := db.Provider.GetUserByEmail(ctx, params.Email)
if err != nil {
log.Debug("Failed to get user by email: ", err)
return res, err
}
isSignUp := user.EmailVerifiedAt == nil
// TODO - Add Login method in DB when we introduce OTP for social media login
loginMethod := constants.AuthRecipeMethodBasicAuth
roles := strings.Split(user.Roles, ",")
scope := []string{"openid", "email", "profile"}
authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod)
if err != nil {
log.Debug("Failed to create auth token: ", err)
return res, err
}
go func() {
db.Provider.DeleteOTP(gc, otp)
if isSignUp {
utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user)
} else {
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, loginMethod, user)
}
db.Provider.AddSession(ctx, models.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(gc.Request),
IP: utils.GetIP(gc.Request),
})
}()
authTokenExpiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
if authTokenExpiresIn <= 0 {
authTokenExpiresIn = 1
}
res = &model.AuthResponse{
Message: `OTP verified successfully.`,
AccessToken: &authToken.AccessToken.Token,
IDToken: &authToken.IDToken.Token,
ExpiresIn: &authTokenExpiresIn,
User: user.AsAPIUser(),
}
sessionKey := loginMethod + ":" + user.ID
cookie.SetSession(gc, authToken.FingerPrintHash)
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash)
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token)
if authToken.RefreshToken != nil {
res.RefreshToken = &authToken.RefreshToken.Token
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token)
}
return res, nil
}

View File

@@ -31,19 +31,44 @@ func addEmailTemplateTest(t *testing.T, s TestSetup) {
assert.Nil(t, emailTemplate) assert.Nil(t, emailTemplate)
}) })
t.Run("should not add email template for empty template", func(t *testing.T) { t.Run("should not add email template for empty subject", func(t *testing.T) {
emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{ emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{
EventName: s.TestInfo.TestEmailTemplateEventTypes[0], EventName: s.TestInfo.TestEmailTemplateEventTypes[0],
Template: " ", Template: " test ",
Subject: " ",
}) })
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, emailTemplate) assert.Nil(t, emailTemplate)
}) })
t.Run("should not add email template for empty template", func(t *testing.T) {
emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{
EventName: s.TestInfo.TestEmailTemplateEventTypes[0],
Template: " ",
Subject: "test",
})
assert.Error(t, err)
assert.Nil(t, emailTemplate)
})
t.Run("should not add email template with empty design", func(t *testing.T) {
emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{
EventName: s.TestInfo.TestEmailTemplateEventTypes[0],
Template: "test",
Subject: "test",
Design: " ",
})
assert.Error(t, err)
assert.Nil(t, emailTemplate)
})
for _, eventType := range s.TestInfo.TestEmailTemplateEventTypes { for _, eventType := range s.TestInfo.TestEmailTemplateEventTypes {
t.Run("should add email template for "+eventType, func(t *testing.T) { t.Run("should add email template for "+eventType, func(t *testing.T) {
emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{ emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{
EventName: eventType, EventName: eventType,
Template: `Test email`, Template: "Test email",
Subject: "Test email",
Design: "Test design",
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, emailTemplate) assert.NotNil(t, emailTemplate)
@@ -52,6 +77,8 @@ func addEmailTemplateTest(t *testing.T, s TestSetup) {
et, err := db.Provider.GetEmailTemplateByEventName(ctx, eventType) et, err := db.Provider.GetEmailTemplateByEventName(ctx, eventType)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, et.EventName, eventType) assert.Equal(t, et.EventName, eventType)
assert.Equal(t, "Test email", et.Subject)
assert.Equal(t, "Test design", et.Design)
}) })
} }
}) })

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