Compare commits

...

38 Commits

Author SHA1 Message Date
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
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
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
9ef5f33f7a feat: add is_multi_factor_auth_enabled 2022-07-23 15:26:44 +05:30
85 changed files with 2987 additions and 1049 deletions

View File

@@ -11,14 +11,26 @@ clean:
rm -rf build
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:
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_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
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_mongodb_db
docker rm -vf authorizer_arangodb
generate:
cd server && go get github.com/99designs/gqlgen/cmd@v0.14.0 && go run github.com/99designs/gqlgen generate

30
app/package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@authorizerdev/authorizer-react": "^0.25.0",
"@authorizerdev/authorizer-react": "^0.26.0-beta.0",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17",
@@ -26,9 +26,9 @@
}
},
"node_modules/@authorizerdev/authorizer-js": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz",
"integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==",
"version": "0.17.0-beta.1",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz",
"integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==",
"dependencies": {
"node-fetch": "^2.6.1"
},
@@ -37,11 +37,11 @@
}
},
"node_modules/@authorizerdev/authorizer-react": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz",
"integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==",
"version": "0.26.0-beta.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz",
"integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==",
"dependencies": {
"@authorizerdev/authorizer-js": "^0.14.0",
"@authorizerdev/authorizer-js": "^0.17.0-beta.1",
"final-form": "^4.20.2",
"react-final-form": "^6.5.3",
"styled-components": "^5.3.0"
@@ -852,19 +852,19 @@
},
"dependencies": {
"@authorizerdev/authorizer-js": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz",
"integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==",
"version": "0.17.0-beta.1",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz",
"integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==",
"requires": {
"node-fetch": "^2.6.1"
}
},
"@authorizerdev/authorizer-react": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz",
"integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==",
"version": "0.26.0-beta.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz",
"integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==",
"requires": {
"@authorizerdev/authorizer-js": "^0.14.0",
"@authorizerdev/authorizer-js": "^0.17.0-beta.1",
"final-form": "^4.20.2",
"react-final-form": "^6.5.3",
"styled-components": "^5.3.0"

View File

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

View File

@@ -17,9 +17,6 @@
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.2",
"dayjs": "^1.10.7",
"draft-js": "^0.11.7",
"draft-js-import-html": "^1.4.1",
"draftjs-to-html": "^0.9.1",
"esbuild": "^0.14.9",
"focus-visible": "^5.2.0",
"framer-motion": "^5.5.5",
@@ -29,14 +26,14 @@
"react-dom": "^17.0.2",
"react-draft-wysiwyg": "^1.15.0",
"react-dropzone": "^12.0.4",
"react-email-editor": "^1.6.1",
"react-icons": "^4.3.1",
"react-router-dom": "^6.2.1",
"typescript": "^4.5.4",
"urql": "^2.0.6"
},
"devDependencies": {
"@types/draftjs-to-html": "^0.8.1",
"@types/react-draft-wysiwyg": "^1.13.4"
"@types/react-email-editor": "^1.1.7"
}
},
"node_modules/@babel/code-frame": {
@@ -1153,25 +1150,6 @@
"react-dom": "^16.8.0 || 17.x"
}
},
"node_modules/@types/draft-js": {
"version": "0.11.9",
"resolved": "https://registry.npmjs.org/@types/draft-js/-/draft-js-0.11.9.tgz",
"integrity": "sha512-cQJBZjjIlGaPA1tOY+wGz2KhlPtAAZOIXpUvGPxPRw5uzZ2tcj8m6Yu1QDV9YgP36+cqE3cUvgkARBzgUiuI/Q==",
"dev": true,
"dependencies": {
"@types/react": "*",
"immutable": "~3.7.4"
}
},
"node_modules/@types/draftjs-to-html": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@types/draftjs-to-html/-/draftjs-to-html-0.8.1.tgz",
"integrity": "sha512-NBkphQs+qZ/sAz/j1pCUaxkPAOx00LTsE88aMSSfcvK+UfCpjHJDqIMCkm6wKotuJvY5w0BtdRazQ0sAaXzPdg==",
"dev": true,
"dependencies": {
"@types/draft-js": "*"
}
},
"node_modules/@types/history": {
"version": "4.7.9",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
@@ -1218,13 +1196,12 @@
"@types/react": "*"
}
},
"node_modules/@types/react-draft-wysiwyg": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/@types/react-draft-wysiwyg/-/react-draft-wysiwyg-1.13.4.tgz",
"integrity": "sha512-wasD1t78JDmQvdPDRPf/mf5FSHMlncunW0F6KMOKB3awzi3Wi21yHMGsRAUOkfTr3R8F+yceG8fSLz0kYWu/QA==",
"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/draft-js": "*",
"@types/react": "*"
}
},
@@ -1296,11 +1273,6 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
},
"node_modules/attr-accept": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
@@ -1387,16 +1359,6 @@
"toggle-selection": "^1.0.6"
}
},
"node_modules/core-js": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
"integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cosmiconfig": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
@@ -1412,14 +1374,6 @@
"node": ">=8"
}
},
"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-box-model": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
@@ -1448,59 +1402,6 @@
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
},
"node_modules/draft-js": {
"version": "0.11.7",
"resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.11.7.tgz",
"integrity": "sha512-ne7yFfN4sEL82QPQEn80xnADR8/Q6ALVworbC5UOSzOvjffmYfFsr3xSZtxbIirti14R7Y33EZC5rivpLgIbsg==",
"dependencies": {
"fbjs": "^2.0.0",
"immutable": "~3.7.4",
"object-assign": "^4.1.1"
},
"peerDependencies": {
"react": ">=0.14.0",
"react-dom": ">=0.14.0"
}
},
"node_modules/draft-js-import-element": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/draft-js-import-element/-/draft-js-import-element-1.4.0.tgz",
"integrity": "sha512-WmYT5PrCm47lGL5FkH6sRO3TTAcn7qNHsD3igiPqLG/RXrqyKrqN4+wBgbcT2lhna/yfWTRtgzAbQsSJoS1Meg==",
"dependencies": {
"draft-js-utils": "^1.4.0",
"synthetic-dom": "^1.4.0"
},
"peerDependencies": {
"draft-js": ">=0.10.0",
"immutable": "3.x.x"
}
},
"node_modules/draft-js-import-html": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/draft-js-import-html/-/draft-js-import-html-1.4.1.tgz",
"integrity": "sha512-KOZmtgxZriCDgg5Smr3Y09TjubvXe7rHPy/2fuLSsL+aSzwUDwH/aHDA/k47U+WfpmL4qgyg4oZhqx9TYJV0tg==",
"dependencies": {
"draft-js-import-element": "^1.4.0"
},
"peerDependencies": {
"draft-js": ">=0.10.0",
"immutable": "3.x.x"
}
},
"node_modules/draft-js-utils": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.4.1.tgz",
"integrity": "sha512-xE81Y+z/muC5D5z9qWmKfxEW1XyXfsBzSbSBk2JRsoD0yzMGGHQm/0MtuqHl/EUDkaBJJLjJ2EACycoDMY/OOg==",
"peerDependencies": {
"draft-js": ">=0.10.0",
"immutable": "3.x.x"
}
},
"node_modules/draftjs-to-html": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/draftjs-to-html/-/draftjs-to-html-0.9.1.tgz",
"integrity": "sha512-fFstE6+IayaVFBEvaFt/wN8vdj8FsTRzij7dy7LI9QIwf5LgfHFi9zSpvCg+feJ2tbYVqHxUkjcibwpsTpgFVQ=="
},
"node_modules/draftjs-utils": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/draftjs-utils/-/draftjs-utils-0.10.2.tgz",
@@ -1774,26 +1675,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/fbjs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-2.0.0.tgz",
"integrity": "sha512-8XA8ny9ifxrAWlyhAbexXcs3rRMtxWcs3M0lctLfB49jRDHiaxj+Mo0XxbwE7nKZYzgCFoq64FS+WFd4IycPPQ==",
"dependencies": {
"core-js": "^3.6.4",
"cross-fetch": "^3.0.4",
"fbjs-css-vars": "^1.0.0",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.18"
}
},
"node_modules/fbjs-css-vars": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="
},
"node_modules/file-selector": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
@@ -1958,14 +1839,6 @@
"immutable": "3.x.x || 4.x.x"
}
},
"node_modules/immutable": {
"version": "3.7.6",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz",
"integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -2049,25 +1922,6 @@
"loose-envify": "cli.js"
}
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -2136,14 +1990,6 @@
"tslib": "^2.1.0"
}
},
"node_modules/promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"dependencies": {
"asap": "~2.0.3"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -2224,6 +2070,14 @@
"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": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
@@ -2424,11 +2278,6 @@
"object-assign": "^4.1.1"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -2462,11 +2311,6 @@
"node": ">=4"
}
},
"node_modules/synthetic-dom": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/synthetic-dom/-/synthetic-dom-1.4.0.tgz",
"integrity": "sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg=="
},
"node_modules/tiny-invariant": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
@@ -2485,11 +2329,6 @@
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@@ -2507,24 +2346,6 @@
"node": ">=4.2.0"
}
},
"node_modules/ua-parser-js": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
@@ -2588,20 +2409,6 @@
"loose-envify": "^1.0.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wonka": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/wonka/-/wonka-4.0.15.tgz",
@@ -3439,25 +3246,6 @@
"tslib": "^2.1.0"
}
},
"@types/draft-js": {
"version": "0.11.9",
"resolved": "https://registry.npmjs.org/@types/draft-js/-/draft-js-0.11.9.tgz",
"integrity": "sha512-cQJBZjjIlGaPA1tOY+wGz2KhlPtAAZOIXpUvGPxPRw5uzZ2tcj8m6Yu1QDV9YgP36+cqE3cUvgkARBzgUiuI/Q==",
"dev": true,
"requires": {
"@types/react": "*",
"immutable": "~3.7.4"
}
},
"@types/draftjs-to-html": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@types/draftjs-to-html/-/draftjs-to-html-0.8.1.tgz",
"integrity": "sha512-NBkphQs+qZ/sAz/j1pCUaxkPAOx00LTsE88aMSSfcvK+UfCpjHJDqIMCkm6wKotuJvY5w0BtdRazQ0sAaXzPdg==",
"dev": true,
"requires": {
"@types/draft-js": "*"
}
},
"@types/history": {
"version": "4.7.9",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
@@ -3504,13 +3292,12 @@
"@types/react": "*"
}
},
"@types/react-draft-wysiwyg": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/@types/react-draft-wysiwyg/-/react-draft-wysiwyg-1.13.4.tgz",
"integrity": "sha512-wasD1t78JDmQvdPDRPf/mf5FSHMlncunW0F6KMOKB3awzi3Wi21yHMGsRAUOkfTr3R8F+yceG8fSLz0kYWu/QA==",
"@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/draft-js": "*",
"@types/react": "*"
}
},
@@ -3575,11 +3362,6 @@
}
}
},
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
},
"attr-accept": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
@@ -3656,11 +3438,6 @@
"toggle-selection": "^1.0.6"
}
},
"core-js": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
"integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg=="
},
"cosmiconfig": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
@@ -3673,14 +3450,6 @@
"yaml": "^1.7.2"
}
},
"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-box-model": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
@@ -3709,43 +3478,6 @@
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
},
"draft-js": {
"version": "0.11.7",
"resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.11.7.tgz",
"integrity": "sha512-ne7yFfN4sEL82QPQEn80xnADR8/Q6ALVworbC5UOSzOvjffmYfFsr3xSZtxbIirti14R7Y33EZC5rivpLgIbsg==",
"requires": {
"fbjs": "^2.0.0",
"immutable": "~3.7.4",
"object-assign": "^4.1.1"
}
},
"draft-js-import-element": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/draft-js-import-element/-/draft-js-import-element-1.4.0.tgz",
"integrity": "sha512-WmYT5PrCm47lGL5FkH6sRO3TTAcn7qNHsD3igiPqLG/RXrqyKrqN4+wBgbcT2lhna/yfWTRtgzAbQsSJoS1Meg==",
"requires": {
"draft-js-utils": "^1.4.0",
"synthetic-dom": "^1.4.0"
}
},
"draft-js-import-html": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/draft-js-import-html/-/draft-js-import-html-1.4.1.tgz",
"integrity": "sha512-KOZmtgxZriCDgg5Smr3Y09TjubvXe7rHPy/2fuLSsL+aSzwUDwH/aHDA/k47U+WfpmL4qgyg4oZhqx9TYJV0tg==",
"requires": {
"draft-js-import-element": "^1.4.0"
}
},
"draft-js-utils": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.4.1.tgz",
"integrity": "sha512-xE81Y+z/muC5D5z9qWmKfxEW1XyXfsBzSbSBk2JRsoD0yzMGGHQm/0MtuqHl/EUDkaBJJLjJ2EACycoDMY/OOg=="
},
"draftjs-to-html": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/draftjs-to-html/-/draftjs-to-html-0.9.1.tgz",
"integrity": "sha512-fFstE6+IayaVFBEvaFt/wN8vdj8FsTRzij7dy7LI9QIwf5LgfHFi9zSpvCg+feJ2tbYVqHxUkjcibwpsTpgFVQ=="
},
"draftjs-utils": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/draftjs-utils/-/draftjs-utils-0.10.2.tgz",
@@ -3897,26 +3629,6 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"fbjs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-2.0.0.tgz",
"integrity": "sha512-8XA8ny9ifxrAWlyhAbexXcs3rRMtxWcs3M0lctLfB49jRDHiaxj+Mo0XxbwE7nKZYzgCFoq64FS+WFd4IycPPQ==",
"requires": {
"core-js": "^3.6.4",
"cross-fetch": "^3.0.4",
"fbjs-css-vars": "^1.0.0",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.18"
}
},
"fbjs-css-vars": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="
},
"file-selector": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
@@ -4045,11 +3757,6 @@
"resolved": "https://registry.npmjs.org/html-to-draftjs/-/html-to-draftjs-1.5.0.tgz",
"integrity": "sha512-kggLXBNciKDwKf+KYsuE+V5gw4dZ7nHyGMX9m0wy7urzWjKGWyNFetmArRLvRV0VrxKN70WylFsJvMTJx02OBQ=="
},
"immutable": {
"version": "3.7.6",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz",
"integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw=="
},
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -4121,14 +3828,6 @@
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4184,14 +3883,6 @@
}
}
},
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"requires": {
"asap": "~2.0.3"
}
},
"prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -4251,6 +3942,11 @@
"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=="
},
"react-fast-compare": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
@@ -4394,11 +4090,6 @@
"object-assign": "^4.1.1"
}
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -4426,11 +4117,6 @@
"has-flag": "^3.0.0"
}
},
"synthetic-dom": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/synthetic-dom/-/synthetic-dom-1.4.0.tgz",
"integrity": "sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg=="
},
"tiny-invariant": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
@@ -4446,11 +4132,6 @@
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@@ -4461,11 +4142,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz",
"integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg=="
},
"ua-parser-js": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ=="
},
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
@@ -4509,20 +4185,6 @@
"loose-envify": "^1.0.0"
}
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"wonka": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/wonka/-/wonka-4.0.15.tgz",

View File

@@ -19,9 +19,6 @@
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.2",
"dayjs": "^1.10.7",
"draft-js": "^0.11.7",
"draft-js-import-html": "^1.4.1",
"draftjs-to-html": "^0.9.1",
"esbuild": "^0.14.9",
"focus-visible": "^5.2.0",
"framer-motion": "^5.5.5",
@@ -31,13 +28,13 @@
"react-dom": "^17.0.2",
"react-draft-wysiwyg": "^1.15.0",
"react-dropzone": "^12.0.4",
"react-email-editor": "^1.6.1",
"react-icons": "^4.3.1",
"react-router-dom": "^6.2.1",
"typescript": "^4.5.4",
"urql": "^2.0.6"
},
"devDependencies": {
"@types/draftjs-to-html": "^0.8.1",
"@types/react-draft-wysiwyg": "^1.13.4"
"@types/react-email-editor": "^1.1.7"
}
}

View File

@@ -83,6 +83,35 @@ const Features = ({ variables, setVariables }: any) => {
/>
</Flex>
</Flex>
<Flex>
<Flex w="100%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Disable Multi Factor Authentication:</Text>
</Flex>
<Flex justifyContent="start" mb={3}>
<InputField
variables={variables}
setVariables={setVariables}
inputType={SwitchInputType.DISABLE_MULTI_FACTOR_AUTHENTICATION}
/>
</Flex>
</Flex>
</Stack>
<Text fontSize="md" paddingTop="2%" fontWeight="bold" mb={5}>
Enable Features
</Text>
<Stack spacing={6} padding="2% 0%">
<Flex>
<Flex w="100%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Enforce Multi Factor Authentication:</Text>
</Flex>
<Flex justifyContent="start" mb={3}>
<InputField
variables={variables}
setVariables={setVariables}
inputType={SwitchInputType.ENFORCE_MULTI_FACTOR_AUTHENTICATION}
/>
</Flex>
</Flex>
</Stack>
</div>
);

View File

@@ -108,7 +108,7 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.GOOGLE_CLIENT_SECRET}
placeholder="Google Secret"
placeholder="Google Client Secret"
/>
</Center>
</Flex>
@@ -146,7 +146,7 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.GITHUB_CLIENT_SECRET}
placeholder="Github Secret"
placeholder="Github Client Secret"
/>
</Center>
</Flex>
@@ -184,7 +184,7 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.FACEBOOK_CLIENT_SECRET}
placeholder="Facebook Secret"
placeholder="Facebook Client Secret"
/>
</Center>
</Flex>
@@ -260,7 +260,7 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.APPLE_CLIENT_SECRET}
placeholder="Apple CLient Secret"
placeholder="Apple Client Secret"
/>
</Center>
</Flex>

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
Center,
@@ -17,13 +17,22 @@ import {
Text,
useDisclosure,
useToast,
Alert,
AlertIcon,
Collapse,
Box,
TableContainer,
Table,
Thead,
Tr,
Th,
Tbody,
Td,
Code,
} from '@chakra-ui/react';
import { FaPlus } from 'react-icons/fa';
import { FaPlus, FaAngleDown, FaAngleUp } from 'react-icons/fa';
import { useClient } from 'urql';
import { Editor } from 'react-draft-wysiwyg';
import { EditorState, convertToRaw, Modifier } from 'draft-js';
import draftToHtml from 'draftjs-to-html';
import { stateFromHTML } from 'draft-js-import-html';
import EmailEditor from 'react-email-editor';
import {
UpdateModalViews,
EmailTemplateInputDataFields,
@@ -39,6 +48,7 @@ interface selectedEmailTemplateDataTypes {
[EmailTemplateInputDataFields.SUBJECT]: string;
[EmailTemplateInputDataFields.CREATED_AT]: number;
[EmailTemplateInputDataFields.TEMPLATE]: string;
[EmailTemplateInputDataFields.DESIGN]: string;
}
interface UpdateEmailTemplateInputPropTypes {
@@ -50,6 +60,7 @@ interface UpdateEmailTemplateInputPropTypes {
interface templateVariableDataTypes {
text: string;
value: string;
description: string;
}
interface emailTemplateDataType {
@@ -62,8 +73,7 @@ interface validatorDataType {
}
const initTemplateData: emailTemplateDataType = {
[EmailTemplateInputDataFields.EVENT_NAME]:
emailTemplateEventNames.BASIC_AUTH_SIGNUP,
[EmailTemplateInputDataFields.EVENT_NAME]: emailTemplateEventNames.Signup,
[EmailTemplateInputDataFields.SUBJECT]: '',
};
@@ -78,11 +88,9 @@ const UpdateEmailTemplate = ({
}: UpdateEmailTemplateInputPropTypes) => {
const client = useClient();
const toast = useToast();
const emailEditorRef = useRef(null);
const { isOpen, onOpen, onClose } = useDisclosure();
const [loading, setLoading] = useState<boolean>(false);
const [editorState, setEditorState] = React.useState<EditorState>(
EditorState.createEmpty()
);
const [templateVariables, setTemplateVariables] = useState<
templateVariableDataTypes[]
>([]);
@@ -92,9 +100,23 @@ const UpdateEmailTemplate = ({
const [validator, setValidator] = useState<validatorDataType>({
...initTemplateValidatorData,
});
const onEditorStateChange = (editorState: EditorState) => {
setEditorState(editorState);
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({
@@ -106,14 +128,8 @@ const UpdateEmailTemplate = ({
};
const validateData = () => {
const rawData: string = draftToHtml(
convertToRaw(editorState.getCurrentContent())
).trim();
return (
!loading &&
rawData &&
rawData !== '<p></p>' &&
rawData !== '<h1></h1>' &&
templateData[EmailTemplateInputDataFields.EVENT_NAME].length > 0 &&
templateData[EmailTemplateInputDataFields.SUBJECT].length > 0 &&
validator[EmailTemplateInputDataFields.SUBJECT]
@@ -123,69 +139,72 @@ const UpdateEmailTemplate = ({
const saveData = async () => {
if (!validateData()) return;
setLoading(true);
const params = {
[EmailTemplateInputDataFields.EVENT_NAME]:
templateData[EmailTemplateInputDataFields.EVENT_NAME],
[EmailTemplateInputDataFields.SUBJECT]:
templateData[EmailTemplateInputDataFields.SUBJECT],
[EmailTemplateInputDataFields.TEMPLATE]: draftToHtml(
convertToRaw(editorState.getCurrentContent())
).trim(),
};
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();
// @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);
setEditorState(
EditorState.createWithContent(stateFromHTML(selectedTemplate.template))
);
} else {
setTemplateData({ ...initTemplateData });
setEditorState(EditorState.createEmpty());
}
};
useEffect(() => {
@@ -195,35 +214,36 @@ const UpdateEmailTemplate = ({
selectedTemplate &&
Object.keys(selectedTemplate || {}).length
) {
const { id, created_at, template, ...rest } = selectedTemplate;
const { id, created_at, template, design, ...rest } = selectedTemplate;
setTemplateData(rest);
setEditorState(EditorState.createWithContent(stateFromHTML(template)));
}
}, [isOpen]);
useEffect(() => {
const updatedTemplateVariables = Object.entries(
emailTemplateVariables
).reduce((acc, varData): any => {
).reduce((acc, [key, val]): any => {
if (
(templateData[EmailTemplateInputDataFields.EVENT_NAME] !==
emailTemplateEventNames.VERIFY_OTP &&
varData[1] === emailTemplateVariables.otp) ||
emailTemplateEventNames['Verify Otp'] &&
val === emailTemplateVariables.otp) ||
(templateData[EmailTemplateInputDataFields.EVENT_NAME] ===
emailTemplateEventNames.VERIFY_OTP &&
varData[1] === emailTemplateVariables.verification_url)
emailTemplateEventNames['Verify Otp'] &&
val === emailTemplateVariables.verification_url)
) {
return acc;
}
return [
...acc,
{
text: varData[0],
value: varData[1],
text: key,
value: val.value,
description: val.description,
},
];
}, []);
setTemplateVariables(updatedTemplateVariables);
}, [templateData[EmailTemplateInputDataFields.EVENT_NAME]]);
return (
<>
{view === UpdateModalViews.ADD ? (
@@ -240,7 +260,14 @@ const UpdateEmailTemplate = ({
) : (
<MenuItem onClick={onOpen}>Edit</MenuItem>
)}
<Modal isOpen={isOpen} onClose={onClose} size="3xl">
<Modal
isOpen={isOpen}
onClose={() => {
resetData();
onClose();
}}
size="6xl"
>
<ModalOverlay />
<ModalContent>
<ModalHeader>
@@ -257,6 +284,73 @@ const UpdateEmailTemplate = ({
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"
@@ -291,7 +385,7 @@ const UpdateEmailTemplate = ({
width="100%"
justifyContent="start"
alignItems="center"
marginBottom="5%"
marginBottom="2%"
>
<Flex flex="1">Subject</Flex>
<Flex flex="3">
@@ -316,33 +410,21 @@ const UpdateEmailTemplate = ({
</Flex>
<Flex
width="100%"
justifyContent="space-between"
justifyContent="flex-start"
alignItems="center"
marginBottom="2%"
>
<Flex>Template Body</Flex>
<Text
style={{
fontSize: 14,
}}
color="gray.400"
>{`To select dynamic variables open curly braces "{"`}</Text>
Template Body
</Flex>
<Flex
width="100%"
justifyContent="flex-start"
alignItems="center"
border="1px solid"
borderColor="gray.200"
>
<EmailEditor ref={emailEditorRef} onReady={onReady} />
</Flex>
<Editor
editorState={editorState}
onEditorStateChange={onEditorStateChange}
editorStyle={{
border: '1px solid #d9d9d9',
borderRadius: '5px',
marginTop: '2%',
height: '35vh',
}}
mention={{
separator: ' ',
trigger: '{',
suggestions: templateVariables,
}}
/>
</Flex>
</ModalBody>
<ModalFooter>

View File

@@ -21,6 +21,9 @@ import {
Text,
useDisclosure,
useToast,
Alert,
AlertIcon,
Divider,
} from '@chakra-ui/react';
import {
FaAngleDown,
@@ -94,7 +97,7 @@ interface validatorDataType {
}
const initWebhookData: webhookDataType = {
[WebhookInputDataFields.EVENT_NAME]: webhookEventNames.USER_LOGIN,
[WebhookInputDataFields.EVENT_NAME]: webhookEventNames['User login'],
[WebhookInputDataFields.ENDPOINT]: '',
[WebhookInputDataFields.ENABLED]: true,
[WebhookInputDataFields.HEADERS]: [{ ...initHeadersData }],
@@ -468,68 +471,12 @@ const UpdateWebhookModal = ({
</Text>
</Flex>
</Flex>
<Flex
width="100%"
justifyContent="center"
alignItems="center"
marginBottom="5%"
flexDirection="column"
>
<Flex
width="100%"
justifyContent="space-between"
alignItems="center"
>
<Flex>
Payload
<Text color="gray.500" ml={1}>
(example)
</Text>
</Flex>
<Button
onClick={() => setIsShowingPayload(!isShowingPayload)}
variant="ghost"
>
{isShowingPayload ? <FaAngleUp /> : <FaAngleDown />}
</Button>
</Flex>
<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
width="100%"
justifyContent="space-between"
alignItems="center"
marginBottom="2%"
marginBottom="5%"
>
<Flex>Headers</Flex>
<Flex>
@@ -546,7 +493,8 @@ const UpdateWebhookModal = ({
</Button>
</Flex>
</Flex>
<Flex flexDirection="column" maxH={220} overflowY="scroll">
<Flex flexDirection="column" maxH={220} overflowY="auto">
{webhook[WebhookInputDataFields.HEADERS]?.map(
(headerData, index) => (
<Flex
@@ -615,6 +563,54 @@ const UpdateWebhookModal = ({
)
)}
</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>
</ModalBody>
<ModalFooter>

View File

@@ -68,6 +68,8 @@ export const SwitchInputType = {
DISABLE_SIGN_UP: 'DISABLE_SIGN_UP',
DISABLE_REDIS_FOR_ENV: 'DISABLE_REDIS_FOR_ENV',
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 = {
@@ -138,6 +140,8 @@ export interface envVarTypes {
DATABASE_TYPE: string;
DATABASE_URL: string;
ACCESS_TOKEN_EXPIRY_TIME: string;
DISABLE_MULTI_FACTOR_AUTHENTICATION: boolean;
ENFORCE_MULTI_FACTOR_AUTHENTICATION: boolean;
}
export const envSubViews = {
@@ -168,6 +172,7 @@ export enum EmailTemplateInputDataFields {
SUBJECT = 'subject',
CREATED_AT = 'created_at',
TEMPLATE = 'template',
DESIGN = 'design',
}
export enum WebhookInputHeaderFields {
@@ -183,20 +188,21 @@ export enum UpdateModalViews {
export const pageLimits: number[] = [5, 10, 15];
export const webhookEventNames = {
USER_SIGNUP: 'user.signup',
USER_CREATED: 'user.created',
USER_LOGIN: 'user.login',
USER_DELETED: 'user.deleted',
USER_ACCESS_ENABLED: 'user.access_enabled',
USER_ACCESS_REVOKED: 'user.access_revoked',
'User signup': 'user.signup',
'User created': 'user.created',
'User login': 'user.login',
'User deleted': 'user.deleted',
'User access enabled': 'user.access_enabled',
'User access revoked': 'user.access_revoked',
};
export const emailTemplateEventNames = {
BASIC_AUTH_SIGNUP: 'basic_auth_signup',
MAGIC_LINK_LOGIN: 'magic_link_login',
UPDATE_EMAIL: 'update_email',
FORGOT_PASSWORD: 'forgot_password',
VERIFY_OTP: 'verify_otp',
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 {
@@ -206,27 +212,90 @@ export enum webhookVerifiedStatus {
}
export const emailTemplateVariables = {
'user.id': '{user.id}}',
'user.email': '{user.email}}',
'user.given_name': '{user.given_name}}',
'user.family_name': '{user.family_name}}',
'user.signup_methods': '{user.signup_methods}}',
'user.email_verified': '{user.email_verified}}',
'user.picture': '{user.picture}}',
'user.roles': '{user.roles}}',
'user.middle_name': '{user.middle_name}}',
'user.nickname': '{user.nickname}}',
'user.preferred_username': '{user.preferred_username}}',
'user.gender': '{user.gender}}',
'user.birthdate': '{user.birthdate}}',
'user.phone_number': '{user.phone_number}}',
'user.phone_number_verified': '{user.phone_number_verified}}',
'user.created_at': '{user.created_at}}',
'user.updated_at': '{user.updated_at}}',
'organization.name': '{organization.name}}',
'organization.logo': '{organization.logo}}',
verification_url: '{verification_url}}',
otp: '{otp}}',
'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 = `{

View File

@@ -60,6 +60,8 @@ export const EnvVariablesQuery = `
DATABASE_TYPE,
DATABASE_URL,
ACCESS_TOKEN_EXPIRY_TIME,
DISABLE_MULTI_FACTOR_AUTHENTICATION,
ENFORCE_MULTI_FACTOR_AUTHENTICATION,
}
}
`;
@@ -89,6 +91,7 @@ export const UserDetailsQuery = `
roles
created_at
revoked_timestamp
is_multi_factor_auth_enabled
}
}
}
@@ -125,12 +128,13 @@ export const WebhooksDataQuery = `
export const EmailTemplatesQuery = `
query getEmailTemplates($params: PaginatedInput!) {
_email_templates(params: $params) {
EmailTemplates {
email_templates {
id
event_name
subject
created_at
template
design
}
pagination {
limit

View File

@@ -40,7 +40,7 @@ import {
UpdateModalViews,
EmailTemplateInputDataFields,
} from '../constants';
import { EmailTemplatesQuery, WebhooksDataQuery } from '../graphql/queries';
import { EmailTemplatesQuery } from '../graphql/queries';
import dayjs from 'dayjs';
import DeleteEmailTemplateModal from '../components/DeleteEmailTemplateModal';
@@ -58,6 +58,7 @@ interface EmailTemplateDataType {
[EmailTemplateInputDataFields.SUBJECT]: string;
[EmailTemplateInputDataFields.CREATED_AT]: number;
[EmailTemplateInputDataFields.TEMPLATE]: string;
[EmailTemplateInputDataFields.DESIGN]: string;
}
const EmailTemplates = () => {
@@ -94,7 +95,7 @@ const EmailTemplates = () => {
})
.toPromise();
if (res.data?._email_templates) {
const { pagination, EmailTemplates: emailTemplates } =
const { pagination, email_templates: emailTemplates } =
res.data?._email_templates;
const maxPages = getMaxPages(pagination);
if (emailTemplates?.length) {

View File

@@ -80,6 +80,8 @@ const Environment = () => {
DATABASE_TYPE: '',
DATABASE_URL: '',
ACCESS_TOKEN_EXPIRY_TIME: '',
DISABLE_MULTI_FACTOR_AUTHENTICATION: false,
ENFORCE_MULTI_FACTOR_AUTHENTICATION: false,
});
const [fieldVisibility, setFieldVisibility] = React.useState<

View File

@@ -68,6 +68,7 @@ interface userDataTypes {
roles: [string];
created_at: number;
revoked_timestamp: number;
is_multi_factor_auth_enabled?: boolean;
}
const enum updateAccessActions {
@@ -250,6 +251,34 @@ export default function Users() {
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 (
<Box m="5" py="5" px="10" bg="white" rounded="md">
@@ -273,6 +302,7 @@ export default function Users() {
<Th>Roles</Th>
<Th>Verified</Th>
<Th>Access</Th>
<Th>MFA</Th>
<Th>Actions</Th>
</Tr>
</Thead>
@@ -305,6 +335,19 @@ export default function Users() {
{user.revoked_timestamp ? 'Revoked' : 'Enabled'}
</Tag>
</Td>
<Td>
<Tag
size="sm"
variant="outline"
colorScheme={
user.is_multi_factor_auth_enabled ? 'green' : 'red'
}
>
{user.is_multi_factor_auth_enabled
? 'Enabled'
: 'Disabled'}
</Tag>
</Td>
<Td>
<Menu>
<MenuButton as={Button} variant="unstyled" size="sm">
@@ -357,6 +400,19 @@ export default function Users() {
Revoke Access
</MenuItem>
)}
{user.is_multi_factor_auth_enabled ? (
<MenuItem
onClick={() => multiFactorAuthUpdateHandler(user)}
>
Disable MFA
</MenuItem>
) : (
<MenuItem
onClick={() => multiFactorAuthUpdateHandler(user)}
>
Enable MFA
</MenuItem>
)}
</MenuList>
</Menu>
</Td>

View File

@@ -47,6 +47,8 @@ const (
EnvKeySmtpPassword = "SMTP_PASSWORD"
// EnvKeySenderEmail key for env variable SENDER_EMAIL
EnvKeySenderEmail = "SENDER_EMAIL"
// EnvKeyIsEmailServiceEnabled key for env variable IS_EMAIL_SERVICE_ENABLED
EnvKeyIsEmailServiceEnabled = "IS_EMAIL_SERVICE_ENABLED"
// EnvKeyJwtType key for env variable JWT_TYPE
EnvKeyJwtType = "JWT_TYPE"
// EnvKeyJwtSecret key for env variable JWT_SECRET
@@ -117,6 +119,12 @@ const (
EnvKeyDisableRedisForEnv = "DISABLE_REDIS_FOR_ENV"
// EnvKeyDisableStrongPassword key for env variable 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
// EnvKeyRoles key for env variable ROLES

View File

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

View File

@@ -14,6 +14,7 @@ type EmailTemplate struct {
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"`
Design string `gorm:"type:text" json:"design" bson:"design" cql:"design"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
}
@@ -29,6 +30,7 @@ func (e *EmailTemplate) AsAPIEmailTemplate() *model.EmailTemplate {
EventName: e.EventName,
Subject: e.Subject,
Template: e.Template,
Design: e.Design,
CreatedAt: refs.NewInt64Ref(e.CreatedAt),
UpdatedAt: refs.NewInt64Ref(e.UpdatedAt),
}

View File

@@ -9,6 +9,7 @@ type CollectionList struct {
Webhook string
WebhookLog string
EmailTemplate string
OTP string
}
var (
@@ -23,5 +24,6 @@ var (
Webhook: Prefix + "webhooks",
WebhookLog: Prefix + "webhook_logs",
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
import (
"encoding/json"
"strings"
"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
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"`
Email string `gorm:"unique" json:"email" bson:"email" cql:"email"`
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"`
SignupMethods string `json:"signup_methods" bson:"signup_methods" cql:"signup_methods"`
GivenName *string `json:"given_name" bson:"given_name" cql:"given_name"`
FamilyName *string `json:"family_name" bson:"family_name" cql:"family_name"`
MiddleName *string `json:"middle_name" bson:"middle_name" cql:"middle_name"`
Nickname *string `json:"nickname" bson:"nickname" cql:"nickname"`
Gender *string `json:"gender" bson:"gender" cql:"gender"`
Birthdate *string `json:"birthdate" bson:"birthdate" cql:"birthdate"`
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"`
Picture *string `gorm:"type:text" json:"picture" bson:"picture" cql:"picture"`
Roles string `json:"roles" bson:"roles" cql:"roles"`
RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp" cql:"revoked_timestamp"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
Email string `gorm:"unique" json:"email" bson:"email" cql:"email"`
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"`
SignupMethods string `json:"signup_methods" bson:"signup_methods" cql:"signup_methods"`
GivenName *string `json:"given_name" bson:"given_name" cql:"given_name"`
FamilyName *string `json:"family_name" bson:"family_name" cql:"family_name"`
MiddleName *string `json:"middle_name" bson:"middle_name" cql:"middle_name"`
Nickname *string `json:"nickname" bson:"nickname" cql:"nickname"`
Gender *string `json:"gender" bson:"gender" cql:"gender"`
Birthdate *string `json:"birthdate" bson:"birthdate" cql:"birthdate"`
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"`
Picture *string `gorm:"type:text" json:"picture" bson:"picture" cql:"picture"`
Roles string `json:"roles" bson:"roles" cql:"roles"`
RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp" cql:"revoked_timestamp"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled" bson:"is_multi_factor_auth_enabled" cql:"is_multi_factor_auth_enabled"`
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 {
isEmailVerified := user.EmailVerifiedAt != nil
isPhoneVerified := user.PhoneNumberVerifiedAt != nil
id := user.ID
if strings.Contains(id, Collections.WebhookLog+"/") {
id = strings.TrimPrefix(id, Collections.WebhookLog+"/")
}
// id := user.ID
// if strings.Contains(id, Collections.User+"/") {
// id = strings.TrimPrefix(id, Collections.User+"/")
// }
return &model.User{
ID: id,
Email: user.Email,
EmailVerified: isEmailVerified,
SignupMethods: user.SignupMethods,
GivenName: user.GivenName,
FamilyName: user.FamilyName,
MiddleName: user.MiddleName,
Nickname: user.Nickname,
PreferredUsername: refs.NewStringRef(user.Email),
Gender: user.Gender,
Birthdate: user.Birthdate,
PhoneNumber: user.PhoneNumber,
PhoneNumberVerified: &isPhoneVerified,
Picture: user.Picture,
Roles: strings.Split(user.Roles, ","),
RevokedTimestamp: user.RevokedTimestamp,
CreatedAt: refs.NewInt64Ref(user.CreatedAt),
UpdatedAt: refs.NewInt64Ref(user.UpdatedAt),
ID: user.ID,
Email: user.Email,
EmailVerified: isEmailVerified,
SignupMethods: user.SignupMethods,
GivenName: user.GivenName,
FamilyName: user.FamilyName,
MiddleName: user.MiddleName,
Nickname: user.Nickname,
PreferredUsername: refs.NewStringRef(user.Email),
Gender: user.Gender,
Birthdate: user.Birthdate,
PhoneNumber: user.PhoneNumber,
PhoneNumberVerified: &isPhoneVerified,
Picture: user.Picture,
Roles: strings.Split(user.Roles, ","),
RevokedTimestamp: user.RevokedTimestamp,
IsMultiFactorAuthEnabled: user.IsMultiFactorAuthEnabled,
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 {
id := v.ID
if strings.Contains(id, Collections.WebhookLog+"/") {
id = strings.TrimPrefix(id, Collections.WebhookLog+"/")
if strings.Contains(id, Collections.VerificationRequest+"/") {
id = strings.TrimPrefix(id, Collections.VerificationRequest+"/")
}
return &model.VerificationRequest{

View File

@@ -16,6 +16,7 @@ import (
func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate models.EmailTemplate) (*model.EmailTemplate, error) {
if emailTemplate.ID == "" {
emailTemplate.ID = uuid.New().String()
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) {
if env.ID == "" {
env.ID = uuid.New().String()
env.Key = env.ID
}
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,
})
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{
db: arangodb,
}, err

View File

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

View File

@@ -2,22 +2,26 @@ package arangodb
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"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/db/models"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid"
)
// AddUser to save user information in database
func (p *provider) AddUser(ctx context.Context, user models.User) (models.User, error) {
if user.ID == "" {
user.ID = uuid.New().String()
user.Key = user.ID
}
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)
bindVars := map[string]interface{}{
"user_id": user.ID,
"user_id": user.Key,
}
cursor, err := p.db.Query(ctx, query, bindVars)
if err != nil {
@@ -174,3 +178,36 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
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) {
if verificationRequest.ID == "" {
verificationRequest.ID = uuid.New().String()
verificationRequest.Key = verificationRequest.ID
}
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) {
if webhook.ID == "" {
webhook.ID = uuid.New().String()
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) {
if webhookLog.ID == "" {
webhookLog.ID = uuid.New().String()
webhookLog.Key = webhookLog.ID
}
webhookLog.Key = webhookLog.ID

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/gocql/gocql"
cansandraDriver "github.com/gocql/gocql"
log "github.com/sirupsen/logrus"
)
type provider struct {
@@ -99,6 +100,7 @@ func NewProvider() (*provider, error) {
cassandraClient.Consistency = gocql.LocalQuorum
cassandraClient.ConnectTimeout = 10 * time.Second
cassandraClient.ProtoVersion = 4
cassandraClient.Timeout = 30 * time.Minute // for large data
session, err := cassandraClient.CreateSession()
if err != nil {
@@ -159,6 +161,13 @@ func NewProvider() (*provider, error) {
if err != nil {
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
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)
@@ -221,6 +230,17 @@ func NewProvider() (*provider, error) {
return nil, err
}
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{
db: session,
}, err

View File

@@ -107,7 +107,7 @@ func (p *provider) UpdateUser(ctx context.Context, user models.User) (models.Use
}
if value == nil {
updateFields += fmt.Sprintf("%s = null,", key)
updateFields += fmt.Sprintf("%s = null, ", key)
continue
}
@@ -122,7 +122,6 @@ func (p *provider) UpdateUser(ctx context.Context, user models.User) (models.Use
updateFields = strings.TrimSuffix(updateFields, ",")
query := fmt.Sprintf("UPDATE %s SET %s WHERE id = '%s'", KeySpace+"."+models.Collections.User, updateFields, user.ID)
err = p.db.Query(query).Exec()
if err != nil {
return user, err
@@ -173,14 +172,14 @@ func (p *provider) ListUsers(ctx context.Context, pagination model.Pagination) (
// there is no offset in cassandra
// so we fetch till limit + offset
// 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()
counter := int64(0)
for scanner.Next() {
if counter >= pagination.Offset {
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 {
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
func (p *provider) GetUserByEmail(ctx context.Context, email string) (models.User, error) {
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)
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)
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.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
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
func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, error) {
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)
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)
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.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
return user, err
}
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())
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{
db: mongodb,
}, nil

View File

@@ -9,7 +9,9 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"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
}
// 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
}
// 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)
// GetUserByID to get user information from database using user ID
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
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)
// DeleteEmailTemplate to delete EmailTemplate
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{
TablePrefix: models.Prefix,
},
AllowGlobalUpdate: true,
}
dbType := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseType
@@ -60,7 +61,7 @@ func NewProvider() (*provider, error) {
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 {
return nil, err
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
@@ -121,3 +122,22 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
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 (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"strconv"
"text/template"
@@ -11,27 +11,75 @@ import (
gomail "gopkg.in/mail.v2"
"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"
)
// addEmailTemplate is used to add html template in email body
func addEmailTemplate(a string, b map[string]interface{}, templateName string) string {
tmpl, err := template.New(templateName).Parse(a)
if err != nil {
output, _ := json.Marshal(b)
return string(output)
func getDefaultTemplate(event string) *model.EmailTemplate {
switch event {
case constants.VerificationTypeBasicAuthSignup, constants.VerificationTypeMagicLinkLogin:
return &model.EmailTemplate{
Subject: emailVerificationSubject,
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 SendMail(to []string, Subject, bodyMessage string) error {
func getEmailTemplate(event string, data map[string]interface{}) (*model.EmailTemplate, 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
envKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyEnv)
if err != nil {
@@ -40,6 +88,13 @@ func SendMail(to []string, Subject, bodyMessage string) error {
if envKey == constants.TestEnv {
return nil
}
tmp, err := getEmailTemplate(event, data)
if err != nil {
log.Errorf("Failed to get event template: ", err)
return err
}
m := gomail.NewMessage()
senderEmail, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeySenderEmail)
if err != nil {
@@ -79,8 +134,8 @@ func SendMail(to []string, Subject, bodyMessage string) error {
m.SetHeader("From", senderEmail)
m.SetHeader("To", to...)
m.SetHeader("Subject", Subject)
m.SetBody("text/html", bodyMessage)
m.SetHeader("Subject", tmp.Subject)
m.SetBody("text/html", tmp.Template)
port, _ := strconv.Atoi(smtpPort)
d := gomail.NewDialer(smtpHost, port, smtpUsername, smtpPassword)
if !isProd {

View File

@@ -1,19 +1,8 @@
package email
import (
log "github.com/sirupsen/logrus"
"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 := `
const (
emailVerificationSubject = "Please verify your email"
emailVerificationTemplate = `
<!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>
@@ -98,23 +87,4 @@ func SendVerificationMail(toEmail, token, hostname string) error {
</body>
</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
import (
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/memorystore"
)
// 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 := `
const (
forgotPasswordSubject = "Reset Password"
forgotPasswordTemplate = `
<!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>
@@ -73,13 +53,13 @@ func SendForgotPasswordMail(toEmail, token, hostname string) error {
<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="{{.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 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>
<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>
</td>
</tr>
@@ -106,18 +86,4 @@ func SendForgotPasswordMail(toEmail, token, hostname string) error {
</body>
</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
import (
log "github.com/sirupsen/logrus"
"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 := `
const (
inviteEmailSubject = "Please accept the invitation"
inviteEmailTemplate = `
<!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>
@@ -64,13 +53,13 @@ func InviteEmail(toEmail, token, verificationURL, redirectURI string) error {
<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="{{.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 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>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
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>
@@ -98,23 +87,4 @@ func InviteEmail(toEmail, token, verificationURL, redirectURI string) error {
</body>
</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>
`
)

1
server/email/utils.go Normal file
View File

@@ -0,0 +1 @@
package email

41
server/env/env.go vendored
View File

@@ -84,6 +84,8 @@ func InitAllEnv() error {
osDisableSignUp := os.Getenv(constants.EnvKeyDisableSignUp)
osDisableRedisForEnv := os.Getenv(constants.EnvKeyDisableRedisForEnv)
osDisableStrongPassword := os.Getenv(constants.EnvKeyDisableStrongPassword)
osEnforceMultiFactorAuthentication := os.Getenv(constants.EnvKeyEnforceMultiFactorAuthentication)
osDisableMultiFactorAuthentication := os.Getenv(constants.EnvKeyDisableMultiFactorAuthentication)
// os slice vars
osAllowedOrigins := os.Getenv(constants.EnvKeyAllowedOrigins)
@@ -490,10 +492,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
if envData[constants.EnvKeySmtpHost] == "" || envData[constants.EnvKeySmtpUsername] == "" || envData[constants.EnvKeySmtpPassword] == "" || envData[constants.EnvKeySenderEmail] == "" && envData[constants.EnvKeySmtpPort] == "" {
envData[constants.EnvKeyDisableEmailVerification] = 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) {

View File

@@ -201,7 +201,7 @@ func PersistEnv() error {
envValue := strings.TrimSpace(os.Getenv(key))
if envValue != "" {
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 value.(bool) != envValueBool {
storeData[key] = envValueBool
@@ -221,6 +221,8 @@ func PersistEnv() error {
// handle derivative cases like disabling email verification & magic login
// 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] == "" {
storeData[constants.EnvKeyIsEmailServiceEnabled] = false
if !storeData[constants.EnvKeyDisableEmailVerification].(bool) {
storeData[constants.EnvKeyDisableEmailVerification] = true
hasChanged = true

View File

@@ -9,7 +9,7 @@ require (
github.com/gin-gonic/gin v1.7.2
github.com/go-playground/validator/v10 v10.8.0 // indirect
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/protobuf v1.5.2 // indirect
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-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
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.0.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8=
github.com/gocql/gocql v1.2.0 h1:TZhsCd7fRuye4VyHr3WCvWwIQaZUmjsqnSIXK9FcVCE=
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/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ type AddEmailTemplateRequest struct {
EventName string `json:"event_name"`
Subject string `json:"subject"`
Template string `json:"template"`
Design string `json:"design"`
}
type AddWebhookRequest struct {
@@ -24,12 +25,13 @@ type AdminSignupInput struct {
}
type AuthResponse struct {
Message string `json:"message"`
AccessToken *string `json:"access_token"`
IDToken *string `json:"id_token"`
RefreshToken *string `json:"refresh_token"`
ExpiresIn *int64 `json:"expires_in"`
User *User `json:"user"`
Message string `json:"message"`
ShouldShowOtpScreen *bool `json:"should_show_otp_screen"`
AccessToken *string `json:"access_token"`
IDToken *string `json:"id_token"`
RefreshToken *string `json:"refresh_token"`
ExpiresIn *int64 `json:"expires_in"`
User *User `json:"user"`
}
type DeleteEmailTemplateRequest struct {
@@ -44,6 +46,7 @@ type EmailTemplate struct {
ID string `json:"id"`
EventName string `json:"event_name"`
Template string `json:"template"`
Design string `json:"design"`
Subject string `json:"subject"`
CreatedAt *int64 `json:"created_at"`
UpdatedAt *int64 `json:"updated_at"`
@@ -51,58 +54,60 @@ type EmailTemplate struct {
type EmailTemplates struct {
Pagination *Pagination `json:"pagination"`
EmailTemplates []*EmailTemplate `json:"EmailTemplates"`
EmailTemplates []*EmailTemplate `json:"email_templates"`
}
type Env struct {
AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"`
AdminSecret *string `json:"ADMIN_SECRET"`
DatabaseName *string `json:"DATABASE_NAME"`
DatabaseURL *string `json:"DATABASE_URL"`
DatabaseType *string `json:"DATABASE_TYPE"`
DatabaseUsername *string `json:"DATABASE_USERNAME"`
DatabasePassword *string `json:"DATABASE_PASSWORD"`
DatabaseHost *string `json:"DATABASE_HOST"`
DatabasePort *string `json:"DATABASE_PORT"`
ClientID string `json:"CLIENT_ID"`
ClientSecret string `json:"CLIENT_SECRET"`
CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"`
SMTPHost *string `json:"SMTP_HOST"`
SMTPPort *string `json:"SMTP_PORT"`
SMTPUsername *string `json:"SMTP_USERNAME"`
SMTPPassword *string `json:"SMTP_PASSWORD"`
SenderEmail *string `json:"SENDER_EMAIL"`
JwtType *string `json:"JWT_TYPE"`
JwtSecret *string `json:"JWT_SECRET"`
JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"`
JwtPublicKey *string `json:"JWT_PUBLIC_KEY"`
AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
AppURL *string `json:"APP_URL"`
RedisURL *string `json:"REDIS_URL"`
ResetPasswordURL *string `json:"RESET_PASSWORD_URL"`
DisableEmailVerification bool `json:"DISABLE_EMAIL_VERIFICATION"`
DisableBasicAuthentication bool `json:"DISABLE_BASIC_AUTHENTICATION"`
DisableMagicLinkLogin bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
DisableLoginPage bool `json:"DISABLE_LOGIN_PAGE"`
DisableSignUp bool `json:"DISABLE_SIGN_UP"`
DisableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"`
DisableStrongPassword bool `json:"DISABLE_STRONG_PASSWORD"`
Roles []string `json:"ROLES"`
ProtectedRoles []string `json:"PROTECTED_ROLES"`
DefaultRoles []string `json:"DEFAULT_ROLES"`
JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"`
GoogleClientID *string `json:"GOOGLE_CLIENT_ID"`
GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"`
GithubClientID *string `json:"GITHUB_CLIENT_ID"`
GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"`
FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"`
FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"`
LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"`
LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"`
AppleClientID *string `json:"APPLE_CLIENT_ID"`
AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"`
AdminSecret *string `json:"ADMIN_SECRET"`
DatabaseName *string `json:"DATABASE_NAME"`
DatabaseURL *string `json:"DATABASE_URL"`
DatabaseType *string `json:"DATABASE_TYPE"`
DatabaseUsername *string `json:"DATABASE_USERNAME"`
DatabasePassword *string `json:"DATABASE_PASSWORD"`
DatabaseHost *string `json:"DATABASE_HOST"`
DatabasePort *string `json:"DATABASE_PORT"`
ClientID string `json:"CLIENT_ID"`
ClientSecret string `json:"CLIENT_SECRET"`
CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"`
SMTPHost *string `json:"SMTP_HOST"`
SMTPPort *string `json:"SMTP_PORT"`
SMTPUsername *string `json:"SMTP_USERNAME"`
SMTPPassword *string `json:"SMTP_PASSWORD"`
SenderEmail *string `json:"SENDER_EMAIL"`
JwtType *string `json:"JWT_TYPE"`
JwtSecret *string `json:"JWT_SECRET"`
JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"`
JwtPublicKey *string `json:"JWT_PUBLIC_KEY"`
AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
AppURL *string `json:"APP_URL"`
RedisURL *string `json:"REDIS_URL"`
ResetPasswordURL *string `json:"RESET_PASSWORD_URL"`
DisableEmailVerification bool `json:"DISABLE_EMAIL_VERIFICATION"`
DisableBasicAuthentication bool `json:"DISABLE_BASIC_AUTHENTICATION"`
DisableMagicLinkLogin bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
DisableLoginPage bool `json:"DISABLE_LOGIN_PAGE"`
DisableSignUp bool `json:"DISABLE_SIGN_UP"`
DisableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"`
DisableStrongPassword bool `json:"DISABLE_STRONG_PASSWORD"`
DisableMultiFactorAuthentication bool `json:"DISABLE_MULTI_FACTOR_AUTHENTICATION"`
EnforceMultiFactorAuthentication bool `json:"ENFORCE_MULTI_FACTOR_AUTHENTICATION"`
Roles []string `json:"ROLES"`
ProtectedRoles []string `json:"PROTECTED_ROLES"`
DefaultRoles []string `json:"DEFAULT_ROLES"`
JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"`
GoogleClientID *string `json:"GOOGLE_CLIENT_ID"`
GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"`
GithubClientID *string `json:"GITHUB_CLIENT_ID"`
GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"`
FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"`
FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"`
LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"`
LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"`
AppleClientID *string `json:"APPLE_CLIENT_ID"`
AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
}
type Error struct {
@@ -164,6 +169,7 @@ type Meta struct {
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
IsSignUpEnabled bool `json:"is_sign_up_enabled"`
IsStrongPasswordEnabled bool `json:"is_strong_password_enabled"`
IsMultiFactorAuthEnabled bool `json:"is_multi_factor_auth_enabled"`
}
type OAuthRevokeInput struct {
@@ -186,6 +192,10 @@ type PaginationInput struct {
Page *int64 `json:"page"`
}
type ResendOTPRequest struct {
Email string `json:"email"`
}
type ResendVerifyEmailInput struct {
Email string `json:"email"`
Identifier string `json:"identifier"`
@@ -207,20 +217,21 @@ type SessionQueryInput struct {
}
type SignUpInput struct {
Email string `json:"email"`
GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"`
Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"`
Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"`
Roles []string `json:"roles"`
Scope []string `json:"scope"`
RedirectURI *string `json:"redirect_uri"`
Email string `json:"email"`
GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"`
Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"`
Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"`
Roles []string `json:"roles"`
Scope []string `json:"scope"`
RedirectURI *string `json:"redirect_uri"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
}
type TestEndpointRequest struct {
@@ -243,78 +254,83 @@ type UpdateEmailTemplateRequest struct {
EventName *string `json:"event_name"`
Template *string `json:"template"`
Subject *string `json:"subject"`
Design *string `json:"design"`
}
type UpdateEnvInput struct {
AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"`
AdminSecret *string `json:"ADMIN_SECRET"`
CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"`
OldAdminSecret *string `json:"OLD_ADMIN_SECRET"`
SMTPHost *string `json:"SMTP_HOST"`
SMTPPort *string `json:"SMTP_PORT"`
SMTPUsername *string `json:"SMTP_USERNAME"`
SMTPPassword *string `json:"SMTP_PASSWORD"`
SenderEmail *string `json:"SENDER_EMAIL"`
JwtType *string `json:"JWT_TYPE"`
JwtSecret *string `json:"JWT_SECRET"`
JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"`
JwtPublicKey *string `json:"JWT_PUBLIC_KEY"`
AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
AppURL *string `json:"APP_URL"`
ResetPasswordURL *string `json:"RESET_PASSWORD_URL"`
DisableEmailVerification *bool `json:"DISABLE_EMAIL_VERIFICATION"`
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
DisableRedisForEnv *bool `json:"DISABLE_REDIS_FOR_ENV"`
DisableStrongPassword *bool `json:"DISABLE_STRONG_PASSWORD"`
Roles []string `json:"ROLES"`
ProtectedRoles []string `json:"PROTECTED_ROLES"`
DefaultRoles []string `json:"DEFAULT_ROLES"`
JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"`
GoogleClientID *string `json:"GOOGLE_CLIENT_ID"`
GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"`
GithubClientID *string `json:"GITHUB_CLIENT_ID"`
GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"`
FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"`
FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"`
LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"`
LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"`
AppleClientID *string `json:"APPLE_CLIENT_ID"`
AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"`
AdminSecret *string `json:"ADMIN_SECRET"`
CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"`
OldAdminSecret *string `json:"OLD_ADMIN_SECRET"`
SMTPHost *string `json:"SMTP_HOST"`
SMTPPort *string `json:"SMTP_PORT"`
SMTPUsername *string `json:"SMTP_USERNAME"`
SMTPPassword *string `json:"SMTP_PASSWORD"`
SenderEmail *string `json:"SENDER_EMAIL"`
JwtType *string `json:"JWT_TYPE"`
JwtSecret *string `json:"JWT_SECRET"`
JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"`
JwtPublicKey *string `json:"JWT_PUBLIC_KEY"`
AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
AppURL *string `json:"APP_URL"`
ResetPasswordURL *string `json:"RESET_PASSWORD_URL"`
DisableEmailVerification *bool `json:"DISABLE_EMAIL_VERIFICATION"`
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
DisableRedisForEnv *bool `json:"DISABLE_REDIS_FOR_ENV"`
DisableStrongPassword *bool `json:"DISABLE_STRONG_PASSWORD"`
DisableMultiFactorAuthentication *bool `json:"DISABLE_MULTI_FACTOR_AUTHENTICATION"`
EnforceMultiFactorAuthentication *bool `json:"ENFORCE_MULTI_FACTOR_AUTHENTICATION"`
Roles []string `json:"ROLES"`
ProtectedRoles []string `json:"PROTECTED_ROLES"`
DefaultRoles []string `json:"DEFAULT_ROLES"`
JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"`
GoogleClientID *string `json:"GOOGLE_CLIENT_ID"`
GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"`
GithubClientID *string `json:"GITHUB_CLIENT_ID"`
GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"`
FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"`
FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"`
LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"`
LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"`
AppleClientID *string `json:"APPLE_CLIENT_ID"`
AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
}
type UpdateProfileInput struct {
OldPassword *string `json:"old_password"`
NewPassword *string `json:"new_password"`
ConfirmNewPassword *string `json:"confirm_new_password"`
Email *string `json:"email"`
GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"`
Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"`
OldPassword *string `json:"old_password"`
NewPassword *string `json:"new_password"`
ConfirmNewPassword *string `json:"confirm_new_password"`
Email *string `json:"email"`
GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"`
Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
}
type UpdateUserInput struct {
ID string `json:"id"`
Email *string `json:"email"`
EmailVerified *bool `json:"email_verified"`
GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"`
Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"`
Roles []*string `json:"roles"`
ID string `json:"id"`
Email *string `json:"email"`
EmailVerified *bool `json:"email_verified"`
GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"`
Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"`
Roles []*string `json:"roles"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
}
type UpdateWebhookRequest struct {
@@ -326,24 +342,25 @@ type UpdateWebhookRequest struct {
}
type User struct {
ID string `json:"id"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
SignupMethods string `json:"signup_methods"`
GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"`
PreferredUsername *string `json:"preferred_username"`
Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
PhoneNumberVerified *bool `json:"phone_number_verified"`
Picture *string `json:"picture"`
Roles []string `json:"roles"`
CreatedAt *int64 `json:"created_at"`
UpdatedAt *int64 `json:"updated_at"`
RevokedTimestamp *int64 `json:"revoked_timestamp"`
ID string `json:"id"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
SignupMethods string `json:"signup_methods"`
GivenName *string `json:"given_name"`
FamilyName *string `json:"family_name"`
MiddleName *string `json:"middle_name"`
Nickname *string `json:"nickname"`
PreferredUsername *string `json:"preferred_username"`
Gender *string `json:"gender"`
Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"`
PhoneNumberVerified *bool `json:"phone_number_verified"`
Picture *string `json:"picture"`
Roles []string `json:"roles"`
CreatedAt *int64 `json:"created_at"`
UpdatedAt *int64 `json:"updated_at"`
RevokedTimestamp *int64 `json:"revoked_timestamp"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
}
type Users struct {
@@ -382,6 +399,11 @@ type VerifyEmailInput struct {
Token string `json:"token"`
}
type VerifyOTPRequest struct {
Email string `json:"email"`
Otp string `json:"otp"`
}
type Webhook struct {
ID string `json:"id"`
EventName *string `json:"event_name"`

View File

@@ -25,6 +25,7 @@ type Meta {
is_magic_link_login_enabled: Boolean!
is_sign_up_enabled: Boolean!
is_strong_password_enabled: Boolean!
is_multi_factor_auth_enabled: Boolean!
}
type User {
@@ -47,6 +48,7 @@ type User {
created_at: Int64
updated_at: Int64
revoked_timestamp: Int64
is_multi_factor_auth_enabled: Boolean
}
type Users {
@@ -78,6 +80,7 @@ type Error {
type AuthResponse {
message: String!
should_show_otp_screen: Boolean
access_token: String
id_token: String
refresh_token: String
@@ -122,6 +125,8 @@ type Env {
DISABLE_SIGN_UP: Boolean!
DISABLE_REDIS_FOR_ENV: Boolean!
DISABLE_STRONG_PASSWORD: Boolean!
DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean!
ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean!
ROLES: [String!]
PROTECTED_ROLES: [String!]
DEFAULT_ROLES: [String!]
@@ -189,6 +194,7 @@ type EmailTemplate {
id: ID!
event_name: String!
template: String!
design: String!
subject: String!
created_at: Int64
updated_at: Int64
@@ -196,7 +202,7 @@ type EmailTemplate {
type EmailTemplates {
pagination: Pagination!
EmailTemplates: [EmailTemplate!]!
email_templates: [EmailTemplate!]!
}
input UpdateEnvInput {
@@ -223,6 +229,8 @@ input UpdateEnvInput {
DISABLE_SIGN_UP: Boolean
DISABLE_REDIS_FOR_ENV: Boolean
DISABLE_STRONG_PASSWORD: Boolean
DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean
ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean
ROLES: [String!]
PROTECTED_ROLES: [String!]
DEFAULT_ROLES: [String!]
@@ -264,6 +272,7 @@ input SignUpInput {
roles: [String!]
scope: [String!]
redirect_uri: String
is_multi_factor_auth_enabled: Boolean
}
input LoginInput {
@@ -295,6 +304,7 @@ input UpdateProfileInput {
birthdate: String
phone_number: String
picture: String
is_multi_factor_auth_enabled: Boolean
}
input UpdateUserInput {
@@ -310,6 +320,7 @@ input UpdateUserInput {
phone_number: String
picture: String
roles: [String]
is_multi_factor_auth_enabled: Boolean
}
input ForgotPasswordInput {
@@ -407,6 +418,7 @@ input AddEmailTemplateRequest {
event_name: String!
subject: String!
template: String!
design: String!
}
input UpdateEmailTemplateRequest {
@@ -414,12 +426,22 @@ input UpdateEmailTemplateRequest {
event_name: String
template: String
subject: String
design: String
}
input DeleteEmailTemplateRequest {
id: ID!
}
input VerifyOTPRequest {
email: String!
otp: String!
}
input ResendOTPRequest {
email: String!
}
type Mutation {
signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse!
@@ -431,6 +453,8 @@ type Mutation {
forgot_password(params: ForgotPasswordInput!): Response!
reset_password(params: ResetPasswordInput!): Response!
revoke(params: OAuthRevokeInput!): Response!
verify_otp(params: VerifyOTPRequest!): AuthResponse!
resend_otp(params: ResendOTPRequest!): Response!
# admin only apis
_delete_user(params: DeleteUserInput!): Response!
_update_user(params: UpdateUserInput!): User!

View File

@@ -51,6 +51,14 @@ func (r *mutationResolver) Revoke(ctx context.Context, params model.OAuthRevokeI
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) {
return resolvers.DeleteUserResolver(ctx, params)
}

View File

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

View File

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

View File

@@ -160,7 +160,7 @@ func (c *provider) GetEnvStore() (map[string]interface{}, error) {
return nil, err
}
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)
if err != nil {
return res, err

View File

@@ -14,8 +14,6 @@ import (
log "github.com/sirupsen/logrus"
)
// TODO add template validator
// AddEmailTemplateResolver resolver for add email template mutation
func AddEmailTemplateResolver(ctx context.Context, params model.AddEmailTemplateRequest) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx)
@@ -42,10 +40,15 @@ func AddEmailTemplateResolver(ctx context.Context, params model.AddEmailTemplate
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{
EventName: params.EventName,
Template: params.Template,
Subject: params.Subject,
Design: params.Design,
})
if err != nil {
log.Debug("Failed to add email template: ", err)

View File

@@ -170,6 +170,8 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
res.DisableLoginPage = store[constants.EnvKeyDisableLoginPage].(bool)
res.DisableSignUp = store[constants.EnvKeyDisableSignUp].(bool)
res.DisableStrongPassword = store[constants.EnvKeyDisableStrongPassword].(bool)
res.EnforceMultiFactorAuthentication = store[constants.EnvKeyEnforceMultiFactorAuthentication].(bool)
res.DisableMultiFactorAuthentication = store[constants.EnvKeyDisableMultiFactorAuthentication].(bool)
return res, nil
}

View File

@@ -49,7 +49,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
log := log.WithFields(log.Fields{
"email": params.Email,
})
_, err = db.Provider.GetUserByEmail(ctx, params.Email)
user, err := db.Provider.GetUserByEmail(ctx, params.Email)
if err != nil {
log.Debug("User not found: ", err)
return res, fmt.Errorf(`user with this email not found`)
@@ -84,8 +84,12 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
return res, err
}
// exec it as go routin so that we can reduce the api latency
go email.SendForgotPasswordMail(params.Email, verificationToken, hostname)
// exec it as go routine so that we can reduce the api latency
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{
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/memorystore"
"github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
"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
isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification)
EnvKeyIsEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil {
log.Debug("Error getting email verification disabled: ", err)
isEmailVerificationDisabled = true
EnvKeyIsEmailServiceEnabled = false
}
if isEmailVerificationDisabled {
if !EnvKeyIsEmailServiceEnabled {
log.Debug("Email server is not configured")
return nil, errors.New("email sending is disabled")
}
@@ -114,7 +115,7 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput)
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 {
log.Debug("Failed to create verification token: ", err)
}
@@ -134,8 +135,17 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput)
} else {
// use basic authentication if that option is on
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"
}
@@ -152,7 +162,12 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput)
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{

View File

@@ -13,8 +13,10 @@ import (
"github.com/authorizerdev/authorizer/server/cookie"
"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/token"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators"
@@ -97,6 +99,47 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
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)
if err != nil {
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
}
// exec it as go routing so that we can reduce the api latency
go email.SendVerificationMail(params.Email, verificationToken, hostname)
// exec it as go routine so that we can reduce the api latency
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{

View File

@@ -107,6 +107,12 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) {
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{
Version: constants.VERSION,
ClientID: clientID,
@@ -120,6 +126,7 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) {
IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled,
IsSignUpEnabled: !isSignUpDisabled,
IsStrongPasswordEnabled: !isStrongPasswordDisabled,
IsMultiFactorAuthEnabled: !isMultiFactorAuthenticationEnabled,
}
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")
}
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)
if err != nil {
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)
}
// exec it as go routin so that we can reduce the api latency
go email.SendVerificationMail(params.Email, verificationToken, hostname)
// exec it as go routine so that we can reduce the api latency
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{
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/memorystore"
"github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators"
@@ -84,6 +85,16 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput)
signupMethod := user.SignupMethods
if !strings.Contains(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

View File

@@ -17,6 +17,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators"
@@ -157,6 +158,20 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
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
isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification)
if err != nil {
@@ -206,9 +221,14 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
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() {
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)
}()

View File

@@ -15,8 +15,6 @@ import (
log "github.com/sirupsen/logrus"
)
// TODO add template validator
// UpdateEmailTemplateResolver resolver for update email template mutation
func UpdateEmailTemplateResolver(ctx context.Context, params model.UpdateEmailTemplateRequest) (*model.Response, error) {
gc, err := utils.GinContextFromContext(ctx)
@@ -67,6 +65,14 @@ func UpdateEmailTemplateResolver(ctx context.Context, params model.UpdateEmailTe
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)
if err != nil {
return nil, err

View File

@@ -234,6 +234,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
// handle derivative cases like disabling email verification & magic login
// 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] == "" {
updatedData[constants.EnvKeyIsEmailServiceEnabled] = false
updatedData[constants.EnvKeyDisableMultiFactorAuthentication] = true
if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) {
updatedData[constants.EnvKeyDisableEmailVerification] = true
}
@@ -243,6 +245,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
if len(params.Roles) > 0 {
if len(params.DefaultRoles) > 0 {
@@ -265,8 +277,6 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
}
}
go clearSessionIfRequired(currentData, updatedData)
// Update local store
memorystore.Provider.UpdateEnvStore(updatedData)
jwk, err := crypto.GenerateJWKBasedOnEnv()
@@ -320,6 +330,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
return res, err
}
go clearSessionIfRequired(currentData, updatedData)
res = &model.Response{
Message: "configurations updated successfully",
}

View File

@@ -2,6 +2,7 @@ package resolvers
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -46,7 +47,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
}
// 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")
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
}
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
if params.NewPassword != nil && params.ConfirmNewPassword == nil {
isPasswordChanging = true
@@ -220,8 +244,12 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
return res, err
}
// exec it as go routin so that we can reduce the api latency
go email.SendVerificationMail(newEmail, verificationToken, hostname)
// exec it as go routine so that we can reduce the api latency
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

@@ -2,6 +2,7 @@ package resolvers
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -15,6 +16,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators"
@@ -45,7 +47,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
"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")
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`)
}
if params.GivenName != nil && user.GivenName != params.GivenName {
if params.GivenName != nil && refs.StringValue(user.GivenName) != refs.StringValue(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
}
if params.MiddleName != nil && user.MiddleName != params.MiddleName {
if params.MiddleName != nil && refs.StringValue(user.MiddleName) != refs.StringValue(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
}
if params.Birthdate != nil && user.Birthdate != params.Birthdate {
if params.Birthdate != nil && refs.StringValue(user.Birthdate) != refs.StringValue(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
}
if params.PhoneNumber != nil && user.PhoneNumber != params.PhoneNumber {
if params.PhoneNumber != nil && refs.StringValue(user.PhoneNumber) != refs.StringValue(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
}
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 {
now := time.Now().Unix()
@@ -143,8 +156,12 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
return res, err
}
// exec it as go routin so that we can reduce the api latency
go email.SendVerificationMail(newEmail, verificationToken, hostname)
// exec it as go routine so that we can reduce the api latency
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

@@ -0,0 +1,99 @@
package test
import (
"context"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func resendOTPTest(t *testing.T, s TestSetup) {
t.Helper()
t.Run(`should resend otp`, func(t *testing.T) {
req, ctx := createContext(s)
email := "resend_otp." + s.TestInfo.Email
res, err := resolvers.SignupResolver(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
assert.NoError(t, err)
assert.NotNil(t, res)
// Login should fail as email is not verified
loginRes, err := resolvers.LoginResolver(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.Error(t, err)
assert.Nil(t, loginRes)
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup)
assert.Nil(t, err)
assert.Equal(t, email, verificationRequest.Email)
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
Token: verificationRequest.Token,
})
assert.Nil(t, err)
assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty")
// Using access token update profile
s.GinContext.Request.Header.Set("Authorization", "Bearer "+refs.StringValue(verifyRes.AccessToken))
ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext)
_, err = resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{
IsMultiFactorAuthEnabled: refs.NewBoolRef(true),
})
// Resend otp should return error as no initial opt is being sent
resendOtpRes, err := resolvers.ResendOTPResolver(ctx, model.ResendOTPRequest{
Email: email,
})
assert.Error(t, err)
assert.Nil(t, resendOtpRes)
// Login should not return error but access token should be empty as otp should have been sent
loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.NoError(t, err)
assert.NotNil(t, loginRes)
assert.Nil(t, loginRes.AccessToken)
// Get otp from db
otp, err := db.Provider.GetOTPByEmail(ctx, email)
assert.NoError(t, err)
assert.NotEmpty(t, otp.Otp)
// resend otp
resendOtpRes, err = resolvers.ResendOTPResolver(ctx, model.ResendOTPRequest{
Email: email,
})
assert.NoError(t, err)
assert.NotEmpty(t, resendOtpRes.Message)
newOtp, err := db.Provider.GetOTPByEmail(ctx, email)
assert.NoError(t, err)
assert.NotEmpty(t, newOtp.Otp)
assert.NotEqual(t, otp.Otp, newOtp)
// Should return error for older otp
verifyOtpRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{
Email: email,
Otp: otp.Otp,
})
assert.Error(t, err)
verifyOtpRes, err = resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{
Email: email,
Otp: newOtp.Otp,
})
assert.NoError(t, err)
assert.NotEqual(t, verifyOtpRes.AccessToken, "", "access token should not be empty")
cleanData(email)
})
}

View File

@@ -33,7 +33,7 @@ func TestResolvers(t *testing.T) {
if utils.StringSliceContains(testDBs, constants.DbTypeSqlite) && len(testDBs) == 1 {
// do nothing
} else {
t.Log("waiting for docker containers to spun up")
t.Log("waiting for docker containers to start...")
// wait for docker containers to spun up
time.Sleep(30 * time.Second)
}
@@ -114,7 +114,10 @@ func TestResolvers(t *testing.T) {
metaTests(t, s)
inviteUserTest(t, s)
validateJwtTokenTest(t, s)
verifyOTPTest(t, s)
resendOTPTest(t, s)
updateAllUsersTest(t, s)
webhookLogsTest(t, s) // get logs after above resolver tests are done
deleteWebhookTest(t, s) // delete webhooks (admin resolver)
})

View File

@@ -57,6 +57,11 @@ func cleanData(email string) {
err = db.Provider.DeleteVerificationRequest(ctx, verificationRequest)
}
otp, err := db.Provider.GetOTPByEmail(ctx, email)
if err == nil {
err = db.Provider.DeleteOTP(ctx, otp)
}
dbUser, err := db.Provider.GetUserByEmail(ctx, email)
if err == nil {
db.Provider.DeleteUser(ctx, dbUser)

View File

@@ -0,0 +1,67 @@
package test
import (
"fmt"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"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/refs"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert"
)
func updateAllUsersTest(t *testing.T, s TestSetup) {
t.Helper()
t.Run("Should update all users", func(t *testing.T) {
_, ctx := createContext(s)
users := []models.User{}
for i := 0; i < 10; i++ {
user := models.User{
Email: fmt.Sprintf("update_all_user_%d_%s", i, s.TestInfo.Email),
SignupMethods: constants.AuthRecipeMethodBasicAuth,
Roles: "user",
}
users = append(users, user)
u, err := db.Provider.AddUser(ctx, user)
assert.NoError(t, err)
assert.NotNil(t, u)
}
err := db.Provider.UpdateUsers(ctx, map[string]interface{}{
"is_multi_factor_auth_enabled": true,
}, nil)
assert.NoError(t, err)
listUsers, err := db.Provider.ListUsers(ctx, model.Pagination{
Limit: 20,
Offset: 0,
})
assert.NoError(t, err)
for _, u := range listUsers.Users {
assert.True(t, refs.BoolValue(u.IsMultiFactorAuthEnabled))
}
// // update few users
updateIds := []string{listUsers.Users[0].ID, listUsers.Users[1].ID}
err = db.Provider.UpdateUsers(ctx, map[string]interface{}{
"is_multi_factor_auth_enabled": false,
}, updateIds)
assert.NoError(t, err)
listUsers, err = db.Provider.ListUsers(ctx, model.Pagination{
Limit: 20,
Offset: 0,
})
for _, u := range listUsers.Users {
if utils.StringSliceContains(updateIds, u.ID) {
assert.False(t, refs.BoolValue(u.IsMultiFactorAuthEnabled))
} else {
assert.True(t, refs.BoolValue(u.IsMultiFactorAuthEnabled))
}
}
})
}

View File

@@ -0,0 +1,75 @@
package test
import (
"context"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func verifyOTPTest(t *testing.T, s TestSetup) {
t.Helper()
t.Run(`should verify otp`, func(t *testing.T) {
req, ctx := createContext(s)
email := "verify_otp." + s.TestInfo.Email
res, err := resolvers.SignupResolver(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
assert.NoError(t, err)
assert.NotNil(t, res)
// Login should fail as email is not verified
loginRes, err := resolvers.LoginResolver(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.Error(t, err)
assert.Nil(t, loginRes)
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup)
assert.Nil(t, err)
assert.Equal(t, email, verificationRequest.Email)
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
Token: verificationRequest.Token,
})
assert.Nil(t, err)
assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty")
// Using access token update profile
s.GinContext.Request.Header.Set("Authorization", "Bearer "+refs.StringValue(verifyRes.AccessToken))
ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext)
updateProfileRes, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{
IsMultiFactorAuthEnabled: refs.NewBoolRef(true),
})
assert.NoError(t, err)
assert.NotEmpty(t, updateProfileRes.Message)
// Login should not return error but access token should be empty as otp should have been sent
loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.NoError(t, err)
assert.NotNil(t, loginRes)
assert.Nil(t, loginRes.AccessToken)
// Get otp from db
otp, err := db.Provider.GetOTPByEmail(ctx, email)
assert.NoError(t, err)
assert.NotEmpty(t, otp.Otp)
verifyOtpRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{
Email: email,
Otp: otp.Otp,
})
assert.Nil(t, err)
assert.NotEqual(t, verifyOtpRes.AccessToken, "", "access token should not be empty")
cleanData(email)
})
}

View File

@@ -2,6 +2,9 @@ package utils
import (
"reflect"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/memorystore"
)
// StringSliceContains checks if a string slice contains a particular string
@@ -58,3 +61,46 @@ func ConvertInterfaceToStringSlice(slice interface{}) []string {
}
return resSlice
}
// GetOrganization to get organization object
func GetOrganization() map[string]interface{} {
orgLogo, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
if err != nil {
return nil
}
orgName, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
if err != nil {
return nil
}
organization := map[string]interface{}{
"name": orgName,
"logo": orgLogo,
}
return organization
}
// GetForgotPasswordURL to get url for given token and hostname
func GetForgotPasswordURL(token, hostname string) string {
resetPasswordUrl, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyResetPasswordURL)
if err != nil {
return ""
}
if resetPasswordUrl == "" {
if err := memorystore.Provider.UpdateEnvVariable(constants.EnvKeyResetPasswordURL, hostname+"/app/reset-password"); err != nil {
return ""
}
}
verificationURL := resetPasswordUrl + "?token=" + token
return verificationURL
}
// GetInviteVerificationURL to get url for invite email verification
func GetInviteVerificationURL(verificationURL, token, redirectURI string) string {
return verificationURL + "?token=" + token + "&redirect_uri=" + redirectURI
}
// GetEmailVerificationURL to get url for invite email verification
func GetEmailVerificationURL(token, hostname string) string {
return hostname + "/verify_email?token=" + token
}

View File

@@ -0,0 +1,25 @@
package utils
import (
"math/rand"
"time"
)
// GenerateOTP to generate random 6 digit otp
func GenerateOTP() string {
code := ""
codeLength := 6
charSet := "ABCDEFGHJKLMNPQRSTUVWXYZ123456789"
charSetLength := int32(len(charSet))
for i := 0; i < codeLength; i++ {
index := randomNumber(0, charSetLength)
code += string(charSet[index])
}
return code
}
func randomNumber(min, max int32) int32 {
rand.Seed(time.Now().UnixNano())
return min + int32(rand.Intn(int(max-min)))
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin"
)
// TODO renamae GinContextKey -> GinContext
// TODO re-name GinContextKey -> GinContext
// GinContext to get gin context from context
func GinContextFromContext(ctx context.Context) (*gin.Context, error) {

View File

@@ -11,6 +11,7 @@ import (
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/refs"
log "github.com/sirupsen/logrus"
)
@@ -52,6 +53,22 @@ func RegisterEvent(ctx context.Context, eventName string, authRecipe string, use
return err
}
// dont trigger webhook call in case of test
envKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyEnv)
if err != nil {
return err
}
if envKey == constants.TestEnv {
db.Provider.AddWebhookLog(ctx, models.WebhookLog{
HttpStatus: 200,
Request: string(requestBody),
Response: string(`{"message": "test"}`),
WebhookID: webhook.ID,
})
return nil
}
requestBytesBuffer := bytes.NewBuffer(requestBody)
req, err := http.NewRequest("POST", refs.StringValue(webhook.Endpoint), requestBytesBuffer)
if err != nil {

View File

@@ -4,7 +4,7 @@ import "github.com/authorizerdev/authorizer/server/constants"
// IsValidEmailTemplateEventName function to validate email template events
func IsValidEmailTemplateEventName(eventName string) bool {
if eventName != constants.VerificationTypeBasicAuthSignup && eventName != constants.VerificationTypeForgotPassword && eventName != constants.VerificationTypeMagicLinkLogin && eventName != constants.VerificationTypeUpdateEmail {
if eventName != constants.VerificationTypeBasicAuthSignup && eventName != constants.VerificationTypeForgotPassword && eventName != constants.VerificationTypeMagicLinkLogin && eventName != constants.VerificationTypeUpdateEmail && eventName != constants.VerificationTypeOTP && eventName != constants.VerificationTypeInviteMember {
return false
}