@redmix/auth-supertokens-setup 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/setup.d.ts +13 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +53 -0
- package/dist/setupHandler.d.ts +7 -0
- package/dist/setupHandler.d.ts.map +1 -0
- package/dist/setupHandler.js +101 -0
- package/dist/templates/api/functions/auth.ts.template +8 -0
- package/dist/templates/api/lib/auth.ts.template +128 -0
- package/dist/templates/api/lib/supertokens.ts.template +75 -0
- package/dist/templates/web/auth.rsc.tsx.template +63 -0
- package/dist/templates/web/auth.tsx.template +61 -0
- package/package.json +59 -0
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Redmix
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# Authentication
|
2
|
+
|
3
|
+
## Contributing
|
4
|
+
|
5
|
+
If you want to contribute a new auth provider integration we recommend you
|
6
|
+
start by implementing it as a custom auth provider in a Redwood App first. When
|
7
|
+
that works you can package it up as an npm package and publish it on your own.
|
8
|
+
You can then create a PR on this repo with support for your new auth provider
|
9
|
+
in our `yarn rw setup auth` cli command. The easiest option is probably to just
|
10
|
+
look at one of the existing auth providers in
|
11
|
+
`packages/cli/src/commands/setup/auth/providers` and the corresponding
|
12
|
+
templates in `../templates`.
|
13
|
+
|
14
|
+
If you need help setting up a custom auth provider you can read the auth docs
|
15
|
+
on the web.
|
16
|
+
|
17
|
+
### Contributing to the base auth implementation
|
18
|
+
|
19
|
+
If you want to contribute to our auth implementation, the interface towards
|
20
|
+
both auth service providers and RW apps we recommend you start looking in
|
21
|
+
`authFactory.ts` and then continue to `AuthProvider.tsx`. `AuthProvider.tsx`
|
22
|
+
has most of our implementation together with all the custom hooks it uses.
|
23
|
+
Another file to be accustomed with is `AuthContext.ts`. The interface in there
|
24
|
+
has pretty good code comments, and is what will be exposed to RW apps.
|
25
|
+
|
26
|
+
## getCurrentUser
|
27
|
+
|
28
|
+
`getCurrentUser` returns the user information together with
|
29
|
+
an optional collection of roles used by requireAuth() to check if the user is authenticated or has role-based access.
|
30
|
+
|
31
|
+
Use in conjunction with `requireAuth` in your services to check that a user is logged in, whether or not they are assigned a role, and optionally raise an error if they're not.
|
32
|
+
|
33
|
+
```js
|
34
|
+
@param decoded - The decoded access token containing user info and JWT claims like `sub`
|
35
|
+
@param { token, SupportedAuthTypes type } - The access token itself as well as the auth provider type
|
36
|
+
@param { APIGatewayEvent event, Context context } - An object which contains information from the invoker
|
37
|
+
such as headers and cookies, and the context information about the invocation such as IP Address
|
38
|
+
```
|
39
|
+
|
40
|
+
### Examples
|
41
|
+
|
42
|
+
#### Checks if currentUser is authenticated
|
43
|
+
|
44
|
+
This example is the standard use of `getCurrentUser`.
|
45
|
+
|
46
|
+
```js
|
47
|
+
export const getCurrentUser = async (
|
48
|
+
decoded,
|
49
|
+
{ _token, _type },
|
50
|
+
{ _event, _context },
|
51
|
+
) => {
|
52
|
+
return { ...decoded, roles: parseJWT({ decoded }).roles }
|
53
|
+
}
|
54
|
+
```
|
55
|
+
|
56
|
+
#### User details fetched via database query
|
57
|
+
|
58
|
+
```js
|
59
|
+
export const getCurrentUser = async (decoded) => {
|
60
|
+
return await db.user.findUnique({ where: { decoded.email } })
|
61
|
+
}
|
62
|
+
```
|
63
|
+
|
64
|
+
#### User info is decoded from the access token
|
65
|
+
|
66
|
+
```js
|
67
|
+
export const getCurrentUser = async (decoded) => {
|
68
|
+
return { ...decoded }
|
69
|
+
}
|
70
|
+
```
|
71
|
+
|
72
|
+
#### User info is contained in the decoded token and roles extracted
|
73
|
+
|
74
|
+
```js
|
75
|
+
export const getCurrentUser = async (decoded) => {
|
76
|
+
return { ...decoded, roles: parseJWT({ decoded }).roles }
|
77
|
+
}
|
78
|
+
```
|
79
|
+
|
80
|
+
#### User record query by email with namespaced app_metadata roles as Auth0 requires custom JWT claims to be namespaced
|
81
|
+
|
82
|
+
```js
|
83
|
+
export const getCurrentUser = async (decoded) => {
|
84
|
+
const currentUser = await db.user.findUnique({
|
85
|
+
where: { email: decoded.email },
|
86
|
+
})
|
87
|
+
|
88
|
+
return {
|
89
|
+
...currentUser,
|
90
|
+
roles: parseJWT({ decoded: decoded, namespace: NAMESPACE }).roles,
|
91
|
+
}
|
92
|
+
}
|
93
|
+
```
|
94
|
+
|
95
|
+
#### User record query by an identity with app_metadata roles
|
96
|
+
|
97
|
+
```js
|
98
|
+
const getCurrentUser = async (decoded) => {
|
99
|
+
const currentUser = await db.user.findUnique({
|
100
|
+
where: { userIdentity: decoded.sub },
|
101
|
+
})
|
102
|
+
return {
|
103
|
+
...currentUser,
|
104
|
+
roles: parseJWT({ decoded: decoded }).roles,
|
105
|
+
}
|
106
|
+
}
|
107
|
+
```
|
108
|
+
|
109
|
+
#### Cookies and other request information are available in the req parameter, just in case
|
110
|
+
|
111
|
+
```js
|
112
|
+
const getCurrentUser = async (_decoded, _raw, { event, _context }) => {
|
113
|
+
const cookies = cookie(event.headers.cookies)
|
114
|
+
const session = cookies['my.cookie.name']
|
115
|
+
const currentUser = await db.sessions.findUnique({ where: { id: session } })
|
116
|
+
return currentUser
|
117
|
+
}
|
118
|
+
```
|
119
|
+
|
120
|
+
## requireAuth
|
121
|
+
|
122
|
+
Use `requireAuth` in your services to check that a user is logged in, whether or not they are assigned a role, and optionally raise an error if they're not.
|
123
|
+
|
124
|
+
```js
|
125
|
+
@param {string=} roles - An optional role or list of roles
|
126
|
+
@param {string[]=} roles - An optional list of roles
|
127
|
+
|
128
|
+
@returns {boolean} - If the currentUser is authenticated (and assigned one of the given roles)
|
129
|
+
|
130
|
+
@throws {AuthenticationError} - If the currentUser is not authenticated
|
131
|
+
@throws {ForbiddenError} If the currentUser is not allowed due to role permissions
|
132
|
+
```
|
133
|
+
|
134
|
+
### Examples
|
135
|
+
|
136
|
+
#### Checks if currentUser is authenticated
|
137
|
+
|
138
|
+
```js
|
139
|
+
requireAuth()
|
140
|
+
```
|
141
|
+
|
142
|
+
#### Checks if currentUser is authenticated and assigned one of the given roles
|
143
|
+
|
144
|
+
```js
|
145
|
+
requireAuth({ role: 'admin' })
|
146
|
+
requireAuth({ role: ['editor', 'author'] })
|
147
|
+
requireAuth({ role: ['publisher'] })
|
148
|
+
```
|
package/dist/index.d.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA"}
|
package/dist/index.js
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __defProp = Object.defineProperty;
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
8
|
+
for (let key of __getOwnPropNames(from))
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
11
|
+
}
|
12
|
+
return to;
|
13
|
+
};
|
14
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
15
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
16
|
+
var index_exports = {};
|
17
|
+
module.exports = __toCommonJS(index_exports);
|
18
|
+
__reExport(index_exports, require("./setup"), module.exports);
|
19
|
+
// Annotate the CommonJS export names for ESM import in node:
|
20
|
+
0 && (module.exports = {
|
21
|
+
...require("./setup")
|
22
|
+
});
|
package/dist/setup.d.ts
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
import type yargs from 'yargs';
|
2
|
+
export declare const command = "supertokens";
|
3
|
+
export declare const description = "Set up auth for for SuperTokens";
|
4
|
+
export declare function builder(yargs: yargs.Argv): Promise<yargs.Argv<{
|
5
|
+
force: boolean;
|
6
|
+
} & {
|
7
|
+
verbose: boolean;
|
8
|
+
}>>;
|
9
|
+
export interface Args {
|
10
|
+
force: boolean;
|
11
|
+
}
|
12
|
+
export declare function handler(options: Args): Promise<void>;
|
13
|
+
//# sourceMappingURL=setup.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,eAAO,MAAM,OAAO,gBAAgB,CAAA;AACpC,eAAO,MAAM,WAAW,oCAAoC,CAAA;AAE5D,wBAAsB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI;;;;IAE9C;AAED,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,wBAAsB,OAAO,CAAC,OAAO,EAAE,IAAI,iBAG1C"}
|
package/dist/setup.js
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __create = Object.create;
|
3
|
+
var __defProp = Object.defineProperty;
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
8
|
+
var __export = (target, all) => {
|
9
|
+
for (var name in all)
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
11
|
+
};
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
14
|
+
for (let key of __getOwnPropNames(from))
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
17
|
+
}
|
18
|
+
return to;
|
19
|
+
};
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
26
|
+
mod
|
27
|
+
));
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
29
|
+
var setup_exports = {};
|
30
|
+
__export(setup_exports, {
|
31
|
+
builder: () => builder,
|
32
|
+
command: () => command,
|
33
|
+
description: () => description,
|
34
|
+
handler: () => handler
|
35
|
+
});
|
36
|
+
module.exports = __toCommonJS(setup_exports);
|
37
|
+
var import_cli_helpers = require("@redmix/cli-helpers");
|
38
|
+
const command = "supertokens";
|
39
|
+
const description = "Set up auth for for SuperTokens";
|
40
|
+
async function builder(yargs) {
|
41
|
+
return (0, import_cli_helpers.standardAuthBuilder)(yargs);
|
42
|
+
}
|
43
|
+
async function handler(options) {
|
44
|
+
const { handler: handler2 } = await import("./setupHandler.js");
|
45
|
+
return handler2(options);
|
46
|
+
}
|
47
|
+
// Annotate the CommonJS export names for ESM import in node:
|
48
|
+
0 && (module.exports = {
|
49
|
+
builder,
|
50
|
+
command,
|
51
|
+
description,
|
52
|
+
handler
|
53
|
+
});
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"setupHandler.d.ts","sourceRoot":"","sources":["../src/setupHandler.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAEnC,wBAAsB,OAAO,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,iBA6BtD;AAGD,eAAO,MAAM,eAAe;;;CAqD3B,CAAA"}
|
@@ -0,0 +1,101 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __create = Object.create;
|
3
|
+
var __defProp = Object.defineProperty;
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
8
|
+
var __export = (target, all) => {
|
9
|
+
for (var name in all)
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
11
|
+
};
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
14
|
+
for (let key of __getOwnPropNames(from))
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
17
|
+
}
|
18
|
+
return to;
|
19
|
+
};
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
26
|
+
mod
|
27
|
+
));
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
29
|
+
var setupHandler_exports = {};
|
30
|
+
__export(setupHandler_exports, {
|
31
|
+
addRoutingLogic: () => addRoutingLogic,
|
32
|
+
handler: () => handler
|
33
|
+
});
|
34
|
+
module.exports = __toCommonJS(setupHandler_exports);
|
35
|
+
var import_fs = __toESM(require("fs"));
|
36
|
+
var import_path = __toESM(require("path"));
|
37
|
+
var import_cli_helpers = require("@redmix/cli-helpers");
|
38
|
+
async function handler({ force: forceArg }) {
|
39
|
+
const { version } = JSON.parse(
|
40
|
+
import_fs.default.readFileSync(import_path.default.resolve(__dirname, "../package.json"), "utf-8")
|
41
|
+
);
|
42
|
+
(0, import_cli_helpers.standardAuthHandler)({
|
43
|
+
basedir: __dirname,
|
44
|
+
forceArg,
|
45
|
+
provider: "supertokens",
|
46
|
+
authDecoderImport: "import { authDecoder } from '@redmix/auth-supertokens-api'",
|
47
|
+
apiPackages: [
|
48
|
+
`@redmix/auth-supertokens-api@${version}`,
|
49
|
+
"supertokens-node@^15"
|
50
|
+
],
|
51
|
+
webPackages: [
|
52
|
+
`@redmix/auth-supertokens-web@${version}`,
|
53
|
+
"supertokens-auth-react@~0.34.0",
|
54
|
+
"supertokens-web-js@~0.7.0"
|
55
|
+
],
|
56
|
+
extraTasks: [addRoutingLogic],
|
57
|
+
notes: [
|
58
|
+
"We've implemented SuperToken's EmailPassword with Social / Enterprise (OAuth 2.0, SAML) login recipe,",
|
59
|
+
"but feel free to switch to something that better fits your needs. See https://supertokens.com/docs/guides.",
|
60
|
+
"",
|
61
|
+
"To get things working, you'll need to add quite a few env vars to your .env file.",
|
62
|
+
"See https://redwoodjs.com/docs/auth/supertokens for a full walkthrough."
|
63
|
+
]
|
64
|
+
});
|
65
|
+
}
|
66
|
+
const addRoutingLogic = {
|
67
|
+
title: `Adding SuperTokens routing logic to Routes.{jsx,tsx}...`,
|
68
|
+
task: () => {
|
69
|
+
const routesPath = (0, import_cli_helpers.getPaths)().web.routes;
|
70
|
+
let content = import_fs.default.readFileSync(routesPath, "utf-8");
|
71
|
+
content = content.replace("import SuperTokens from 'supertokens-auth-react'", "").replace(/if \(SuperTokens.canHandleRoute\(\)\) {[^}]+}/, "");
|
72
|
+
if (!/\s*if\s*\(canHandleRoute\(PreBuiltUI\)\)\s*\{/.test(content)) {
|
73
|
+
let hasImportedSuperTokensFunctions = false;
|
74
|
+
content = content.split("\n").reduce((acc, line) => {
|
75
|
+
if (!hasImportedSuperTokensFunctions && line.includes("import") && line.includes("@redmix")) {
|
76
|
+
acc.push(
|
77
|
+
"import { canHandleRoute, getRoutingComponent } from 'supertokens-auth-react/ui'"
|
78
|
+
);
|
79
|
+
acc.push("");
|
80
|
+
hasImportedSuperTokensFunctions = true;
|
81
|
+
}
|
82
|
+
acc.push(line);
|
83
|
+
return acc;
|
84
|
+
}, []).join("\n");
|
85
|
+
content = content.replace(
|
86
|
+
"import { useAuth } from './auth'",
|
87
|
+
"import { useAuth, PreBuiltUI } from './auth'"
|
88
|
+
);
|
89
|
+
content = content.replace(
|
90
|
+
/const Routes = \(\) => \{\n/,
|
91
|
+
"const Routes = () => {\n if (canHandleRoute(PreBuiltUI)) {\n return getRoutingComponent(PreBuiltUI)\n }\n\n"
|
92
|
+
);
|
93
|
+
import_fs.default.writeFileSync(routesPath, content);
|
94
|
+
}
|
95
|
+
}
|
96
|
+
};
|
97
|
+
// Annotate the CommonJS export names for ESM import in node:
|
98
|
+
0 && (module.exports = {
|
99
|
+
addRoutingLogic,
|
100
|
+
handler
|
101
|
+
});
|
@@ -0,0 +1,128 @@
|
|
1
|
+
import { parseJWT } from '@redmix/api'
|
2
|
+
import { AuthenticationError, ForbiddenError } from '@redmix/graphql-server'
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Represents the user attributes returned by the decoding the
|
6
|
+
* Authentication provider's JWT together with an optional list of roles.
|
7
|
+
*/
|
8
|
+
type RedwoodUser = Record<string, unknown> & { roles?: string[] }
|
9
|
+
|
10
|
+
/**
|
11
|
+
* getCurrentUser returns the user information together with
|
12
|
+
* an optional collection of roles used by requireAuth() to check
|
13
|
+
* if the user is authenticated or has role-based access
|
14
|
+
*
|
15
|
+
* @param decoded - The decoded access token containing user info and JWT claims like `sub`. Note could be null.
|
16
|
+
* @param { token, SupportedAuthTypes type } - The access token itself as well as the auth provider type
|
17
|
+
* @param { APIGatewayEvent event, Context context } - An object which contains information from the invoker
|
18
|
+
* such as headers and cookies, and the context information about the invocation such as IP Address
|
19
|
+
*
|
20
|
+
* !! BEWARE !! Anything returned from this function will be available to the
|
21
|
+
* client--it becomes the content of `currentUser` on the web side (as well as
|
22
|
+
* `context.currentUser` on the api side). You should carefully add additional
|
23
|
+
* fields to the return object only once you've decided they are safe to be seen
|
24
|
+
* if someone were to open the Web Inspector in their browser.
|
25
|
+
*
|
26
|
+
* @see https://github.com/redmix-run/redmix/tree/main/packages/auth for examples
|
27
|
+
*
|
28
|
+
* @returns RedwoodUser
|
29
|
+
*/
|
30
|
+
export const getCurrentUser = async (
|
31
|
+
decoded,
|
32
|
+
/* eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars */
|
33
|
+
{ token, type },
|
34
|
+
/* eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars */
|
35
|
+
{ event, context }
|
36
|
+
): Promise<RedwoodUser> => {
|
37
|
+
if (!decoded) {
|
38
|
+
return null
|
39
|
+
}
|
40
|
+
|
41
|
+
const { roles } = parseJWT({ decoded })
|
42
|
+
|
43
|
+
if (roles) {
|
44
|
+
return { ...decoded, roles }
|
45
|
+
}
|
46
|
+
|
47
|
+
return { ...decoded }
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
* The user is authenticated if there is a currentUser in the context
|
52
|
+
*
|
53
|
+
* @returns {boolean} - If the currentUser is authenticated
|
54
|
+
*/
|
55
|
+
export const isAuthenticated = (): boolean => {
|
56
|
+
return !!context.currentUser
|
57
|
+
}
|
58
|
+
|
59
|
+
/**
|
60
|
+
* When checking role membership, roles can be a single value, a list, or none.
|
61
|
+
* You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client`
|
62
|
+
*/
|
63
|
+
type AllowedRoles = string | string[] | undefined
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Checks if the currentUser is authenticated (and assigned one of the given roles)
|
67
|
+
*
|
68
|
+
* @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles
|
69
|
+
*
|
70
|
+
* @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles,
|
71
|
+
* or when no roles are provided to check against. Otherwise returns false.
|
72
|
+
*/
|
73
|
+
export const hasRole = (roles: AllowedRoles): boolean => {
|
74
|
+
if (!isAuthenticated()) {
|
75
|
+
return false
|
76
|
+
}
|
77
|
+
|
78
|
+
const currentUserRoles = context.currentUser?.roles
|
79
|
+
|
80
|
+
if (typeof roles === 'string') {
|
81
|
+
if (typeof currentUserRoles === 'string') {
|
82
|
+
// roles to check is a string, currentUser.roles is a string
|
83
|
+
return currentUserRoles === roles
|
84
|
+
} else if (Array.isArray(currentUserRoles)) {
|
85
|
+
// roles to check is a string, currentUser.roles is an array
|
86
|
+
return currentUserRoles?.some((allowedRole) => roles === allowedRole)
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
if (Array.isArray(roles)) {
|
91
|
+
if (Array.isArray(currentUserRoles)) {
|
92
|
+
// roles to check is an array, currentUser.roles is an array
|
93
|
+
return currentUserRoles?.some((allowedRole) =>
|
94
|
+
roles.includes(allowedRole)
|
95
|
+
)
|
96
|
+
} else if (typeof currentUserRoles === 'string') {
|
97
|
+
// roles to check is an array, currentUser.roles is a string
|
98
|
+
return roles.some((allowedRole) => currentUserRoles === allowedRole)
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
// roles not found
|
103
|
+
return false
|
104
|
+
}
|
105
|
+
|
106
|
+
/**
|
107
|
+
* Use requireAuth in your services to check that a user is logged in,
|
108
|
+
* whether or not they are assigned a role, and optionally raise an
|
109
|
+
* error if they're not.
|
110
|
+
*
|
111
|
+
* @param roles?: {@link AllowedRoles} - When checking role membership, these roles grant access.
|
112
|
+
*
|
113
|
+
* @returns - If the currentUser is authenticated (and assigned one of the given roles)
|
114
|
+
*
|
115
|
+
* @throws {@link AuthenticationError} - If the currentUser is not authenticated
|
116
|
+
* @throws {@link ForbiddenError} - If the currentUser is not allowed due to role permissions
|
117
|
+
*
|
118
|
+
* @see https://github.com/redmix-run/redmix/tree/main/packages/auth for examples
|
119
|
+
*/
|
120
|
+
export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
|
121
|
+
if (!isAuthenticated()) {
|
122
|
+
throw new AuthenticationError("You don't have permission to do that.")
|
123
|
+
}
|
124
|
+
|
125
|
+
if (roles && !hasRole(roles)) {
|
126
|
+
throw new ForbiddenError("You don't have access to do that.")
|
127
|
+
}
|
128
|
+
}
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import * as Session from 'supertokens-node/recipe/session'
|
2
|
+
import ThirdPartyEmailPassword from 'supertokens-node/recipe/thirdpartyemailpassword'
|
3
|
+
import type { TypeInput } from 'supertokens-node/types'
|
4
|
+
|
5
|
+
const websiteDomain =
|
6
|
+
process.env.SUPERTOKENS_WEBSITE_DOMAIN || 'http://localhost:8910'
|
7
|
+
const apiDomain = process.env.SUPERTOKENS_API_DOMAIN || websiteDomain
|
8
|
+
const apiGatewayPath =
|
9
|
+
process.env.SUPERTOKENS_API_GATEWAY_PATH || '/.redwood/functions'
|
10
|
+
|
11
|
+
export const config: TypeInput = {
|
12
|
+
# The below options are ok here even if you're not running on top of AWS Lambda, since Redwood internally translates Fastify request/response objects to and from the AWS Lambda format.
|
13
|
+
framework: 'awsLambda',
|
14
|
+
isInServerlessEnv: true,
|
15
|
+
appInfo: {
|
16
|
+
appName: process.env.SUPERTOKENS_APP_NAME,
|
17
|
+
apiDomain,
|
18
|
+
websiteDomain,
|
19
|
+
apiGatewayPath,
|
20
|
+
websiteBasePath: '/auth',
|
21
|
+
apiBasePath: '/auth',
|
22
|
+
},
|
23
|
+
supertokens: {
|
24
|
+
connectionURI: process.env.SUPERTOKENS_CONNECTION_URI,
|
25
|
+
apiKey: process.env.SUPERTOKENS_API_KEY,
|
26
|
+
},
|
27
|
+
recipeList: [
|
28
|
+
ThirdPartyEmailPassword.init({
|
29
|
+
providers: [
|
30
|
+
{
|
31
|
+
config: {
|
32
|
+
thirdPartyId: 'google',
|
33
|
+
clients: [
|
34
|
+
{
|
35
|
+
clientId: process.env.SUPERTOKENS_GOOGLE_CLIENT_ID,
|
36
|
+
clientSecret: process.env.SUPERTOKENS_GOOGLE_CLIENT_SECRET,
|
37
|
+
},
|
38
|
+
],
|
39
|
+
},
|
40
|
+
},
|
41
|
+
{
|
42
|
+
config: {
|
43
|
+
thirdPartyId: 'github',
|
44
|
+
clients: [
|
45
|
+
{
|
46
|
+
clientId: process.env.SUPERTOKENS_GITHUB_CLIENT_ID,
|
47
|
+
clientSecret: process.env.SUPERTOKENS_GITHUB_CLIENT_SECRET,
|
48
|
+
},
|
49
|
+
],
|
50
|
+
},
|
51
|
+
},
|
52
|
+
{
|
53
|
+
config: {
|
54
|
+
thirdPartyId: 'apple',
|
55
|
+
clients: [
|
56
|
+
{
|
57
|
+
clientId: process.env.SUPERTOKENS_APPLE_CLIENT_ID,
|
58
|
+
additionalConfig: {
|
59
|
+
keyId: process.env.SUPERTOKENS_APPLE_SECRET_KEY_ID,
|
60
|
+
privateKey: process.env.SUPERTOKENS_APPLE_SECRET_PRIVATE_KEY,
|
61
|
+
teamId: process.env.SUPERTOKENS_APPLE_SECRET_TEAM_ID,
|
62
|
+
},
|
63
|
+
},
|
64
|
+
],
|
65
|
+
},
|
66
|
+
},
|
67
|
+
],
|
68
|
+
}),
|
69
|
+
Session.init({
|
70
|
+
getTokenTransferMethod: () => {
|
71
|
+
return 'header'
|
72
|
+
},
|
73
|
+
}),
|
74
|
+
],
|
75
|
+
}
|
@@ -0,0 +1,63 @@
|
|
1
|
+
'use client'
|
2
|
+
|
3
|
+
import SuperTokens, { SuperTokensWrapper } from 'supertokens-auth-react'
|
4
|
+
import Session from 'supertokens-auth-react/recipe/session'
|
5
|
+
import ThirdPartyEmailPassword, {
|
6
|
+
Github,
|
7
|
+
Google,
|
8
|
+
Apple,
|
9
|
+
} from 'supertokens-auth-react/recipe/thirdpartyemailpassword'
|
10
|
+
import { ThirdPartyEmailPasswordPreBuiltUI } from 'supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui'
|
11
|
+
|
12
|
+
import { createAuth } from '@redmix/auth-supertokens-web'
|
13
|
+
import { isBrowser } from '@redmix/prerender/browserUtils'
|
14
|
+
|
15
|
+
const websiteDomain =
|
16
|
+
process.env.SUPERTOKENS_WEBSITE_DOMAIN || 'http://localhost:8910'
|
17
|
+
const apiDomain = process.env.SUPERTOKENS_API_DOMAIN || websiteDomain
|
18
|
+
const apiGatewayPath =
|
19
|
+
process.env.SUPERTOKENS_API_GATEWAY_PATH || '/.redwood/functions'
|
20
|
+
|
21
|
+
const superTokensClient = {
|
22
|
+
sessionRecipe: Session,
|
23
|
+
redirectToAuth: SuperTokens.redirectToAuth,
|
24
|
+
}
|
25
|
+
|
26
|
+
export const PreBuiltUI = [ThirdPartyEmailPasswordPreBuiltUI]
|
27
|
+
|
28
|
+
isBrowser &&
|
29
|
+
SuperTokens.init({
|
30
|
+
appInfo: {
|
31
|
+
appName: process.env.SUPERTOKENS_APP_NAME,
|
32
|
+
apiDomain,
|
33
|
+
websiteDomain,
|
34
|
+
apiGatewayPath,
|
35
|
+
websiteBasePath: '/auth',
|
36
|
+
apiBasePath: '/auth',
|
37
|
+
},
|
38
|
+
recipeList: [
|
39
|
+
Session.init(),
|
40
|
+
ThirdPartyEmailPassword.init({
|
41
|
+
signInAndUpFeature: {
|
42
|
+
providers: [Github.init(), Google.init(), Apple.init()],
|
43
|
+
},
|
44
|
+
}),
|
45
|
+
],
|
46
|
+
})
|
47
|
+
|
48
|
+
const { AuthProvider: SuperTokensAuthProvider, useAuth } =
|
49
|
+
createAuth(superTokensClient)
|
50
|
+
|
51
|
+
interface Props {
|
52
|
+
children: React.ReactNode
|
53
|
+
}
|
54
|
+
|
55
|
+
const AuthProvider = ({ children }: Props) => {
|
56
|
+
return (
|
57
|
+
<SuperTokensWrapper>
|
58
|
+
<SuperTokensAuthProvider>{children}</SuperTokensAuthProvider>
|
59
|
+
</SuperTokensWrapper>
|
60
|
+
)
|
61
|
+
}
|
62
|
+
|
63
|
+
export { AuthProvider, useAuth }
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import SuperTokens, { SuperTokensWrapper } from 'supertokens-auth-react'
|
2
|
+
import Session from 'supertokens-auth-react/recipe/session'
|
3
|
+
import ThirdPartyEmailPassword, {
|
4
|
+
Github,
|
5
|
+
Google,
|
6
|
+
Apple,
|
7
|
+
} from 'supertokens-auth-react/recipe/thirdpartyemailpassword'
|
8
|
+
import { ThirdPartyEmailPasswordPreBuiltUI } from 'supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui'
|
9
|
+
|
10
|
+
import { createAuth } from '@redmix/auth-supertokens-web'
|
11
|
+
import { isBrowser } from '@redmix/prerender/browserUtils'
|
12
|
+
|
13
|
+
const websiteDomain =
|
14
|
+
process.env.SUPERTOKENS_WEBSITE_DOMAIN || 'http://localhost:8910'
|
15
|
+
const apiDomain = process.env.SUPERTOKENS_API_DOMAIN || websiteDomain
|
16
|
+
const apiGatewayPath =
|
17
|
+
process.env.SUPERTOKENS_API_GATEWAY_PATH || '/.redwood/functions'
|
18
|
+
|
19
|
+
const superTokensClient = {
|
20
|
+
sessionRecipe: Session,
|
21
|
+
redirectToAuth: SuperTokens.redirectToAuth,
|
22
|
+
}
|
23
|
+
|
24
|
+
export const PreBuiltUI = [ThirdPartyEmailPasswordPreBuiltUI]
|
25
|
+
|
26
|
+
isBrowser &&
|
27
|
+
SuperTokens.init({
|
28
|
+
appInfo: {
|
29
|
+
appName: process.env.SUPERTOKENS_APP_NAME,
|
30
|
+
apiDomain,
|
31
|
+
websiteDomain,
|
32
|
+
apiGatewayPath,
|
33
|
+
websiteBasePath: '/auth',
|
34
|
+
apiBasePath: '/auth',
|
35
|
+
},
|
36
|
+
recipeList: [
|
37
|
+
Session.init(),
|
38
|
+
ThirdPartyEmailPassword.init({
|
39
|
+
signInAndUpFeature: {
|
40
|
+
providers: [Github.init(), Google.init(), Apple.init()],
|
41
|
+
},
|
42
|
+
}),
|
43
|
+
],
|
44
|
+
})
|
45
|
+
|
46
|
+
const { AuthProvider: SuperTokensAuthProvider, useAuth } =
|
47
|
+
createAuth(superTokensClient)
|
48
|
+
|
49
|
+
interface Props {
|
50
|
+
children: React.ReactNode
|
51
|
+
}
|
52
|
+
|
53
|
+
const AuthProvider = ({ children }: Props) => {
|
54
|
+
return (
|
55
|
+
<SuperTokensWrapper>
|
56
|
+
<SuperTokensAuthProvider>{children}</SuperTokensAuthProvider>
|
57
|
+
</SuperTokensWrapper>
|
58
|
+
)
|
59
|
+
}
|
60
|
+
|
61
|
+
export { AuthProvider, useAuth }
|
package/package.json
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
{
|
2
|
+
"name": "@redmix/auth-supertokens-setup",
|
3
|
+
"version": "0.0.1",
|
4
|
+
"repository": {
|
5
|
+
"type": "git",
|
6
|
+
"url": "git+https://github.com/redmix-run/redmix.git",
|
7
|
+
"directory": "packages/auth-providers/supertokens/setup"
|
8
|
+
},
|
9
|
+
"license": "MIT",
|
10
|
+
"type": "commonjs",
|
11
|
+
"exports": {
|
12
|
+
".": {
|
13
|
+
"types": "./dist/index.d.ts",
|
14
|
+
"default": "./dist/index.js"
|
15
|
+
},
|
16
|
+
"./dist/setup": {
|
17
|
+
"types": "./dist/setup.d.ts",
|
18
|
+
"default": "./dist/setup.js"
|
19
|
+
},
|
20
|
+
"./dist/setupHandler": {
|
21
|
+
"types": "./dist/setupHandler.d.ts",
|
22
|
+
"default": "./dist/setupHandler.js"
|
23
|
+
}
|
24
|
+
},
|
25
|
+
"main": "./dist/index.js",
|
26
|
+
"types": "./dist/index.d.ts",
|
27
|
+
"files": [
|
28
|
+
"dist"
|
29
|
+
],
|
30
|
+
"scripts": {
|
31
|
+
"build": "tsx ./build.mts && yarn build:types",
|
32
|
+
"build:pack": "yarn pack -o redmix-auth-supertokens-setup.tgz",
|
33
|
+
"build:types": "tsc --build --verbose ./tsconfig.json",
|
34
|
+
"build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"",
|
35
|
+
"check:attw": "yarn attw -P",
|
36
|
+
"check:package": "concurrently npm:check:attw yarn:publint",
|
37
|
+
"prepublishOnly": "NODE_ENV=production yarn build",
|
38
|
+
"test": "vitest run",
|
39
|
+
"test:watch": "vitest watch"
|
40
|
+
},
|
41
|
+
"dependencies": {
|
42
|
+
"@redmix/cli-helpers": "0.0.1"
|
43
|
+
},
|
44
|
+
"devDependencies": {
|
45
|
+
"@arethetypeswrong/cli": "0.16.4",
|
46
|
+
"@redmix/framework-tools": "0.0.1",
|
47
|
+
"@types/yargs": "17.0.33",
|
48
|
+
"concurrently": "8.2.2",
|
49
|
+
"memfs": "4.17.0",
|
50
|
+
"publint": "0.3.11",
|
51
|
+
"tsx": "4.19.3",
|
52
|
+
"typescript": "5.6.2",
|
53
|
+
"vitest": "2.1.9"
|
54
|
+
},
|
55
|
+
"publishConfig": {
|
56
|
+
"access": "public"
|
57
|
+
},
|
58
|
+
"gitHead": "25a2481ac394049b7c864eda26381814a0124a79"
|
59
|
+
}
|