├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ ├── package.yaml
│ ├── prerelease.yaml
│ └── test.yaml
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
└── pre-commit
├── .vscode
├── extensions.json
└── settings.json
├── .yarn
└── sdks
│ ├── eslint
│ ├── bin
│ │ └── eslint.js
│ ├── lib
│ │ ├── api.js
│ │ └── unsupported-api.js
│ └── package.json
│ ├── integrations.yml
│ ├── prettier
│ ├── bin-prettier.js
│ ├── index.js
│ └── package.json
│ └── typescript
│ ├── bin
│ ├── tsc
│ └── tsserver
│ ├── lib
│ ├── tsc.js
│ ├── tsserver.js
│ ├── tsserverlibrary.js
│ └── typescript.js
│ └── package.json
├── .yarnrc.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── commitlint.config.js
├── lerna.json
├── package.json
├── packages
├── client
│ ├── .babelrc
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── Authorizer.ts
│ │ ├── Autorizer.test.ts
│ │ ├── broadcast.ts
│ │ ├── index.ts
│ │ ├── logger.ts
│ │ ├── observable.ts
│ │ ├── status.ts
│ │ └── types.ts
│ ├── tsconfig.build.json
│ ├── tsconfig.json
│ └── tsup.config.js
├── react
│ ├── .babelrc
│ ├── .gitignore
│ ├── .storybook
│ │ ├── globals.css
│ │ ├── main.ts
│ │ └── preview.ts
│ ├── CHANGELOG.md
│ ├── LICENSE
│ ├── README.md
│ ├── components.json
│ ├── components
│ │ └── ui
│ │ │ ├── button.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── form.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ └── tooltip.tsx
│ ├── dts.config.js
│ ├── jest.config.js
│ ├── lib
│ │ ├── auth
│ │ │ └── index.tsx
│ │ └── utils.ts
│ ├── package.json
│ ├── src
│ │ ├── AzureSignInButton
│ │ │ ├── AzureSignInButton.stories.tsx
│ │ │ ├── AzureSignInButton.test.tsx
│ │ │ └── index.tsx
│ │ ├── DiscordSignInButton
│ │ │ ├── DiscordSignInButton.stories.tsx
│ │ │ ├── DiscordSignInButton.test.tsx
│ │ │ └── index.tsx
│ │ ├── EmailSignIn
│ │ │ ├── EmailSignInButton.stories.tsx
│ │ │ ├── EmailSignInButton.tsx
│ │ │ ├── Form.tsx
│ │ │ ├── hooks.ts
│ │ │ ├── index.tsx
│ │ │ └── types.ts
│ │ ├── GitHubSignInButton
│ │ │ ├── GitHubSignInButton.stories.tsx
│ │ │ ├── GitHubSignInButton.test.tsx
│ │ │ └── index.tsx
│ │ ├── GoogleLoginButton
│ │ │ ├── GoogleLoginButton.stories.tsx
│ │ │ ├── GoogleLoginButton.tsx
│ │ │ ├── GoogleSSOButton.test.tsx
│ │ │ └── index.tsx
│ │ ├── HubSpotSignInButton
│ │ │ ├── HubSpotSignInButton.stories.tsx
│ │ │ ├── HubSpotSignInButton.test.tsx
│ │ │ └── index.tsx
│ │ ├── LinkedInSignInButton
│ │ │ ├── LinkedInSignInButton.stories.tsx
│ │ │ ├── LinkedInSignInButton.test.tsx
│ │ │ └── index.tsx
│ │ ├── SignInForm
│ │ │ ├── Form.tsx
│ │ │ ├── SignInForm.stories.tsx
│ │ │ ├── SignInForm.test.tsx
│ │ │ ├── SignInForm.tsx
│ │ │ ├── hooks.tsx
│ │ │ ├── index.tsx
│ │ │ └── types.ts
│ │ ├── SignOutButton
│ │ │ ├── SignOutButton.stories.tsx
│ │ │ ├── SignOutButton.test.tsx
│ │ │ └── index.tsx
│ │ ├── SignUpForm
│ │ │ ├── Form.tsx
│ │ │ ├── SignUpForm.stories.tsx
│ │ │ ├── SignUpForm.test.tsx
│ │ │ ├── SignUpForm.tsx
│ │ │ ├── hooks.tsx
│ │ │ ├── index.tsx
│ │ │ └── types.ts
│ │ ├── SignedIn
│ │ │ └── index.tsx
│ │ ├── SignedOut
│ │ │ └── index.tsx
│ │ ├── SlackSignInButton
│ │ │ ├── SlackSignInButton.stories.tsx
│ │ │ ├── SlackSignInButton.test.tsx
│ │ │ └── index.tsx
│ │ ├── TenantSelector
│ │ │ ├── TenantSelector.stories.tsx
│ │ │ ├── hooks.ts
│ │ │ ├── index.tsx
│ │ │ └── types.ts
│ │ ├── UserInfo
│ │ │ ├── UserInfo.stories.tsx
│ │ │ ├── UserInfo.test.tsx
│ │ │ ├── hooks.tsx
│ │ │ └── index.tsx
│ │ ├── XSignInButton
│ │ │ ├── XSignInButton.stories.tsx
│ │ │ ├── XSignInButton.test.tsx
│ │ │ └── index.tsx
│ │ ├── index.ts
│ │ ├── resetPassword
│ │ │ ├── ForgotPassword
│ │ │ │ ├── ForgotPassword.stories.tsx
│ │ │ │ ├── Form.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── PasswordResetRequestForm
│ │ │ │ ├── Form.tsx
│ │ │ │ ├── PasswordResetRequestForm.stories.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── hooks.ts
│ │ │ ├── index.tsx
│ │ │ └── types.ts
│ │ └── types.ts
│ ├── test
│ │ ├── fetch.mock.ts
│ │ └── matchMedia.mock
│ ├── tsconfig.build.json
│ ├── tsconfig.json
│ └── tsup.config.js
└── server
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── DEVELOPERS.md
│ ├── README.md
│ ├── example.env
│ ├── jest.config.js
│ ├── openapitools.json
│ ├── package.json
│ ├── src
│ ├── Api.ts
│ ├── Server.test.ts
│ ├── Server.ts
│ ├── api
│ │ ├── handlers
│ │ │ ├── DELETE.test.ts
│ │ │ ├── DELETE.ts
│ │ │ ├── GET.ts
│ │ │ ├── Get.test.ts
│ │ │ ├── POST.test.ts
│ │ │ ├── POST.ts
│ │ │ ├── PUT.test.ts
│ │ │ ├── PUT.ts
│ │ │ ├── index.ts
│ │ │ └── withContext
│ │ │ │ ├── index.ts
│ │ │ │ └── withContext.test.ts
│ │ ├── openapi
│ │ │ ├── swagger-doc.json
│ │ │ └── swagger.json
│ │ ├── routes
│ │ │ ├── auth
│ │ │ │ ├── callback.ts
│ │ │ │ ├── csrf.ts
│ │ │ │ ├── error.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── password-reset.ts
│ │ │ │ ├── providers.ts
│ │ │ │ ├── session.ts
│ │ │ │ ├── signin.ts
│ │ │ │ ├── signout.ts
│ │ │ │ ├── verify-email.ts
│ │ │ │ └── verify-request.ts
│ │ │ ├── me
│ │ │ │ └── index.ts
│ │ │ ├── signup
│ │ │ │ ├── POST.ts
│ │ │ │ ├── index.tsx
│ │ │ │ └── signup.test.ts
│ │ │ ├── tenants
│ │ │ │ ├── GET.ts
│ │ │ │ ├── POST.ts
│ │ │ │ ├── [tenantId]
│ │ │ │ │ ├── DELETE.ts
│ │ │ │ │ ├── GET.ts
│ │ │ │ │ ├── PUT.ts
│ │ │ │ │ └── users
│ │ │ │ │ │ ├── GET.ts
│ │ │ │ │ │ ├── POST.ts
│ │ │ │ │ │ ├── [userId]
│ │ │ │ │ │ ├── DELETE.ts
│ │ │ │ │ │ ├── PUT.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ ├── apiTenants.test.ts
│ │ │ │ └── index.ts
│ │ │ └── users
│ │ │ │ ├── GET.ts
│ │ │ │ ├── POST.ts
│ │ │ │ ├── [userId]
│ │ │ │ └── PUT.ts
│ │ │ │ ├── apiUsers.test.ts
│ │ │ │ └── index.ts
│ │ ├── swagger.ts
│ │ ├── types.ts
│ │ └── utils
│ │ │ ├── auth.ts
│ │ │ ├── request.test.ts
│ │ │ ├── request.ts
│ │ │ └── routes
│ │ │ └── index.ts
│ ├── auth
│ │ ├── auth.test.ts
│ │ ├── getCsrf.ts
│ │ └── index.ts
│ ├── db
│ │ ├── DBManager.ts
│ │ ├── NileInstance.test.ts
│ │ ├── NileInstance.ts
│ │ ├── PoolProxy.ts
│ │ ├── db.test.ts
│ │ └── index.ts
│ ├── index.ts
│ ├── lib
│ │ ├── express.test.ts
│ │ ├── express.ts
│ │ └── nitro.ts
│ ├── tenants
│ │ ├── index.ts
│ │ ├── tenants.test.ts
│ │ └── types.ts
│ ├── types.ts
│ ├── users
│ │ ├── index.ts
│ │ ├── types.ts
│ │ └── users.test.ts
│ └── utils
│ │ ├── Config
│ │ ├── Config.test.ts
│ │ ├── envVars.test.ts
│ │ ├── envVars.ts
│ │ └── index.ts
│ │ ├── Event
│ │ └── index.ts
│ │ ├── Logger.ts
│ │ ├── ResponseError.ts
│ │ ├── constants.ts
│ │ └── fetch.ts
│ ├── test
│ ├── configKeys.ts
│ ├── fetch.mock.ts
│ ├── integration
│ │ └── integration.test.ts
│ └── jest.setup.js
│ ├── tsconfig.build.json
│ ├── tsconfig.json
│ └── tsup.config.ts
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | [*.{js,json,yml}]
8 | charset = utf-8
9 | indent_style = space
10 | indent_size = 2
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | lib/nile/src/generated
3 | node_modules
4 | !.storybook
5 | storybook-static
6 | .log
7 | .yarn/*
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "plugins": [
4 | "react-hooks",
5 | "@typescript-eslint",
6 | "prettier",
7 | "import",
8 | "react"
9 | ],
10 | "settings": {
11 | "react": {
12 | "version": "detect"
13 | }
14 | },
15 | "extends": [
16 | "plugin:react/recommended",
17 | "eslint:recommended",
18 | "prettier",
19 | "plugin:@typescript-eslint/recommended",
20 | "plugin:storybook/recommended"
21 | ],
22 | "parserOptions": {
23 | "extraFileExtensions": [".mjs"]
24 | },
25 | "env": {
26 | "browser": true,
27 | "node": true
28 | },
29 | "rules": {
30 | "prettier/prettier": [
31 | 2,
32 | {
33 | "bracketSpacing": true,
34 | "jsxBracketSameLine": false,
35 | "printWidth": 80,
36 | "semi": true,
37 | "singleQuote": true,
38 | "tabWidth": 2,
39 | "trailingComma": "es5"
40 | }
41 | ],
42 |
43 | "indent": ["error", 2, { "SwitchCase": 1 }],
44 | "semi": "error",
45 | "import/namespace": 2,
46 | "quotes": [
47 | "error",
48 | "single",
49 | {
50 | "avoidEscape": true
51 | }
52 | ],
53 | "react-hooks/rules-of-hooks": "error",
54 | "no-console": 2,
55 | "object-curly-newline": [
56 | 0,
57 | {
58 | "multiline": true,
59 | "minProperties": 4
60 | }
61 | ],
62 | "react/prop-types": 0,
63 | "react/react-in-jsx-scope": "off",
64 | "react/jsx-key": 2,
65 | "react-hooks/exhaustive-deps": "error",
66 | "react/jsx-curly-brace-presence": [2, "never"],
67 | "react/self-closing-comp": 2,
68 | "import/order": [
69 | 2,
70 | {
71 | "newlines-between": "always",
72 | "groups": ["builtin", "external", "parent", "sibling", "index"]
73 | }
74 | ],
75 | "import/newline-after-import": 2,
76 | "import/prefer-default-export": 0,
77 | "linebreak-style": [2, "unix"],
78 | "sort-imports": 0
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/.github/workflows/package.yaml:
--------------------------------------------------------------------------------
1 | name: Publish @niledatabase packages to GitHub Packages
2 |
3 | on:
4 | push:
5 | branches: [stable]
6 | env:
7 | BRANCH_NAME: stable
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: write
13 | packages: write
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 | ref: ${{ env.BRANCH_NAME }}
20 |
21 | - name: Enable Corepack before setting up Node
22 | run: corepack enable
23 |
24 | # Setup .npmrc file to publish to GitHub Packages
25 | - name: Setup Node
26 | uses: actions/setup-node@v4
27 | with:
28 | node-version: "20.x"
29 | # registry-url: 'https://npm.pkg.github.com'
30 | # Defaults to the user or organization that owns the workflow file
31 | # scope: '@niledatabase'
32 | - name: Authenticate to npm
33 | run: |
34 | echo "@niledatabase:wq:registry=https://registry.npmjs.org/"
35 | echo "registry=https://registry.npmjs.org/" >> .npmrc
36 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
37 | npm whoami
38 | env:
39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
40 |
41 | - name: Install @niledatabase/server
42 | working-directory: packages/server
43 | run: yarn install --immutable
44 |
45 | - name: Install @niledatabase/react
46 | working-directory: packages/react
47 | run: yarn install --immutable
48 |
49 | - name: Install @niledatabase/client
50 | working-directory: packages/client
51 | run: yarn install --immutable
52 |
53 | - name: Build @niledatabase/packages
54 | run: yarn build
55 |
56 | - name: Version
57 | run: |
58 | git config user.name "${{ github.actor }}"
59 | git config user.email "${{ github.actor }}@users.noreply.github.com"
60 | npx lerna version --conventional-graduate --force-publish --conventional-commits --yes
61 | env:
62 | GITHUB_TOKEN: ${{ secrets.PAT }}
63 |
64 | - name: Publish
65 | env:
66 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
67 | GITHUB_TOKEN: ${{ secrets.PAT }}
68 | run: |
69 | npx lerna publish from-git --yes
70 |
71 | - name: "Get Previous tag"
72 | id: previoustag
73 | uses: "WyriHaximus/github-action-get-previous-tag@v1"
74 | env:
75 | GITHUB_TOKEN: ${{ secrets.PAT }}
76 |
77 | - name: Create Release Notes
78 | uses: actions/github-script@v6
79 | with:
80 | github-token: ${{ secrets.PAT }}
81 | script: |
82 | await github.request(`POST /repos/${{ github.repository }}/releases`, {
83 | tag_name: "${{ steps.previoustag.outputs.tag }}",
84 | generate_release_notes: true
85 | });
86 |
--------------------------------------------------------------------------------
/.github/workflows/prerelease.yaml:
--------------------------------------------------------------------------------
1 | name: Publish @niledatabase packages
2 |
3 | on:
4 | pull_request:
5 | types:
6 | - closed
7 | branches:
8 | - main
9 | env:
10 | BRANCH_NAME: main
11 |
12 | jobs:
13 | publish:
14 | if: github.event.pull_request.merged == true
15 | runs-on: ubuntu-latest
16 | permissions:
17 | contents: write
18 | packages: write
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v4
22 | with:
23 | fetch-depth: 0
24 | ref: ${{ env.BRANCH_NAME }}
25 | token: ${{ secrets.PAT }}
26 |
27 | - name: Enable Corepack before setting up Node
28 | run: corepack enable
29 |
30 | - name: Setup Node
31 | uses: actions/setup-node@v4
32 | with:
33 | node-version: "20.x"
34 |
35 | - name: Authenticate to npm
36 | run: |
37 | echo "@niledatabase:registry=https://registry.npmjs.org/" >> .npmrc
38 | echo "registry=https://registry.npmjs.org/" >> .npmrc
39 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
40 | npm whoami
41 | env:
42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
43 |
44 | - name: Install @niledatabase/server
45 | working-directory: packages/server
46 | run: yarn install --immutable
47 |
48 | - name: Install @niledatabase/react
49 | working-directory: packages/react
50 | run: yarn install --immutable
51 |
52 | - name: Install @niledatabase/client
53 | working-directory: packages/client
54 | run: yarn install --immutable
55 |
56 | - name: Build @niledatabase/packages
57 | run: yarn build
58 |
59 | - name: Version
60 | run: |
61 | git config user.name "${{ github.actor }}"
62 | git config user.email "${{ github.actor }}@users.noreply.github.com"
63 | npx lerna version --force-publish --conventional-prerelease --conventional-commits --yes
64 | env:
65 | GITHUB_TOKEN: ${{ secrets.PAT }}
66 |
67 | - name: Publish to npm
68 | env:
69 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
70 | GITHUB_TOKEN: ${{ secrets.PAT }}
71 | run: |
72 | npx lerna publish from-git --yes --dist-tag alpha
73 |
74 | - name: Update npmrc for GitHub
75 | run: |
76 | rm -rf .npmrc
77 | echo "@niledatabase:registry=https://npm.pkg.github.com/" >> .npmrc
78 | echo "//npm.pkg.github.com/:_authToken=$NPM_TOKEN" >> .npmrc
79 | env:
80 | NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }}
81 | GITHUB_TOKEN: ${{ secrets.PAT }}
82 |
83 | - name: Publish to GitHub Packages
84 | run: npx lerna publish from-git --yes --registry=https://npm.pkg.github.com/
85 | env:
86 | GITHUB_TOKEN: ${{ secrets.PAT }}
87 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | pull_request:
5 | branches: "**"
6 | jobs:
7 | test:
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | lib: ["server", "react", "client"]
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Enable Corepack
16 | run: corepack enable
17 |
18 | - name: install Node.js
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version: lts/*
22 | cache: "yarn"
23 |
24 | - name: Install deps
25 | run: yarn install --immutable
26 |
27 | - name: build
28 | run: yarn build
29 |
30 | - name: test
31 | env:
32 | NILEDB_USER: ${{ secrets.NILEDB_USER }}
33 | NILEDB_PASSWORD: ${{ secrets.NILEDB_PASSWORD }}
34 | NILEDB_POSTGRES_URL: ${{ secrets.NILEDB_POSTGRES_URL }}
35 | NILEDB_API_URL: ${{ secrets.NILEDB_API_URL }}
36 | run: yarn test:${{matrix.lib}}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | .eslintcache
4 | .idea
5 |
6 |
7 | *.yarn/*
8 | .yarn/*
9 | !.yarn/patches
10 | !.yarn/plugins
11 | !.yarn/releases
12 | !.yarn/sdks
13 | !.yarn/versions
14 |
15 | # Swap the comments on the following lines if you don't wish to use zero-installs
16 | # Documentation here: https://yarnpkg.com/features/zero-installs
17 | # !.yarn/cache
18 | # !lib/nile/.yarn/cache
19 | #.pnp.*
20 | package-lock.json
21 | *.log
22 |
23 | storybook-static
24 | *.tsbuildinfo
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn lint-staged
5 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "arcanis.vscode-zipfs",
4 | "dbaeumer.vscode-eslint",
5 | "esbenp.prettier-vscode"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "search.exclude": {
3 | "**/.yarn": true,
4 | "**/.pnp.*": true
5 | },
6 | "eslint.nodePath": ".yarn/sdks",
7 | "prettier.prettierPath": ".yarn/sdks/prettier/index.js",
8 | "typescript.tsdk": ".yarn/sdks/typescript/lib",
9 | "typescript.enablePromptUseWorkspaceTsdk": true
10 | }
11 |
--------------------------------------------------------------------------------
/.yarn/sdks/eslint/bin/eslint.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire, register} = require(`module`);
5 | const {resolve} = require(`path`);
6 | const {pathToFileURL} = require(`url`);
7 |
8 | const relPnpApiPath = "../../../../.pnp.cjs";
9 |
10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
12 | const absRequire = createRequire(absPnpApiPath);
13 |
14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
16 |
17 | if (existsSync(absPnpApiPath)) {
18 | if (!process.versions.pnp) {
19 | // Setup the environment to be able to require eslint/bin/eslint.js
20 | require(absPnpApiPath).setup();
21 | if (isPnpLoaderEnabled && register) {
22 | register(pathToFileURL(absPnpLoaderPath));
23 | }
24 | }
25 | }
26 |
27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath)
28 | ? exports => absRequire(absUserWrapperPath)(exports)
29 | : exports => exports;
30 |
31 | // Defer to the real eslint/bin/eslint.js your application uses
32 | module.exports = wrapWithUserWrapper(absRequire(`eslint/bin/eslint.js`));
33 |
--------------------------------------------------------------------------------
/.yarn/sdks/eslint/lib/api.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire, register} = require(`module`);
5 | const {resolve} = require(`path`);
6 | const {pathToFileURL} = require(`url`);
7 |
8 | const relPnpApiPath = "../../../../.pnp.cjs";
9 |
10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
12 | const absRequire = createRequire(absPnpApiPath);
13 |
14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
16 |
17 | if (existsSync(absPnpApiPath)) {
18 | if (!process.versions.pnp) {
19 | // Setup the environment to be able to require eslint
20 | require(absPnpApiPath).setup();
21 | if (isPnpLoaderEnabled && register) {
22 | register(pathToFileURL(absPnpLoaderPath));
23 | }
24 | }
25 | }
26 |
27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath)
28 | ? exports => absRequire(absUserWrapperPath)(exports)
29 | : exports => exports;
30 |
31 | // Defer to the real eslint your application uses
32 | module.exports = wrapWithUserWrapper(absRequire(`eslint`));
33 |
--------------------------------------------------------------------------------
/.yarn/sdks/eslint/lib/unsupported-api.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire, register} = require(`module`);
5 | const {resolve} = require(`path`);
6 | const {pathToFileURL} = require(`url`);
7 |
8 | const relPnpApiPath = "../../../../.pnp.cjs";
9 |
10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
12 | const absRequire = createRequire(absPnpApiPath);
13 |
14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
16 |
17 | if (existsSync(absPnpApiPath)) {
18 | if (!process.versions.pnp) {
19 | // Setup the environment to be able to require eslint/use-at-your-own-risk
20 | require(absPnpApiPath).setup();
21 | if (isPnpLoaderEnabled && register) {
22 | register(pathToFileURL(absPnpLoaderPath));
23 | }
24 | }
25 | }
26 |
27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath)
28 | ? exports => absRequire(absUserWrapperPath)(exports)
29 | : exports => exports;
30 |
31 | // Defer to the real eslint/use-at-your-own-risk your application uses
32 | module.exports = wrapWithUserWrapper(absRequire(`eslint/use-at-your-own-risk`));
33 |
--------------------------------------------------------------------------------
/.yarn/sdks/eslint/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint",
3 | "version": "8.57.1-sdk",
4 | "main": "./lib/api.js",
5 | "type": "commonjs",
6 | "bin": {
7 | "eslint": "./bin/eslint.js"
8 | },
9 | "exports": {
10 | "./package.json": "./package.json",
11 | ".": "./lib/api.js",
12 | "./use-at-your-own-risk": "./lib/unsupported-api.js"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.yarn/sdks/integrations.yml:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by @yarnpkg/sdks.
2 | # Manual changes might be lost!
3 |
4 | integrations:
5 | - vscode
6 |
--------------------------------------------------------------------------------
/.yarn/sdks/prettier/bin-prettier.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire, register} = require(`module`);
5 | const {resolve} = require(`path`);
6 | const {pathToFileURL} = require(`url`);
7 |
8 | const relPnpApiPath = "../../../.pnp.cjs";
9 |
10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
12 | const absRequire = createRequire(absPnpApiPath);
13 |
14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
16 |
17 | if (existsSync(absPnpApiPath)) {
18 | if (!process.versions.pnp) {
19 | // Setup the environment to be able to require prettier/bin-prettier.js
20 | require(absPnpApiPath).setup();
21 | if (isPnpLoaderEnabled && register) {
22 | register(pathToFileURL(absPnpLoaderPath));
23 | }
24 | }
25 | }
26 |
27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath)
28 | ? exports => absRequire(absUserWrapperPath)(exports)
29 | : exports => exports;
30 |
31 | // Defer to the real prettier/bin-prettier.js your application uses
32 | module.exports = wrapWithUserWrapper(absRequire(`prettier/bin-prettier.js`));
33 |
--------------------------------------------------------------------------------
/.yarn/sdks/prettier/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire, register} = require(`module`);
5 | const {resolve} = require(`path`);
6 | const {pathToFileURL} = require(`url`);
7 |
8 | const relPnpApiPath = "../../../.pnp.cjs";
9 |
10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
12 | const absRequire = createRequire(absPnpApiPath);
13 |
14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
16 |
17 | if (existsSync(absPnpApiPath)) {
18 | if (!process.versions.pnp) {
19 | // Setup the environment to be able to require prettier
20 | require(absPnpApiPath).setup();
21 | if (isPnpLoaderEnabled && register) {
22 | register(pathToFileURL(absPnpLoaderPath));
23 | }
24 | }
25 | }
26 |
27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath)
28 | ? exports => absRequire(absUserWrapperPath)(exports)
29 | : exports => exports;
30 |
31 | // Defer to the real prettier your application uses
32 | module.exports = wrapWithUserWrapper(absRequire(`prettier`));
33 |
--------------------------------------------------------------------------------
/.yarn/sdks/prettier/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prettier",
3 | "version": "2.8.8-sdk",
4 | "main": "./index.js",
5 | "type": "commonjs",
6 | "bin": "./bin-prettier.js"
7 | }
8 |
--------------------------------------------------------------------------------
/.yarn/sdks/typescript/bin/tsc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire, register} = require(`module`);
5 | const {resolve} = require(`path`);
6 | const {pathToFileURL} = require(`url`);
7 |
8 | const relPnpApiPath = "../../../../.pnp.cjs";
9 |
10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
12 | const absRequire = createRequire(absPnpApiPath);
13 |
14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
16 |
17 | if (existsSync(absPnpApiPath)) {
18 | if (!process.versions.pnp) {
19 | // Setup the environment to be able to require typescript/bin/tsc
20 | require(absPnpApiPath).setup();
21 | if (isPnpLoaderEnabled && register) {
22 | register(pathToFileURL(absPnpLoaderPath));
23 | }
24 | }
25 | }
26 |
27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath)
28 | ? exports => absRequire(absUserWrapperPath)(exports)
29 | : exports => exports;
30 |
31 | // Defer to the real typescript/bin/tsc your application uses
32 | module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`));
33 |
--------------------------------------------------------------------------------
/.yarn/sdks/typescript/bin/tsserver:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire, register} = require(`module`);
5 | const {resolve} = require(`path`);
6 | const {pathToFileURL} = require(`url`);
7 |
8 | const relPnpApiPath = "../../../../.pnp.cjs";
9 |
10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
12 | const absRequire = createRequire(absPnpApiPath);
13 |
14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
16 |
17 | if (existsSync(absPnpApiPath)) {
18 | if (!process.versions.pnp) {
19 | // Setup the environment to be able to require typescript/bin/tsserver
20 | require(absPnpApiPath).setup();
21 | if (isPnpLoaderEnabled && register) {
22 | register(pathToFileURL(absPnpLoaderPath));
23 | }
24 | }
25 | }
26 |
27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath)
28 | ? exports => absRequire(absUserWrapperPath)(exports)
29 | : exports => exports;
30 |
31 | // Defer to the real typescript/bin/tsserver your application uses
32 | module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`));
33 |
--------------------------------------------------------------------------------
/.yarn/sdks/typescript/lib/tsc.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire, register} = require(`module`);
5 | const {resolve} = require(`path`);
6 | const {pathToFileURL} = require(`url`);
7 |
8 | const relPnpApiPath = "../../../../.pnp.cjs";
9 |
10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
12 | const absRequire = createRequire(absPnpApiPath);
13 |
14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
16 |
17 | if (existsSync(absPnpApiPath)) {
18 | if (!process.versions.pnp) {
19 | // Setup the environment to be able to require typescript/lib/tsc.js
20 | require(absPnpApiPath).setup();
21 | if (isPnpLoaderEnabled && register) {
22 | register(pathToFileURL(absPnpLoaderPath));
23 | }
24 | }
25 | }
26 |
27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath)
28 | ? exports => absRequire(absUserWrapperPath)(exports)
29 | : exports => exports;
30 |
31 | // Defer to the real typescript/lib/tsc.js your application uses
32 | module.exports = wrapWithUserWrapper(absRequire(`typescript/lib/tsc.js`));
33 |
--------------------------------------------------------------------------------
/.yarn/sdks/typescript/lib/typescript.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {existsSync} = require(`fs`);
4 | const {createRequire, register} = require(`module`);
5 | const {resolve} = require(`path`);
6 | const {pathToFileURL} = require(`url`);
7 |
8 | const relPnpApiPath = "../../../../.pnp.cjs";
9 |
10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath);
11 | const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
12 | const absRequire = createRequire(absPnpApiPath);
13 |
14 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
15 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
16 |
17 | if (existsSync(absPnpApiPath)) {
18 | if (!process.versions.pnp) {
19 | // Setup the environment to be able to require typescript
20 | require(absPnpApiPath).setup();
21 | if (isPnpLoaderEnabled && register) {
22 | register(pathToFileURL(absPnpLoaderPath));
23 | }
24 | }
25 | }
26 |
27 | const wrapWithUserWrapper = existsSync(absUserWrapperPath)
28 | ? exports => absRequire(absUserWrapperPath)(exports)
29 | : exports => exports;
30 |
31 | // Defer to the real typescript your application uses
32 | module.exports = wrapWithUserWrapper(absRequire(`typescript`));
33 |
--------------------------------------------------------------------------------
/.yarn/sdks/typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typescript",
3 | "version": "5.6.2-sdk",
4 | "main": "./lib/typescript.js",
5 | "type": "commonjs",
6 | "bin": {
7 | "tsc": "./bin/tsc",
8 | "tsserver": "./bin/tsserver"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | compressionLevel: mixed
2 |
3 | enableGlobalCache: false
4 |
5 | nodeLinker: node-modules
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Nile-JS
2 |
3 | Welcome to Nile-JS. We are glad you are interested in contributing to this project. We welcome contributions of all kinds, including bug reports, feature requests, code improvements, and documentation updates.
4 |
5 | ## Packages and Tech Stack
6 |
7 | The core of the SDK is in two packages:
8 |
9 | - **[Server](./packages/server/README.md)** - This package includes the configuration classes, methods and types for all Nile APIs (user management, tenant management, authentication), as well as a powerful query interface for working with your Nile database.
10 | - **[React](./packages/react/README.md)** - This package includes the drop-in UI components and convenient hooks for authentication, user management and tenant management.
11 |
12 | Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
13 |
14 | We use [`yarn`](https://yarnpkg.com/) (please avoid `npm`) for managing dependencies and builds.
15 |
16 | ## Getting Started
17 |
18 | You can read a more detailed guide in our [documentation](https://thenile.dev/docs/auth/contributing/develop)
19 |
20 | 1. **Fork the Repository**
21 | Click the "Fork" button on GitHub to create your own copy of the repo.
22 |
23 | 2. **Clone the Repo**
24 |
25 | ```sh
26 | git clone https://github.com/your-username/nile-js.git
27 | cd nile-auth
28 | ```
29 |
30 | 3. **Install Dependencies**
31 |
32 | ```sh
33 | yarn install
34 | ```
35 |
36 | ## Reporting issues
37 |
38 | Whether you run into issues with Nile Auth itself or while attempting to contribute, we are here for you.
39 |
40 | - **GitHub Issues** – Report bugs or request features in our [discussion board](https://github.com/orgs/niledatabase/discussions).
41 | - **Discord** – Join our developer community [here](https://discord.com/invite/8UuBB84tTy).
42 |
43 | If you run into security issues, we prefer you contact [support@thenile.dev](mailto:support@thenile.dev) privately. We'll look into it with priority and give you full credit for discovery.
44 |
45 | ## Contribution Guidelines
46 |
47 | - **Feature Requests & Issues**: Open a GitHub issue to discuss before starting work.
48 | - **Pull Requests**:
49 | - Create a feature branch (`git checkout -b feature/your-feature`).
50 | - Follow existing code style and linting rules.
51 | - Add tests where applicable.
52 | - Submit a PR with a clear description.
53 | - **Code of Conduct**: Be respectful and constructive in discussions.
54 |
55 | ## Testing
56 |
57 | Review our [testing guide](https://thenile.dev/docs/auth/contributing/testing) for suggestions on how to test.
58 |
59 | Happy coding! 🚀
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 The Nile Platform
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.
22 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": ["packages/react", "packages/client", "packages/server"],
3 | "version": "5.0.0-alpha.3",
4 | "npmClient": "yarn",
5 | "useWorkspaces": true,
6 | "command": {
7 | "publish": {
8 | "conventionalCommits": true,
9 | "yes": true,
10 | "access": "public"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@niledatabase/packages",
3 | "license": "MIT",
4 | "version": "0.0.2",
5 | "private": true,
6 | "workspaces": [
7 | "packages/*"
8 | ],
9 | "scripts": {
10 | "build:storybook": "yarn build && yarn workspace @niledatabase/react build:storybook",
11 | "dev:react": "yarn workspace @niledatabase/react dev",
12 | "build:react": "yarn workspace @niledatabase/react build",
13 | "build:client": "yarn workspace @niledatabase/client build",
14 | "build:server": "yarn workspace @niledatabase/server build",
15 | "build": "yarn build:server && yarn build:react && yarn build:client",
16 | "lint": "yarn eslint . --max-warnings=0",
17 | "postinstall": "husky install",
18 | "prepare": "husky install",
19 | "publish": "yarn lerna publish",
20 | "test:react": "yarn workspace @niledatabase/react test",
21 | "test:server": "yarn workspace @niledatabase/server test",
22 | "test:client": "yarn workspace @niledatabase/client test"
23 | },
24 | "resolutions": {
25 | "@types/mime": "3.0.4",
26 | "@nestjs/common": "^10.4.16"
27 | },
28 | "devDependencies": {
29 | "@commitlint/cli": "17.8.1",
30 | "@commitlint/config-conventional": "17.8.1",
31 | "@types/mime": "^4.0.0",
32 | "@typescript-eslint/eslint-plugin": "^5.62.0",
33 | "@typescript-eslint/parser": "^5.62.0",
34 | "eslint": "^8.54.0",
35 | "eslint-config-next": "^13.5.6",
36 | "eslint-config-prettier": "^8.10.0",
37 | "eslint-plugin-import": "^2.29.0",
38 | "eslint-plugin-prettier": "^4.2.1",
39 | "eslint-plugin-react": "^7.37.0",
40 | "eslint-plugin-react-hooks": "^4.6.0",
41 | "eslint-plugin-storybook": "^0.6.15",
42 | "husky": "8.0.3",
43 | "lerna": "^6.6.2",
44 | "lint-staged": "^13.3.0",
45 | "prettier": "^2.8.8",
46 | "prettier-eslint": "^15.0.1",
47 | "tslint-config-prettier": "^1.18.0",
48 | "typescript": "^5.5.0"
49 | },
50 | "lint-staged": {
51 | "packages/**/**/*.{mjs,js,ts,jsx,tsx}": "yarn lint --cache --fix ."
52 | },
53 | "packageManager": "yarn@4.5.0"
54 | }
55 |
--------------------------------------------------------------------------------
/packages/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "sourceType": "unambiguous",
3 | "presets": [
4 | [
5 | "@babel/preset-env",
6 | {
7 | "targets": {
8 | "chrome": 100,
9 | "node": "current"
10 | }
11 | }
12 | ],
13 | "@babel/preset-typescript",
14 | "@babel/preset-react"
15 | ],
16 | "plugins": []
17 | }
18 |
--------------------------------------------------------------------------------
/packages/client/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 | yarn.lock
7 | *storybook.log
8 | .storybook/output.css
9 | .yarn
--------------------------------------------------------------------------------
/packages/client/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | # [5.0.0-alpha.3](https://github.com/niledatabase/nile-js/compare/v5.0.0-alpha.2...v5.0.0-alpha.3) (2025-05-30)
7 |
8 | ### Bug Fixes
9 |
10 | - **server:** forgot password ([f26917f](https://github.com/niledatabase/nile-js/commit/f26917ff2f83d5edab3fdcbea26f66bf95ce998a))
11 |
12 | # [5.0.0-alpha.2](https://github.com/niledatabase/nile-js/compare/v5.0.0-alpha.1...v5.0.0-alpha.2) (2025-05-29)
13 |
14 | ### Features
15 |
16 | - **client:** remove web, make client ([455ba97](https://github.com/niledatabase/nile-js/commit/455ba97d5923743f6880b5ad8f8d2b714d649373))
17 |
--------------------------------------------------------------------------------
/packages/client/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ---
6 |
7 | # Nile's Client SDK
8 |
9 | This package (`@niledatabase/client`) is part of [Nile's Javascript SDK](https://github.com/niledatabase/nile-js/tree/main).
10 |
11 | Nile's React package provides:
12 |
13 | - 🎨 UI components for authentication, user management, and tenant management (customizable with Tailwind CSS)
14 | - 🪝 React hooks for authentication, user management, and tenant management functionality
15 |
16 | You can browse all the components and explore their properties in [Nile's documentation](https://www.thenile.dev/docs/auth/components/signin) or in [Storybook](https://storybook.thenile.dev).
17 |
18 | The components and hooks are designed to work best and provide a secure user experience when used with the generated routes provided by [Nile's Server-Side SDK](https://www.npmjs.com/package/@niledatabase/server).
19 |
20 | **Nile is a Postgres platform that decouples storage from compute, virtualizes tenants, and supports vertical and horizontal scaling globally to ship B2B applications fast while being safe with limitless scale.** All B2B applications are multi-tenant. A tenant/customer is primarily a company, an organization, or a workspace in your product that contains a group of users. A B2B application provides services to multiple tenants. Tenant is the basic building block of all B2B applications.
21 |
22 | ## Usage
23 |
24 | ### Installation
25 |
26 | ```bash
27 | npm install @niledatabase/client
28 | ```
29 |
30 | ### Social Login (SSO)
31 |
32 | Nile-Auth supports multiple social providers. You configure and enable them in [Nile console](https://console.thenile.dev), and then simply drop-in the components. For example, for Discord authentication:
33 |
34 | ## Learn more
35 |
36 | - You can learn more about Nile and the SDK in [https://thenile.dev/docs]
37 | - You can find detailed code examples in [our main repo](https://github.com/niledatabase/niledatabase)
38 | - Nile SDK interacts with APIs in Nile Auth service. You can learn more about it in the [repository](https://github.com/niledatabase/nile-auth) and the [docs](https://thenile.dev/docs/auth)
39 |
--------------------------------------------------------------------------------
/packages/client/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'jsdom',
5 | };
6 |
--------------------------------------------------------------------------------
/packages/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@niledatabase/client",
3 | "version": "5.0.0-alpha.3",
4 | "license": "MIT",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs"
12 | }
13 | },
14 | "files": [
15 | "dist"
16 | ],
17 | "engines": {
18 | "node": ">=20.0"
19 | },
20 | "scripts": {
21 | "build": "tsup src/index.ts",
22 | "dev:local": "tsup src/index.ts --watch",
23 | "test": "yarn jest"
24 | },
25 | "prettier": {
26 | "printWidth": 80,
27 | "semi": true,
28 | "singleQuote": true,
29 | "trailingComma": "es5"
30 | },
31 | "author": "jrea",
32 | "repository": {
33 | "type": "git",
34 | "url": "https://github.com/niledatabase/nile-js.git",
35 | "directory": "packages/client"
36 | },
37 | "publishConfig": {
38 | "access": "public"
39 | },
40 | "devDependencies": {
41 | "@babel/core": "^7.23.3",
42 | "@babel/preset-env": "^7.23.3",
43 | "@babel/preset-react": "^7.23.3",
44 | "@babel/preset-typescript": "^7.23.3",
45 | "@rollup/plugin-babel": "^6.0.4",
46 | "@testing-library/jest-dom": "^5.17.0",
47 | "@testing-library/react": "^14.1.2",
48 | "@types/jest": "^29.5.9",
49 | "@types/react": "^19.0.0",
50 | "@types/react-dom": "^19.0.0",
51 | "@typescript-eslint/parser": "^6.12.0",
52 | "babel-jest": "29.7.0",
53 | "babel-loader": "^9.1.3",
54 | "jest": "^29.7.0",
55 | "jest-environment-jsdom": "^29.7.0",
56 | "ts-jest": "^29.1.1",
57 | "tslib": "^2.6.2",
58 | "tsup": "^8.3.0",
59 | "typescript": "^5.3.2"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/client/src/broadcast.ts:
--------------------------------------------------------------------------------
1 | /** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
2 | export function now() {
3 | return Math.floor(Date.now() / 1000);
4 | }
5 |
6 | export interface BroadcastMessage {
7 | event?: 'session';
8 | data?: { trigger?: 'signout' | 'getSession' };
9 | clientId: string;
10 | timestamp: number;
11 | }
12 |
13 | /**
14 | * Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
15 | * Only not using it directly, because Safari does not support it.
16 | *
17 | * https://caniuse.com/?search=broadcastchannel
18 | */
19 | export function BroadcastChannel(name = 'nextauth.message') {
20 | return {
21 | /** Get notified by other tabs/windows. */
22 | receive(onReceive: (message: BroadcastMessage) => void) {
23 | const handler = (event: StorageEvent) => {
24 | if (event.key !== name) return;
25 | const message: BroadcastMessage = JSON.parse(event.newValue ?? '{}');
26 | if (message?.event !== 'session' || !message?.data) return;
27 |
28 | onReceive(message);
29 | };
30 | window.addEventListener('storage', handler);
31 | return () => window.removeEventListener('storage', handler);
32 | },
33 | /** Notify other tabs/windows. */
34 | post(message: Record) {
35 | if (typeof window === 'undefined') return;
36 | try {
37 | localStorage.setItem(
38 | name,
39 | JSON.stringify({ ...message, timestamp: now() })
40 | );
41 | } catch {
42 | /**
43 | * The localStorage API isn't always available.
44 | * It won't work in private mode prior to Safari 11 for example.
45 | * Notifications are simply dropped if an error is encountered.
46 | */
47 | }
48 | },
49 | };
50 | }
51 |
52 | export const broadcast = BroadcastChannel();
53 |
--------------------------------------------------------------------------------
/packages/client/src/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | auth,
3 | getSession,
4 | getCsrfToken,
5 | signOut,
6 | signIn,
7 | signUp,
8 | resetPassword,
9 | getProviders,
10 | default as Authorizer,
11 | } from './Authorizer';
12 | export { getStatus } from './status';
13 | export { broadcast } from './broadcast';
14 | export * from './types';
15 |
--------------------------------------------------------------------------------
/packages/client/src/observable.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | function isEqual(a: any, b: any): boolean {
3 | if (a === b) return true; // Same reference or primitive value
4 |
5 | if (
6 | typeof a !== 'object' ||
7 | typeof b !== 'object' ||
8 | a === null ||
9 | b === null
10 | ) {
11 | return false; // If one of them is not an object (or is null), return false
12 | }
13 |
14 | if (Array.isArray(a) !== Array.isArray(b)) return false; // One is an array, the other isn't
15 |
16 | const keysA = Object.keys(a);
17 | const keysB = Object.keys(b);
18 |
19 | if (keysA.length !== keysB.length) return false; // Different number of keys
20 |
21 | for (const key of keysA) {
22 | if (!keysB.includes(key) || !isEqual(a[key], b[key])) {
23 | return false; // Key missing or values are not deeply equal
24 | }
25 | }
26 |
27 | return true;
28 | }
29 |
30 | import { Listener } from './types';
31 |
32 | export function createObservableObject>(
33 | obj: T,
34 | listenerKeys = ['loading', 'session'],
35 | eventName = 'objectChange'
36 | ) {
37 | const eventTarget = new EventTarget();
38 | const listeners = new Map();
39 |
40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
41 | const handler: ProxyHandler = {
42 | set(target, key, value) {
43 | const prev = target[key];
44 | target[key] = value;
45 | if (isEqual(prev, value)) return true;
46 |
47 | // only fire on these two for now
48 | if (listenerKeys.includes(String(key))) {
49 | eventTarget.dispatchEvent(
50 | new CustomEvent(eventName, {
51 | detail: { key, prev, next: value },
52 | })
53 | );
54 | }
55 | return true;
56 | },
57 | };
58 |
59 | return {
60 | proxy: new Proxy(obj, handler),
61 | eventTarget,
62 | addListener(callback: Listener) {
63 | if (listeners.has(callback)) {
64 | return;
65 | }
66 | const wrappedCallback = (e: Event) => callback((e as CustomEvent).detail);
67 | listeners.set(callback, wrappedCallback);
68 |
69 | eventTarget.addEventListener(eventName, wrappedCallback);
70 | },
71 | removeListener(callback: Listener) {
72 | const wrappedCallback = listeners.get(callback);
73 | if (wrappedCallback) {
74 | eventTarget.removeEventListener(eventName, wrappedCallback);
75 | listeners.delete(callback);
76 | }
77 | },
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/packages/client/src/status.ts:
--------------------------------------------------------------------------------
1 | import { NonErrorSession } from './types';
2 |
3 | export function getStatus(
4 | load: boolean,
5 | sess: NonErrorSession | null | undefined
6 | ) {
7 | if (load) {
8 | return 'loading';
9 | }
10 | if (sess) {
11 | return 'authenticated';
12 | }
13 | return 'unauthenticated';
14 | }
15 |
--------------------------------------------------------------------------------
/packages/client/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "include": ["src/**/*"],
4 | "compilerOptions": {
5 | "baseUrl": "./src",
6 | "declaration": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
4 | "include": ["src/**/*"],
5 | "compilerOptions": {
6 | "jsx": "react",
7 | "lib": ["dom", "esnext"],
8 | "esModuleInterop": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/client/tsup.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig([
4 | {
5 | entry: ['src/index.ts'],
6 | format: ['esm', 'cjs'],
7 | dts: {
8 | entry: 'src/index.ts',
9 | },
10 | outDir: 'dist',
11 | splitting: false,
12 | sourcemap: true,
13 | clean: true,
14 | target: 'node20',
15 | },
16 | ]);
17 |
--------------------------------------------------------------------------------
/packages/react/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "sourceType": "unambiguous",
3 | "presets": [
4 | [
5 | "@babel/preset-env",
6 | {
7 | "targets": {
8 | "chrome": 100,
9 | "node": "current"
10 | }
11 | }
12 | ],
13 | "@babel/preset-typescript",
14 | "@babel/preset-react"
15 | ],
16 | "plugins": []
17 | }
18 |
--------------------------------------------------------------------------------
/packages/react/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 | yarn.lock
7 | *storybook.log
8 | .storybook/output.css
9 | .yarn
--------------------------------------------------------------------------------
/packages/react/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import { join, dirname } from 'path';
2 | import { createRequire } from 'module';
3 |
4 | const require = createRequire(import.meta.url);
5 | import type { StorybookConfig } from '@storybook/react-webpack5';
6 |
7 | /**
8 | * This function is used to resolve the absolute path of a package.
9 | * It is needed in projects that use Yarn PnP or are set up within a monorepo.
10 | */
11 | function getAbsolutePath(value: string): string {
12 | return dirname(require.resolve(join(value, 'package.json')));
13 | }
14 | const config: StorybookConfig = {
15 | stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
16 | addons: [
17 | getAbsolutePath('@storybook/addon-webpack5-compiler-swc'),
18 | getAbsolutePath('@storybook/addon-onboarding'),
19 | getAbsolutePath('@storybook/addon-links'),
20 | getAbsolutePath('@storybook/addon-essentials'),
21 | getAbsolutePath('@chromatic-com/storybook'),
22 | getAbsolutePath('@storybook/addon-interactions'),
23 | getAbsolutePath('@storybook/addon-themes'),
24 | ],
25 | framework: {
26 | name: getAbsolutePath('@storybook/react-webpack5'),
27 | options: {},
28 | },
29 | };
30 | export default config;
31 |
--------------------------------------------------------------------------------
/packages/react/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | import './output.css';
2 | import type { Preview } from '@storybook/react';
3 | import { withThemeByClassName } from '@storybook/addon-themes';
4 |
5 | const preview: Preview = {
6 | decorators: [
7 | withThemeByClassName({
8 | themes: { light: 'light', dark: 'dark' },
9 | defaultTheme: 'dark',
10 | }),
11 | ],
12 | };
13 |
14 | export default preview;
15 |
--------------------------------------------------------------------------------
/packages/react/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 jrea
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.
--------------------------------------------------------------------------------
/packages/react/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": ".storybook/global.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "packages/react/components",
15 | "utils": "packages/react/lib/utils",
16 | "ui": "packages/react/components/ui",
17 | "lib": "packages/react/lib",
18 | "hooks": "packages/react/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/packages/react/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slottable, Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 | import { Loader2 } from 'lucide-react';
5 |
6 | import { cn } from '../../lib/utils';
7 |
8 | const buttonVariants = cva(
9 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 gap-1',
10 | {
11 | variants: {
12 | variant: {
13 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
16 | outline:
17 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20 | ghost: 'hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 hover:underline',
22 | },
23 | size: {
24 | default: 'h-10 px-4 py-2',
25 | sm: 'h-9 rounded-md px-3',
26 | lg: 'h-11 rounded-md px-8',
27 | icon: 'h-10 w-10',
28 | },
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default',
33 | },
34 | }
35 | );
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean;
41 | loading?: boolean;
42 | }
43 |
44 | const Button = React.forwardRef(
45 | (
46 | {
47 | asChild = false,
48 | children,
49 | className,
50 | disabled,
51 | loading,
52 | size,
53 | variant,
54 | ...props
55 | },
56 | ref
57 | ) => {
58 | const Comp = asChild ? Slot : 'button';
59 | return (
60 |
66 |
67 | {loading ? (
68 |
69 |
70 |
71 |
72 |
{children}
73 |
74 | ) : (
75 | children
76 | )}
77 |
78 |
79 | );
80 | }
81 | );
82 |
83 | Button.displayName = 'Button';
84 |
85 | export { Button, buttonVariants };
86 |
--------------------------------------------------------------------------------
/packages/react/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '../../lib/utils';
4 |
5 | export type InputProps = React.InputHTMLAttributes;
6 |
7 | const Input = React.forwardRef(
8 | ({ className, type, ...props }, ref) => {
9 | return (
10 |
19 | );
20 | }
21 | );
22 | Input.displayName = 'Input';
23 |
24 | export { Input };
25 |
--------------------------------------------------------------------------------
/packages/react/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as LabelPrimitive from '@radix-ui/react-label';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '../../lib/utils';
6 |
7 | const labelVariants = cva(
8 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
9 | );
10 |
11 | const Label = React.forwardRef<
12 | React.ComponentRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ));
22 | Label.displayName = LabelPrimitive.Root.displayName;
23 |
24 | export { Label };
25 |
--------------------------------------------------------------------------------
/packages/react/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
5 |
6 | import { cn } from '../../lib/utils';
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ComponentRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
19 |
28 |
29 | ));
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
31 |
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
33 |
--------------------------------------------------------------------------------
/packages/react/dts.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const svgr = require('@svgr/rollup');
3 |
4 | module.exports = {
5 | rollup(config) {
6 | config.plugins.push(svgr());
7 | return config;
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/packages/react/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'jsdom',
5 | };
6 |
--------------------------------------------------------------------------------
/packages/react/src/AzureSignInButton/AzureSignInButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from './index';
4 |
5 | const meta = {
6 | title: 'Social/Azure',
7 | component: Button,
8 | };
9 |
10 | export default meta;
11 |
12 | export function AzureSSO() {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/react/src/AzureSignInButton/AzureSignInButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import Button from './index';
5 |
6 | describe('Azure sso button', () => {
7 | it('renders using the context', () => {
8 | render();
9 | screen.getByText('Continue with Microsoft');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/react/src/AzureSignInButton/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { Slot } from '@radix-ui/react-slot';
5 | import { signIn } from '@niledatabase/client';
6 |
7 | import { cn } from '../../lib/utils';
8 | import { buttonVariants, ButtonProps } from '../../components/ui/button';
9 | import { SSOButtonProps } from '../types';
10 |
11 | const AzureSignInButton = ({
12 | callbackUrl,
13 | className,
14 | buttonText = 'Continue with Microsoft',
15 | variant,
16 | size,
17 | init,
18 | asChild = false,
19 | auth,
20 | fetchUrl,
21 | baseUrl,
22 | onClick,
23 | ...props
24 | }: ButtonProps & SSOButtonProps) => {
25 | const Comp = asChild ? Slot : 'button';
26 | return (
27 | {
34 | const res = await signIn('azure-ad', {
35 | callbackUrl,
36 | init,
37 | auth,
38 | fetchUrl,
39 | baseUrl,
40 | });
41 | onClick && onClick(e, res);
42 | }}
43 | {...props}
44 | >
45 |
46 | {buttonText}
47 |
48 | );
49 | };
50 |
51 | AzureSignInButton.displayName = 'AzureSignInButton';
52 | export default AzureSignInButton;
53 |
54 | const MicrosoftIcon = () => {
55 | return (
56 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/packages/react/src/DiscordSignInButton/DiscordSignInButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from './index';
4 |
5 | const meta = {
6 | title: 'Social/Discord',
7 | component: Button,
8 | };
9 |
10 | export default meta;
11 |
12 | export function DiscordSSO() {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/react/src/DiscordSignInButton/DiscordSignInButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import Button from './index';
5 |
6 | describe('Discord sso button', () => {
7 | it('renders using the context', () => {
8 | render();
9 | screen.getByText('Continue with Discord');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/react/src/EmailSignIn/EmailSignInButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import EmailSignIn from './Form';
4 | import EmailSignInButton from './EmailSignInButton';
5 |
6 | const meta = {
7 | title: 'Social/Email',
8 | component: EmailSignIn,
9 | };
10 |
11 | export default meta;
12 |
13 | export function SignInWithEmail() {
14 | return (
15 |
16 | {
18 | // noop
19 | }}
20 | />
21 |
22 | );
23 | }
24 |
25 | export function VerifyEmailAddress() {
26 | return (
27 |
28 | Hello user, before you continue, you need to verify your email address.
29 |
30 | {
35 | // do something
36 | }}
37 | onFailure={(e) => {
38 | alert(e?.error);
39 | }}
40 | >
41 | Verify my email address
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/packages/react/src/EmailSignIn/EmailSignInButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import React from 'react';
4 | import { Mail } from 'lucide-react';
5 | import { signIn } from '@niledatabase/client';
6 |
7 | import { ButtonProps, buttonVariants } from '../../components/ui/button';
8 | import { cn } from '../../lib/utils';
9 | import { SSOButtonProps } from '../types';
10 |
11 | type EmailError = void | {
12 | error: string;
13 | ok: boolean;
14 | status: number;
15 | url: null | string;
16 | };
17 | type AllProps = ButtonProps &
18 | SSOButtonProps & {
19 | callbackUrl?: string;
20 | redirect?: boolean;
21 | email: string;
22 | onSent?: () => void;
23 | onFailure?: (error: EmailError) => void;
24 | buttonText?: string;
25 | };
26 |
27 | /**
28 | * This works when the email identity provider is configured in the admin dashboard.
29 | * @param props callbackUrl: the url to send the user to from their email
30 | * @param props redirect: redirect to the default (unbranded) 'check your email' page. default is false
31 | * @param props email: the email to send to
32 | * @param props onSent: called if the email was sent
33 | * @param props onFailure: called if there was a reportable
34 | * @returns a JSX.Element to render
35 | */
36 |
37 | const EmailSignInButton = ({
38 | callbackUrl,
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | redirect = false,
44 | buttonText = 'Continue with Email',
45 | email,
46 | onFailure,
47 | onSent,
48 | fetchUrl,
49 | baseUrl,
50 | auth,
51 | ...props
52 | }: AllProps) => {
53 | const Comp = asChild ? Slot : 'button';
54 | return (
55 | {
59 | const res = await signIn('email', {
60 | email,
61 | callbackUrl,
62 | redirect,
63 | fetchUrl,
64 | baseUrl,
65 | auth,
66 | });
67 |
68 | if (res && 'error' in res) {
69 | onFailure && onFailure(res as EmailError);
70 | } else {
71 | onSent && onSent();
72 | }
73 | }}
74 | {...props}
75 | >
76 | {props.children ? (
77 | props.children
78 | ) : (
79 |
80 |
81 | {buttonText}
82 |
83 | )}
84 |
85 | );
86 | };
87 |
88 | EmailSignInButton.displayName = 'EmailSignInButton';
89 | export default EmailSignInButton;
90 |
--------------------------------------------------------------------------------
/packages/react/src/EmailSignIn/Form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4 | import { useForm } from 'react-hook-form';
5 | import { Mail } from 'lucide-react';
6 |
7 | import { Email, Form } from '../../components/ui/form';
8 | import { Button } from '../../components/ui/button';
9 |
10 | import { EmailSignInInfo, Props } from './types';
11 | import { useEmailSignIn } from './hooks';
12 |
13 | const queryClient = new QueryClient();
14 |
15 | export default function EmailSigningIn(props: Props) {
16 | const { client, ...remaining } = props ?? {};
17 | return (
18 |
19 |
20 |
21 | );
22 | }
23 | export function EmailSignInForm(props: Props & EmailSignInInfo) {
24 | const signIn = useEmailSignIn(props);
25 | const form = useForm({ defaultValues: { email: '' } });
26 | return (
27 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/packages/react/src/EmailSignIn/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query';
2 | import { signIn } from '@niledatabase/client';
3 |
4 | import { Props } from './types';
5 |
6 | export function useEmailSignIn(params?: Props) {
7 | const {
8 | onSuccess,
9 | onError,
10 | beforeMutate,
11 | callbackUrl,
12 | redirect = false,
13 | init,
14 | } = params ?? {};
15 | const mutation = useMutation({
16 | mutationFn: async (_data) => {
17 | const d = { ..._data, callbackUrl, redirect };
18 | const possibleData = beforeMutate && beforeMutate(d);
19 | const data = possibleData ?? d;
20 | const res = await signIn('email', { init, ...data });
21 | if (res?.error) {
22 | throw new Error(res.error);
23 | }
24 | return res as unknown as Response;
25 | },
26 | onSuccess,
27 | onError,
28 | });
29 | return mutation.mutate;
30 | }
31 |
--------------------------------------------------------------------------------
/packages/react/src/EmailSignIn/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './Form';
2 | export { default as EmailSignInButton } from './EmailSignInButton';
3 | export { useEmailSignIn } from './hooks';
4 |
--------------------------------------------------------------------------------
/packages/react/src/EmailSignIn/types.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from '@tanstack/react-query';
2 | import { SignInOptions } from '@niledatabase/client';
3 |
4 | export type EmailSignInInfo = SignInOptions;
5 | type SignInSuccess = (response: Response) => void;
6 |
7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
8 | export type AllowedAny = any;
9 |
10 | export type Props = {
11 | redirect?: boolean;
12 | onSuccess?: SignInSuccess;
13 | onError?: (e: Error, info: EmailSignInInfo) => void;
14 | beforeMutate?: (data: AllowedAny) => AllowedAny;
15 | buttonText?: string;
16 | client?: QueryClient;
17 | callbackUrl?: string;
18 | init?: RequestInit;
19 | };
20 |
--------------------------------------------------------------------------------
/packages/react/src/GitHubSignInButton/GitHubSignInButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from './index';
4 |
5 | const meta = {
6 | title: 'Social/GitHub',
7 | component: Button,
8 | };
9 |
10 | export default meta;
11 |
12 | export function GitHubSSO() {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/react/src/GitHubSignInButton/GitHubSignInButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import Button from './index';
5 |
6 | describe('GitHub sso button', () => {
7 | it('renders using the context', () => {
8 | render();
9 | screen.getByText('Continue with GitHub');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/react/src/GitHubSignInButton/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { Slot } from '@radix-ui/react-slot';
5 | import { signIn } from '@niledatabase/client';
6 |
7 | import { cn } from '../../lib/utils';
8 | import { buttonVariants, ButtonProps } from '../../components/ui/button';
9 | import { SSOButtonProps } from '../types';
10 |
11 | const GitHubSignInButton = ({
12 | callbackUrl,
13 | className,
14 | buttonText = 'Continue with GitHub',
15 | variant,
16 | size,
17 | init,
18 | asChild = false,
19 | auth,
20 | fetchUrl,
21 | baseUrl,
22 | onClick,
23 | ...props
24 | }: ButtonProps & SSOButtonProps) => {
25 | const Comp = asChild ? Slot : 'button';
26 | return (
27 | {
34 | const res = await signIn('github', {
35 | callbackUrl,
36 | init,
37 | auth,
38 | fetchUrl,
39 | baseUrl,
40 | });
41 | onClick && onClick(e, res);
42 | }}
43 | {...props}
44 | >
45 |
46 | {buttonText}
47 |
48 | );
49 | };
50 |
51 | GitHubSignInButton.displayName = 'GitHubSignInButton';
52 | export default GitHubSignInButton;
53 |
54 | const Icon = () => {
55 | return (
56 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/packages/react/src/GoogleLoginButton/GoogleLoginButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import GoogleLoginButton from './GoogleLoginButton';
4 |
5 | const meta = {
6 | title: 'Social/Google',
7 | component: GoogleLoginButton,
8 | };
9 |
10 | export default meta;
11 |
12 | export function GoogleSSO() {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/react/src/GoogleLoginButton/GoogleSSOButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import GoogleSSOButton from './GoogleLoginButton';
5 |
6 | describe('google sso button', () => {
7 | it('renders using the context', () => {
8 | render();
9 | screen.getByText('Continue with Google');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/react/src/GoogleLoginButton/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './GoogleLoginButton';
2 |
--------------------------------------------------------------------------------
/packages/react/src/HubSpotSignInButton/HubSpotSignInButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from './index';
4 |
5 | const meta = {
6 | title: 'Social/HubSpot',
7 | component: Button,
8 | };
9 |
10 | export default meta;
11 |
12 | export function HubSpotSSO() {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/react/src/HubSpotSignInButton/HubSpotSignInButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import Button from './index';
5 |
6 | describe('hubspot sso button', () => {
7 | it('renders using the context', () => {
8 | render();
9 | screen.getByText('Continue with HubSpot');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/react/src/HubSpotSignInButton/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { Slot } from '@radix-ui/react-slot';
5 | import { signIn } from '@niledatabase/client';
6 |
7 | import { cn } from '../../lib/utils';
8 | import { buttonVariants, ButtonProps } from '../../components/ui/button';
9 | import { SSOButtonProps } from '../types';
10 |
11 | const HubSpotSignInButton = ({
12 | callbackUrl,
13 | className,
14 | buttonText = 'Continue with HubSpot',
15 | variant,
16 | size,
17 | init,
18 | asChild = false,
19 | auth,
20 | fetchUrl,
21 | baseUrl,
22 | onClick,
23 | ...props
24 | }: ButtonProps & SSOButtonProps) => {
25 | const Comp = asChild ? Slot : 'button';
26 | return (
27 | {
34 | const res = await signIn('hubspot', {
35 | callbackUrl,
36 | init,
37 | auth,
38 | fetchUrl,
39 | baseUrl,
40 | });
41 | onClick && onClick(e, res);
42 | }}
43 | {...props}
44 | >
45 |
46 | {buttonText}
47 |
48 | );
49 | };
50 |
51 | HubSpotSignInButton.displayName = 'HubSpotSignInButton';
52 | export default HubSpotSignInButton;
53 |
54 | const Icon = () => {
55 | return (
56 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/packages/react/src/LinkedInSignInButton/LinkedInSignInButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from './index';
4 |
5 | const meta = {
6 | title: 'Social/LinkedIn',
7 | component: Button,
8 | };
9 |
10 | export default meta;
11 |
12 | export function LinkedInSSO() {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/react/src/LinkedInSignInButton/LinkedInSignInButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import Button from './index';
5 |
6 | describe('LinkedIn sso button', () => {
7 | it('renders using the context', () => {
8 | render();
9 | screen.getByText('Continue with LinkedIn');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/react/src/LinkedInSignInButton/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { Slot } from '@radix-ui/react-slot';
5 | import { signIn } from '@niledatabase/client';
6 |
7 | import { cn } from '../../lib/utils';
8 | import { buttonVariants, ButtonProps } from '../../components/ui/button';
9 | import { SSOButtonProps } from '../types';
10 |
11 | const LinkedInSignInButton = ({
12 | callbackUrl,
13 | className,
14 | buttonText = 'Continue with LinkedIn',
15 | variant,
16 | size,
17 | asChild = false,
18 | init,
19 | auth,
20 | fetchUrl,
21 | baseUrl,
22 | onClick,
23 | ...props
24 | }: ButtonProps & SSOButtonProps) => {
25 | const Comp = asChild ? Slot : 'button';
26 | return (
27 | {
34 | const res = await signIn('linkedin', {
35 | callbackUrl,
36 | init,
37 | auth,
38 | fetchUrl,
39 | baseUrl,
40 | });
41 | onClick && onClick(e, res);
42 | }}
43 | {...props}
44 | >
45 |
46 | {buttonText}
47 |
48 | );
49 | };
50 |
51 | LinkedInSignInButton.displayName = 'LinkedInSignInButton';
52 | export default LinkedInSignInButton;
53 |
54 | const Icon = () => {
55 | return (
56 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/packages/react/src/SignInForm/Form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4 | import { useForm } from 'react-hook-form';
5 |
6 | import { Button } from '../../components/ui/button';
7 | import { Email, Form, Password } from '../../components/ui/form';
8 |
9 | import { useSignIn } from './hooks';
10 | import { Props } from './types';
11 |
12 | const queryClient = new QueryClient();
13 |
14 | export default function SigningIn(props: Props) {
15 | const { client, ...remaining } = props ?? {};
16 | return (
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | export function SignInForm(props: Props) {
24 | const signIn = useSignIn(props);
25 | const form = useForm({ defaultValues: { email: '', password: '' } });
26 |
27 | return (
28 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/packages/react/src/SignInForm/SignInForm.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import SignIn from '.';
5 |
6 | const meta: Meta = {
7 | title: 'Sign in form',
8 | component: SignIn,
9 | };
10 |
11 | export default meta;
12 |
13 | export function SignInForm() {
14 | return ;
15 | }
16 |
--------------------------------------------------------------------------------
/packages/react/src/SignInForm/SignInForm.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3 |
4 | import { useSignIn } from './hooks';
5 | import SigningIn from './Form';
6 |
7 | // Mock dependencies
8 | jest.mock('./hooks', () => ({
9 | useSignIn: jest.fn(),
10 | }));
11 |
12 | describe('SigningIn', () => {
13 | it('submits email and password to signIn', async () => {
14 | const signInMock = jest.fn();
15 | (useSignIn as jest.Mock).mockReturnValue(signInMock);
16 |
17 | render();
18 |
19 | fireEvent.change(screen.getByLabelText('email'), {
20 | target: { value: 'test@example.com' },
21 | });
22 | fireEvent.change(screen.getByLabelText('password'), {
23 | target: { value: 'password123' },
24 | });
25 |
26 | fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
27 |
28 | await waitFor(() => {
29 | expect(signInMock).toHaveBeenCalled();
30 | });
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/packages/react/src/SignInForm/SignInForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 |
4 | import { cn } from '../../lib/utils';
5 |
6 | import FormSignIn from './Form';
7 | import { Props } from './types';
8 |
9 | export default function SigningIn({ className, ...props }: Props) {
10 | return (
11 |
12 |
Sign In
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/packages/react/src/SignInForm/hooks.tsx:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query';
2 | import { signIn } from '@niledatabase/client';
3 |
4 | import { Props, LoginInfo } from './types';
5 |
6 | export function useSignIn(params?: Props) {
7 | const {
8 | onSuccess,
9 | onError,
10 | beforeMutate,
11 | callbackUrl,
12 | init,
13 | baseUrl,
14 | fetchUrl,
15 | resetUrl,
16 | auth,
17 | redirect,
18 | } = params ?? {};
19 | const mutation = useMutation({
20 | mutationFn: async (_data: LoginInfo) => {
21 | const d = { ..._data, callbackUrl };
22 | const possibleData = beforeMutate && beforeMutate(d);
23 | const data = possibleData ?? d;
24 | const res = await signIn(data.provider, {
25 | init,
26 | auth,
27 | baseUrl,
28 | fetchUrl,
29 | redirect,
30 | resetUrl,
31 | ...data,
32 | });
33 | if (!res?.ok && res?.error) {
34 | throw new Error(res.error);
35 | }
36 | return res;
37 | },
38 | onSuccess,
39 | onError,
40 | });
41 | return mutation.mutate;
42 | }
43 |
--------------------------------------------------------------------------------
/packages/react/src/SignInForm/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './SignInForm';
2 | export { useSignIn } from './hooks';
3 |
--------------------------------------------------------------------------------
/packages/react/src/SignInForm/types.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from '@tanstack/react-query';
2 | import { BuiltInProviderType } from '@niledatabase/client';
3 |
4 | import { ComponentFetchProps } from '../../lib/utils';
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
7 | export type AllowedAny = any;
8 |
9 | export type LoginInfo = {
10 | provider: BuiltInProviderType;
11 | email?: string;
12 | password?: string;
13 | };
14 | type LoginSuccess = (
15 | response: AllowedAny,
16 | formValues: LoginInfo,
17 | ...args: AllowedAny
18 | ) => void;
19 |
20 | export type Props = ComponentFetchProps & {
21 | beforeMutate?: (data: AllowedAny) => AllowedAny;
22 | onSuccess?: LoginSuccess;
23 | onError?: (error: Error, data: AllowedAny) => void;
24 | callbackUrl?: string;
25 | resetUrl?: string;
26 | client?: QueryClient;
27 | className?: string;
28 | baseUrl?: string;
29 | fetchUrl?: string;
30 | redirect?: boolean;
31 | };
32 |
--------------------------------------------------------------------------------
/packages/react/src/SignOutButton/SignOutButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from './index';
4 |
5 | const meta = {
6 | title: 'Sign Out Button',
7 | component: Button,
8 | };
9 |
10 | export default meta;
11 |
12 | export function SignOutButton() {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/react/src/SignOutButton/SignOutButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import Button from './index';
5 |
6 | describe('Sign out button', () => {
7 | it('renders using the context', () => {
8 | render();
9 | screen.getByText('Sign out');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/react/src/SignOutButton/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { Slot } from '@radix-ui/react-slot';
5 | import { LogOut } from 'lucide-react';
6 | import { signOut } from '@niledatabase/client';
7 |
8 | import { ComponentFetchProps } from '../../lib/utils';
9 | import { buttonVariants, ButtonProps } from '../../components/ui/button';
10 |
11 | type Props = ButtonProps &
12 | ComponentFetchProps & {
13 | redirect?: boolean;
14 | callbackUrl?: string;
15 | buttonText?: string;
16 | baseUrl?: string;
17 | fetchUrl?: string;
18 | basePath?: string;
19 | };
20 |
21 | const SignOutButton = ({
22 | callbackUrl,
23 | redirect,
24 | className,
25 | buttonText = 'Sign out',
26 | variant,
27 | size,
28 | baseUrl,
29 | fetchUrl,
30 | basePath,
31 | auth,
32 | asChild = false,
33 | ...props
34 | }: Props) => {
35 | const Comp = asChild ? Slot : 'button';
36 | return (
37 | {
41 | signOut({ callbackUrl, redirect, baseUrl, auth, fetchUrl, basePath });
42 | }}
43 | {...props}
44 | >
45 | {props.children ? (
46 | props.children
47 | ) : (
48 |
49 |
50 | {buttonText}
51 |
52 | )}
53 |
54 | );
55 | };
56 |
57 | SignOutButton.displayName = 'SignOutButton';
58 | export default SignOutButton;
59 |
--------------------------------------------------------------------------------
/packages/react/src/SignUpForm/Form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4 | import { useForm } from 'react-hook-form';
5 |
6 | import { Email, Form, Password } from '../../components/ui/form';
7 | import { Button } from '../../components/ui/button';
8 |
9 | import { Props } from './types';
10 | import { useSignUp } from './hooks';
11 |
12 | const queryClient = new QueryClient();
13 | export default function SignUpForm(props: Props) {
14 | const { client } = props ?? {};
15 | return (
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | export function SignInForm(props: Props) {
23 | const signUp = useSignUp(props);
24 | const form = useForm({ defaultValues: { email: '', password: '' } });
25 |
26 | return (
27 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/packages/react/src/SignUpForm/SignUpForm.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3 |
4 | import { useSignUp } from './hooks';
5 | import SigningIn from './Form';
6 |
7 | // Mock dependencies
8 | jest.mock('./hooks', () => ({
9 | useSignUp: jest.fn(),
10 | }));
11 |
12 | describe('SigningIn', () => {
13 | it('submits email and password to signIn', async () => {
14 | const signUpMock = jest.fn();
15 | (useSignUp as jest.Mock).mockReturnValue(signUpMock);
16 |
17 | render();
18 |
19 | fireEvent.change(screen.getByLabelText('email'), {
20 | target: { value: 'test@example.com' },
21 | });
22 | fireEvent.change(screen.getByLabelText('password'), {
23 | target: { value: 'password123' },
24 | });
25 |
26 | fireEvent.click(screen.getByRole('button', { name: /sign up/i }));
27 |
28 | await waitFor(() => {
29 | expect(signUpMock).toHaveBeenCalled();
30 | });
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/packages/react/src/SignUpForm/SignUpForm.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { cn } from '../../lib/utils';
4 |
5 | import { Props } from './types';
6 | import SignUpForm from './Form';
7 |
8 | export default function SigningUp({ className, ...props }: Props) {
9 | return (
10 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/react/src/SignUpForm/hooks.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClient, useMutation } from '@tanstack/react-query';
2 | import { signUp } from '@niledatabase/client';
3 |
4 | import { usePrefetch } from '../../lib/utils';
5 |
6 | import { Props, SignUpInfo } from './types';
7 |
8 | export function useSignUp(
9 | params: Props,
10 | client?: QueryClient
11 | ) {
12 | const { onSuccess, onError, beforeMutate, ...remaining } = params;
13 |
14 | const mutation = useMutation(
15 | {
16 | mutationFn: async (_data) => {
17 | const possibleData = beforeMutate && beforeMutate(_data);
18 | const payload: T = { ..._data, ...possibleData };
19 | const { data, error } = await signUp({
20 | ...remaining,
21 | ...payload,
22 | });
23 | if (error) {
24 | throw new Error(error);
25 | }
26 | return data;
27 | },
28 |
29 | onSuccess,
30 | onError,
31 | },
32 | client
33 | );
34 |
35 | usePrefetch(params);
36 |
37 | return mutation.mutate;
38 | }
39 |
--------------------------------------------------------------------------------
/packages/react/src/SignUpForm/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './SignUpForm';
2 | export { useSignUp } from './hooks';
3 |
--------------------------------------------------------------------------------
/packages/react/src/SignUpForm/types.ts:
--------------------------------------------------------------------------------
1 | import { PrefetchParams } from 'packages/react/lib/utils';
2 |
3 | // could probably add CreateTenantUserRequest too.
4 | export type SignUpInfo = {
5 | email: string;
6 | password: string;
7 | tenantId?: string;
8 | fetchUrl?: string;
9 | callbackUrl?: string;
10 | newTenantName?: string;
11 | };
12 |
13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
14 | export type AllowedAny = any;
15 |
16 | export type Props = PrefetchParams & {
17 | onSuccess?: (response: Response, formValues: SignUpInfo) => void;
18 | onError?: (e: Error, info: SignUpInfo) => void;
19 | beforeMutate?: (data: AllowedAny) => AllowedAny;
20 | buttonText?: string;
21 | callbackUrl?: string;
22 | createTenant?: string | boolean;
23 | className?: string;
24 | redirect?: boolean;
25 | };
26 |
--------------------------------------------------------------------------------
/packages/react/src/SignedIn/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { NileSession, NonErrorSession } from '@niledatabase/client';
4 |
5 | import {
6 | useSession,
7 | SessionProvider,
8 | SessionProviderProps,
9 | } from '../../lib/auth';
10 |
11 | export function convertSession(
12 | startSession: NileSession
13 | ): NonErrorSession | undefined | null {
14 | if (startSession && 'exp' in startSession) {
15 | return {
16 | ...startSession,
17 | expires: new Date(startSession.exp * 1000).toISOString(),
18 | };
19 | }
20 |
21 | // handled previously with `SignedIn`
22 | return startSession as NonErrorSession;
23 | }
24 |
25 | export default function SignedIn({
26 | children,
27 | session: startSession,
28 | ...props
29 | }: SessionProviderProps & {
30 | className?: string;
31 | }) {
32 | if (startSession instanceof Response) {
33 | return null;
34 | }
35 | const session = convertSession(startSession);
36 | return (
37 |
38 | {children}
39 |
40 | );
41 | }
42 |
43 | function SignedInChecker({
44 | children,
45 | className,
46 | }: {
47 | className?: string;
48 | children: React.ReactNode;
49 | }) {
50 | const { status } = useSession();
51 |
52 | if (status === 'authenticated') {
53 | if (className) {
54 | return {children}
;
55 | }
56 | return children;
57 | }
58 | return null;
59 | }
60 |
--------------------------------------------------------------------------------
/packages/react/src/SignedOut/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { SessionProviderProps } from 'packages/react/lib/auth';
4 |
5 | import { useSession, SessionProvider } from '../../lib/auth';
6 | import { convertSession } from '../SignedIn';
7 |
8 | export default function SignedOut({
9 | children,
10 | session: startSession,
11 | ...props
12 | }: SessionProviderProps & {
13 | className?: string;
14 | }) {
15 | if (startSession instanceof Response) {
16 | return null;
17 | }
18 | const session = convertSession(startSession);
19 | return (
20 |
21 |
22 | {children}
23 |
24 |
25 | );
26 | }
27 | function SignedOutChecker({
28 | className,
29 | children,
30 | }: {
31 | children: React.ReactNode;
32 | className?: string;
33 | }) {
34 | const { status } = useSession();
35 | if (status === 'unauthenticated') {
36 | if (className) {
37 | return {children}
;
38 | }
39 | return children;
40 | }
41 | return null;
42 | }
43 |
--------------------------------------------------------------------------------
/packages/react/src/SlackSignInButton/SlackSignInButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from './index';
4 |
5 | const meta = {
6 | title: 'Social/Slack',
7 | component: Button,
8 | };
9 |
10 | export default meta;
11 |
12 | export function SlackSSO() {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/react/src/SlackSignInButton/SlackSignInButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import Button from './index';
5 |
6 | describe('Slack sso button', () => {
7 | it('renders using the context', () => {
8 | render();
9 | screen.getByText('Continue with Slack');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/react/src/TenantSelector/TenantSelector.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import TenantSelector from '.';
5 |
6 | const meta: Meta = {
7 | title: 'Tenant selector',
8 | component: TenantSelector,
9 | };
10 |
11 | export default meta;
12 |
13 | export function SelectTenant() {
14 | document.cookie = 'nile.tenant_id=1';
15 | const tenants = [
16 | { id: '1', name: 'Tenant 1' },
17 | { id: '2', name: 'Tenant 2' },
18 | ];
19 | return (
20 |
21 |
22 |
23 | );
24 | }
25 | export function EmptyState() {
26 | document.cookie = 'nile.tenant_id=';
27 | const tenants = [
28 | { id: '1', name: 'Tenant 1' },
29 | { id: '2', name: 'Tenant 2' },
30 | ];
31 | return (
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/packages/react/src/TenantSelector/hooks.ts:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 | import { QueryClient, useQuery } from '@tanstack/react-query';
3 |
4 | import { Tenant } from '../../../server/src/tenants/types';
5 | import { X_NILE_TENANT } from '../../../server/src/utils/constants';
6 | import { componentFetch } from '../../lib/utils';
7 |
8 | import { HookProps } from './types';
9 |
10 | export function useTenants(
11 | params: HookProps & { disableQuery?: boolean },
12 | client?: QueryClient
13 | ) {
14 | const { disableQuery, tenants, baseUrl = '' } = params;
15 |
16 | // Using useQuery to fetch tenants data
17 | const query = useQuery(
18 | {
19 | queryKey: ['tenants', baseUrl],
20 | queryFn: async () => {
21 | const response = await componentFetch(
22 | params.fetchUrl ?? '/tenants',
23 | {
24 | headers: {
25 | 'content-type': 'application/json',
26 | },
27 | },
28 | params
29 | );
30 |
31 | if (!response.ok) {
32 | throw new Error('Failed to fetch tenants');
33 | }
34 |
35 | return response.json();
36 | },
37 | enabled: !disableQuery || tenants?.length === 0,
38 | initialData: tenants,
39 | },
40 | client
41 | );
42 |
43 | return query;
44 | }
45 |
46 | export function useTenantId(
47 | params?: HookProps & { tenant?: Tenant },
48 | client?: QueryClient
49 | ): [string | undefined, (tenant: string) => void] {
50 | const [tenant, setTenant] = React.useState(
51 | params?.tenant?.id
52 | );
53 | const { refetch } = useTenants({ disableQuery: true, ...params }, client);
54 |
55 | useEffect(() => {
56 | if (!tenant) {
57 | const tenantId = getCookie(X_NILE_TENANT);
58 | if (tenantId) {
59 | setTenant(tenantId);
60 | } else {
61 | // if there's nothing in the cookie, we need to ask for tenants again
62 | refetch();
63 | }
64 | }
65 | }, [refetch, tenant]);
66 |
67 | const handleTenantSet = useCallback((tenant: string) => {
68 | setTenant(tenant);
69 | setCookie(X_NILE_TENANT, tenant);
70 | }, []);
71 | return [tenant, handleTenantSet];
72 | }
73 |
74 | const getCookie = (name: string) => {
75 | const cookieArr = document.cookie.split('; ');
76 | for (const cookie of cookieArr) {
77 | const [cookieName, cookieValue] = cookie.split('=');
78 | if (cookieName === name) {
79 | return cookieValue;
80 | }
81 | }
82 | return null;
83 | };
84 |
85 | const setCookie = (name: string, value: string) => {
86 | document.cookie = `${name}=${value}; path=/; samesite=lax`;
87 | };
88 |
--------------------------------------------------------------------------------
/packages/react/src/TenantSelector/types.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from '@tanstack/react-query';
2 |
3 | import { Tenant } from '../../../server/src/tenants/types';
4 | import { ComponentFetchProps } from '../../lib/utils';
5 |
6 | export type HookProps = ComponentFetchProps & {
7 | fetchUrl?: string;
8 | tenants?: Tenant[];
9 | onError?: (e: Error) => void;
10 | };
11 |
12 | export type ComponentProps = HookProps & {
13 | client?: QueryClient;
14 | activeTenant?: string;
15 | useCookie?: boolean;
16 | className?: string;
17 | emptyText?: string;
18 | buttonText?: string;
19 | };
20 |
--------------------------------------------------------------------------------
/packages/react/src/UserInfo/UserInfo.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import UserInfo from '.';
5 |
6 | const meta: Meta = {
7 | title: 'User information',
8 | component: UserInfo,
9 | };
10 |
11 | export default meta;
12 |
13 | export function UserProfile() {
14 | return (
15 |
16 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/packages/react/src/UserInfo/UserInfo.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import Profile from './index';
5 |
6 | describe('Profile info', () => {
7 | it('renders ', () => {
8 | render(
9 |
19 | );
20 | screen.getByText('SpongeBob SquarePants');
21 | screen.getByText('fake@fake.com');
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/react/src/UserInfo/hooks.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { QueryClient, useQuery } from '@tanstack/react-query';
3 | import { ActiveSession } from '@niledatabase/client';
4 |
5 | import { componentFetch, ComponentFetchProps } from '../../lib/utils';
6 | import { User } from '../../../server/src/users/types';
7 |
8 | export type HookProps = ComponentFetchProps & {
9 | user?: User | undefined | null;
10 | baseUrl?: string;
11 | client?: QueryClient;
12 | fetchUrl?: string;
13 | };
14 | export function useMe(props: HookProps): User | null {
15 | const { baseUrl = '', fetchUrl, client, user, auth } = props;
16 | const { data, isLoading } = useQuery(
17 | {
18 | queryKey: ['me', baseUrl],
19 | queryFn: async () => {
20 | const res = await componentFetch(fetchUrl ?? '/me', props);
21 | return await res.json();
22 | },
23 | enabled: user == null,
24 | },
25 | client
26 | );
27 |
28 | if (user || data) {
29 | return user ?? data;
30 | }
31 | // we possibly have email, so return that while we wait for `me` to load
32 | if (auth && !(user && isLoading)) {
33 | return (auth.state?.session as ActiveSession)?.user ?? data;
34 | }
35 | return null;
36 | }
37 |
--------------------------------------------------------------------------------
/packages/react/src/XSignInButton/XSignInButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Button from './index';
4 |
5 | const meta = {
6 | title: 'Social/X',
7 | component: Button,
8 | };
9 |
10 | export default meta;
11 |
12 | export function XSSO() {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/react/src/XSignInButton/XSignInButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import Button from './index';
5 |
6 | describe('X sso button', () => {
7 | it('renders using the context', () => {
8 | render();
9 | screen.getByText('Continue with X');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/react/src/XSignInButton/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { Slot } from '@radix-ui/react-slot';
5 | import { signIn } from '@niledatabase/client';
6 |
7 | import { cn } from '../../lib/utils';
8 | import { buttonVariants, ButtonProps } from '../../components/ui/button';
9 | import { SSOButtonProps } from '../types';
10 |
11 | const XSignInButton = ({
12 | callbackUrl,
13 | className,
14 | buttonText = 'Continue with X',
15 | variant,
16 | size,
17 | init,
18 | onClick,
19 | asChild = false,
20 | auth,
21 | fetchUrl,
22 | baseUrl,
23 | ...props
24 | }: ButtonProps & SSOButtonProps) => {
25 | const Comp = asChild ? Slot : 'button';
26 | return (
27 | {
34 | const res = await signIn('twitter', {
35 | callbackUrl,
36 | init,
37 | auth,
38 | fetchUrl,
39 | baseUrl,
40 | });
41 | onClick && onClick(e, res);
42 | }}
43 | {...props}
44 | >
45 |
46 | {buttonText}
47 |
48 | );
49 | };
50 |
51 | XSignInButton.displayName = 'XSignInButton';
52 | export default XSignInButton;
53 |
54 | const Icon = () => {
55 | return (
56 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/packages/react/src/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | default as EmailSignIn,
3 | EmailSignInButton,
4 | useEmailSignIn,
5 | } from './EmailSignIn';
6 |
7 | export { default as Google } from './GoogleLoginButton';
8 |
9 | export { default as Azure } from './AzureSignInButton';
10 |
11 | export { default as Discord } from './DiscordSignInButton';
12 |
13 | export { default as GitHub } from './GitHubSignInButton';
14 |
15 | export { default as HubSpot } from './HubSpotSignInButton';
16 |
17 | export { default as LinkedIn } from './LinkedInSignInButton';
18 |
19 | export { default as Slack } from './SlackSignInButton';
20 |
21 | export { default as X } from './XSignInButton';
22 |
23 | export { default as SignUpForm, useSignUp } from './SignUpForm';
24 |
25 | export { default as SignInForm, useSignIn } from './SignInForm';
26 |
27 | export { default as SignOutButton } from './SignOutButton';
28 |
29 | export { default as SignedIn } from './SignedIn';
30 |
31 | export { default as SignedOut } from './SignedOut';
32 |
33 | export {
34 | default as TenantSelector,
35 | useTenantId,
36 | useTenants,
37 | } from './TenantSelector';
38 |
39 | export { default as UserInfo, useMe } from './UserInfo';
40 |
41 | export {
42 | useResetPassword,
43 | PasswordResetForm,
44 | PasswordResetRequestForm,
45 | } from './resetPassword';
46 |
47 | export { Email, Password } from '../components/ui/form';
48 |
49 | export { SessionProvider, SessionContext, useSession } from '../lib/auth';
50 | export {
51 | getSession,
52 | getCsrfToken,
53 | getProviders,
54 | signIn,
55 | signOut,
56 | auth,
57 | Authorizer,
58 | } from '@niledatabase/client';
59 |
--------------------------------------------------------------------------------
/packages/react/src/resetPassword/ForgotPassword/ForgotPassword.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import ForgotPasswordC from '.';
5 |
6 | const meta: Meta = {
7 | title: 'Forgot password',
8 | component: ForgotPassword,
9 | };
10 |
11 | export default meta;
12 |
13 | export function ForgotPassword() {
14 | return ;
15 | }
16 |
--------------------------------------------------------------------------------
/packages/react/src/resetPassword/ForgotPassword/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3 | import React from 'react';
4 |
5 | import { Props } from '../types';
6 | import { cn } from '../../../lib/utils';
7 |
8 | import PasswordResetForm from './Form';
9 |
10 | const queryClient = new QueryClient();
11 |
12 | export default function ResetPasswordForm(params: Props) {
13 | const { client, ...props } = params;
14 | return (
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | function ResetForm({ className, ...props }: Props) {
22 | return (
23 |
24 |
Reset password
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/packages/react/src/resetPassword/PasswordResetRequestForm/Form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3 | import { useForm } from 'react-hook-form';
4 | import React from 'react';
5 |
6 | import { Button } from '../../../components/ui/button';
7 | import { Email, Form } from '../../../components/ui/form';
8 | import { useResetPassword } from '../hooks';
9 | import { Props } from '../types';
10 |
11 | const queryClient = new QueryClient();
12 | export default function ResetPasswordForm(props: Props) {
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | function ResetForm(props: Props) {
21 | const { defaultValues, ...params } = props;
22 | const form = useForm({ defaultValues: { email: '', ...defaultValues } });
23 | const resetPassword = useResetPassword({ ...params, redirect: true });
24 | return (
25 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/packages/react/src/resetPassword/PasswordResetRequestForm/PasswordResetRequestForm.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta } from '@storybook/react';
3 |
4 | import RequestResetPasswordForm from '.';
5 |
6 | const meta: Meta = {
7 | title: 'Reset password form',
8 | component: RequestResetPasswordForm,
9 | };
10 |
11 | export default meta;
12 |
13 | export function RequestResetPassword() {
14 | return ;
15 | }
16 |
--------------------------------------------------------------------------------
/packages/react/src/resetPassword/PasswordResetRequestForm/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Props } from '../types';
4 |
5 | import FormReset from './Form';
6 |
7 | export default function ResetPasswordForm(props: Props) {
8 | return (
9 |
10 |
Request password reset
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/packages/react/src/resetPassword/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query';
2 | import { resetPassword } from '@niledatabase/client';
3 |
4 | import { useCsrf } from '../../lib/utils';
5 |
6 | import { MutateFnParams, Params } from './types';
7 |
8 | export function useResetPassword(params?: Params) {
9 | const {
10 | auth,
11 | baseUrl = '',
12 | beforeMutate,
13 | callbackUrl,
14 | fetchUrl,
15 | init,
16 | onError,
17 | onSuccess,
18 | redirect = false,
19 | } = params ?? {};
20 | const mutation = useMutation({
21 | mutationFn: async (_data: MutateFnParams) => {
22 | const possibleData = beforeMutate && beforeMutate(_data);
23 | const data = possibleData ?? _data;
24 |
25 | return await resetPassword({
26 | auth,
27 | baseUrl,
28 | callbackUrl,
29 | fetchUrl,
30 | init,
31 | redirect,
32 | ...data,
33 | });
34 | },
35 | onSuccess: (data) => {
36 | onSuccess && onSuccess(data);
37 | },
38 | onError,
39 | });
40 |
41 | useCsrf(params);
42 |
43 | return mutation.mutate;
44 | }
45 |
--------------------------------------------------------------------------------
/packages/react/src/resetPassword/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as PasswordResetRequestForm } from './PasswordResetRequestForm';
2 | export { default as PasswordResetForm } from './ForgotPassword';
3 | export { useResetPassword } from './hooks';
4 |
--------------------------------------------------------------------------------
/packages/react/src/resetPassword/types.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFetchProps, PrefetchParams } from '../../lib/utils';
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
4 | export type AllowedAny = any;
5 |
6 | export type Params = ComponentFetchProps &
7 | PrefetchParams & {
8 | beforeMutate?: (data: AllowedAny) => AllowedAny;
9 | onSuccess?: (res: Response | undefined) => void;
10 | onError?: (error: Error, data: AllowedAny) => void;
11 | callbackUrl?: string;
12 | basePath?: string;
13 | redirect?: boolean;
14 | };
15 |
16 | export type MutateFnParams = {
17 | email: string;
18 | password?: string;
19 | };
20 |
21 | export type Props = Params & {
22 | className?: string;
23 | defaultValues?: MutateFnParams & {
24 | confirmPassword?: string;
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/packages/react/src/types.ts:
--------------------------------------------------------------------------------
1 | import { PartialAuthorizer, Authorizer } from '@niledatabase/client';
2 |
3 | export interface SignInResponse {
4 | error: string | null;
5 | status: number;
6 | ok: boolean;
7 | url: string | null;
8 | }
9 |
10 | export type SSOButtonProps = {
11 | callbackUrl?: string;
12 | buttonText?: string;
13 | init?: RequestInit;
14 | baseUrl?: string;
15 | fetchUrl?: string;
16 | auth?: Authorizer | PartialAuthorizer;
17 | onClick?: (
18 | e: React.MouseEvent,
19 | res: SignInResponse | undefined
20 | ) => void;
21 | };
22 |
--------------------------------------------------------------------------------
/packages/react/test/fetch.mock.ts:
--------------------------------------------------------------------------------
1 | class FakeResponse {
2 | payload: string;
3 | constructor(payload: string) {
4 | this.payload = payload;
5 | const pload = JSON.parse(payload);
6 | Object.keys(pload).map((key) => {
7 | // @ts-expect-error - its a mock
8 | this[key] = pload[key];
9 | });
10 | }
11 | json = async () => {
12 | return JSON.parse(this.payload);
13 | };
14 | ok = true;
15 | clone = async () => {
16 | return this;
17 | };
18 | }
19 |
20 | export async function _token() {
21 | return new FakeResponse(
22 | JSON.stringify({
23 | token: {
24 | token: 'something',
25 | maxAge: 3600,
26 | },
27 | })
28 | );
29 | }
30 | // it's fake, but it's fetch.
31 | export const token = _token as unknown as typeof fetch;
32 |
--------------------------------------------------------------------------------
/packages/react/test/matchMedia.mock:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
3 | Object.defineProperty(window, 'matchMedia', {
4 | writable: true,
5 | value: jest.fn().mockImplementation((query) => ({
6 | matches: false,
7 | media: query,
8 | onchange: null,
9 | addListener: jest.fn(), // Deprecated
10 | removeListener: jest.fn(), // Deprecated
11 | addEventListener: jest.fn(),
12 | removeEventListener: jest.fn(),
13 | dispatchEvent: jest.fn(),
14 | })),
15 | });
16 |
--------------------------------------------------------------------------------
/packages/react/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "include": ["src/**/*"],
4 | "compilerOptions": {
5 | "baseUrl": "./src",
6 | "declaration": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
4 | "include": ["src/**/*", "test", "src/**/*.stories.tsx", ".storybook/config.ts"],
5 | "compilerOptions": {
6 | "jsx": "react",
7 | "lib": ["dom", "esnext"],
8 | "esModuleInterop": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/react/tsup.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig({
4 | minify: true,
5 | target: 'es2022',
6 | external: ['react'],
7 | sourcemap: true,
8 | dts: true,
9 | format: ['esm', 'cjs'],
10 | esbuildOptions(options) {
11 | options.banner = {
12 | js: '"use client"',
13 | };
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/packages/server/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | .env*
3 | public
--------------------------------------------------------------------------------
/packages/server/DEVELOPERS.md:
--------------------------------------------------------------------------------
1 | # @niledatabase/server
2 |
3 | Consolidates the API and DB for working with Nile.
4 |
5 | ### Adding an endpoint:
6 |
7 | #### Add the openapi spec
8 |
9 | 1. Find or add to `src` the name of the base property eg `src/tenants`
10 | 1. Add a new folder with new method eg `src/tenants/createTenantUser`
11 | 1. Add an `openapi/paths` folder under the method folder and insert a JSON openapi spec. [This helps with conversion](https://onlineyamltools.com/convert-yaml-to-json)
12 | 1. If there are common schemas or responses, add them to `src/openapi` and reference them accordingly
13 | 1. Update `/openapi/index.json` with any modifications, including the file you added/changed
14 | 1. `yarn build` to be sure it works.
15 |
16 | #### Add new function to the sdk
17 |
18 | 1. Add the method (using the method name) and a function for obtaining URL to the base index file with types eg`src/tenants/index` (this should be a lot of copy paste)
19 | 1. Add a test under the method folder to be sure it goes to the correct url.
20 |
--------------------------------------------------------------------------------
/packages/server/example.env:
--------------------------------------------------------------------------------
1 | # env vars for running the integration test
2 | # generated creds from dev/prod
3 | NILEDB_USER=
4 | NILEDB_PASSWORD=
5 |
6 | # comment out for prod
7 | NILEDB_API=api.dev.thenile.dev
8 | NILEDB_HOST=db.dev.thenile.dev
9 |
10 | # tenant where a user is
11 | NILEDB_TENANT=
12 |
13 | # user and password for login
14 | EMAIL=
15 | PASSWORD=
16 |
17 | # test verification that the user above is this user
18 | USER_ID=
--------------------------------------------------------------------------------
/packages/server/jest.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 |
3 | module.exports = {
4 | preset: 'ts-jest',
5 | testEnvironment: 'node',
6 | setupFiles: ['/test/jest.setup.js'],
7 | };
8 |
--------------------------------------------------------------------------------
/packages/server/openapitools.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
3 | "spaces": 2,
4 | "generator-cli": {
5 | "version": "7.7.0"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@niledatabase/server",
3 | "version": "5.0.0-alpha.3",
4 | "license": "MIT",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "exports": {
8 | ".": {
9 | "types": "./dist/index.d.ts",
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs"
12 | },
13 | "./express": {
14 | "types": "./dist/express.d.ts",
15 | "require": "./dist/express.js",
16 | "import": "./dist/express.mjs"
17 | },
18 | "./nitro": {
19 | "types": "./dist/nitro.d.ts",
20 | "require": "./dist/nitro.js",
21 | "import": "./dist/nitro.mjs"
22 | }
23 | },
24 | "files": [
25 | "dist"
26 | ],
27 | "engines": {
28 | "node": ">=18.0"
29 | },
30 | "scripts": {
31 | "start": "dts watch",
32 | "build:spec": "mkdir -p public && yarn next-swagger-doc-cli src/api/openapi/swagger-doc.json",
33 | "build": "yarn build:spec && tsup",
34 | "test": "dts test",
35 | "integration": "NODE_ENV=DEV dts test integration",
36 | "lint": "eslint src"
37 | },
38 | "prettier": {
39 | "printWidth": 80,
40 | "semi": true,
41 | "singleQuote": true,
42 | "trailingComma": "es5"
43 | },
44 | "author": "jrea",
45 | "repository": {
46 | "type": "git",
47 | "url": "https://github.com/niledatabase/nile-js.git",
48 | "directory": "packages/server"
49 | },
50 | "publishConfig": {
51 | "access": "public"
52 | },
53 | "devDependencies": {
54 | "@apidevtools/swagger-cli": "^4.0.4",
55 | "@babel/core": "^7.23.3",
56 | "@openapitools/openapi-generator-cli": "^2.18.4",
57 | "@types/jest": "^29.5.9",
58 | "@types/mime": "^4.0.0",
59 | "@types/pg": "^8.11.4",
60 | "@typescript-eslint/parser": "^5.62.0",
61 | "babel-loader": "^9.1.3",
62 | "dts-cli": "^2.0.3",
63 | "eslint": "^8.54.0",
64 | "eslint-config-prettier": "^8.10.0",
65 | "eslint-plugin-prettier": "^4.2.1",
66 | "husky": "^8.0.3",
67 | "jest": "^29.7.0",
68 | "jest-environment-jsdom": "^29.7.0",
69 | "next-swagger-doc": "^0.4.0",
70 | "ts-jest": "^29.1.1",
71 | "tslib": "^2.6.2",
72 | "tsup": "^8.3.6",
73 | "typescript": "^5.3.2"
74 | },
75 | "dependencies": {
76 | "dotenv": "^16.4.5",
77 | "h3": "^1.15.1",
78 | "pg": "^8.11.3"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/packages/server/src/api/handlers/DELETE.test.ts:
--------------------------------------------------------------------------------
1 | import { appRoutes } from '../utils/routes';
2 | import { Config } from '../../utils/Config';
3 | import {
4 | X_NILE_ORIGIN,
5 | X_NILE_SECURECOOKIES,
6 | X_NILE_TENANT,
7 | } from '../../utils/constants';
8 |
9 | import DELETER from './DELETE';
10 |
11 | jest.mock('../utils/auth', () => () => 'a session, relax');
12 | describe('DELETER', () => {
13 | const apiGet = DELETER(
14 | appRoutes(),
15 | new Config({ apiUrl: 'http://thenile.dev/v2/databases/testdb' })
16 | );
17 | global.fetch = jest.fn();
18 |
19 | beforeEach(() => {
20 | //@ts-expect-error - fetch
21 | global.fetch.mockClear();
22 | });
23 |
24 | [
25 | 'tenants',
26 | 'tenants/{tenantId}',
27 | 'tenants/{tenantId}/users',
28 | 'tenants/${tenantId}/users/${userId}',
29 | ].forEach((key) => {
30 | it(`matches ${key} `, async () => {
31 | const headersArray: { key: string; value: string }[] = [];
32 | let params: Request = {} as Request;
33 |
34 | const req = {
35 | method: 'POST',
36 | [X_NILE_TENANT]: '123',
37 | nextUrl: {
38 | pathname: `/api/${key}`,
39 | },
40 | headers: new Headers({ host: 'http://localhost:3000' }),
41 | url: `http://localhost:3001/api/${key}`,
42 | clone: jest.fn(() => ({ body: '{}' })),
43 | };
44 |
45 | //@ts-expect-error - fetch
46 | global.fetch = jest.fn((url, p) => {
47 | if (p) {
48 | params = new Request(url, p);
49 | }
50 | return Promise.resolve({ status: 200 });
51 | });
52 |
53 | const fn = await apiGet(req as unknown as Request);
54 |
55 | expect(fn).toBeTruthy();
56 | expect(fn?.status).toEqual(200);
57 | params.headers.forEach((value, key) => {
58 | if (key !== 'content-type') {
59 | headersArray.push({ key, value });
60 | }
61 | });
62 | expect(headersArray).toEqual([
63 | { key: 'host', value: 'localhost:3001' },
64 | { key: X_NILE_ORIGIN, value: 'http://localhost:3001' },
65 | { key: X_NILE_SECURECOOKIES, value: 'false' },
66 | ]);
67 | });
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/packages/server/src/api/handlers/DELETE.ts:
--------------------------------------------------------------------------------
1 | import Logger from '../../utils/Logger';
2 | import tenants, { matches as matchesTenants } from '../routes/tenants';
3 | import tenantUsers, {
4 | matches as matchesTenantsUsers,
5 | } from '../routes/tenants/[tenantId]/users';
6 | import me, { matches as matchesMe } from '../routes/me';
7 | import tenantUser, {
8 | matches as matchesTenantUser,
9 | } from '../routes/tenants/[tenantId]/users/[userId]';
10 | import { Routes } from '../types';
11 | import { Config } from '../../utils/Config';
12 |
13 | export default function DELETER(configRoutes: Routes, config: Config) {
14 | const { info, warn } = Logger(config, '[DELETE MATCHER]');
15 | return async function DELETE(req: Request) {
16 | if (matchesTenantUser(configRoutes, req)) {
17 | info('matches tenant user');
18 | return tenantUser(req, config);
19 | }
20 | if (matchesTenantsUsers(configRoutes, req)) {
21 | info('matches tenant users');
22 | return tenantUsers(req, config);
23 | }
24 |
25 | if (matchesTenants(configRoutes, req)) {
26 | info('matches tenants');
27 | return tenants(req, config);
28 | }
29 | if (matchesMe(configRoutes, req)) {
30 | info('matches me');
31 | return me(req, config);
32 | }
33 |
34 | warn('No DELETE routes matched');
35 | return new Response(null, { status: 404 });
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server/src/api/handlers/Get.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | X_NILE_ORIGIN,
3 | X_NILE_SECURECOOKIES,
4 | X_NILE_TENANT,
5 | } from '../../utils/constants';
6 | import { Config } from '../../utils/Config';
7 | import { appRoutes } from '../utils/routes';
8 |
9 | import GETTER from './GET';
10 |
11 | jest.mock('../utils/auth', () => () => 'a session, relax');
12 |
13 | describe('getter', () => {
14 | const apiGet = GETTER(
15 | appRoutes(),
16 | new Config({ apiUrl: 'http://thenile.dev/v2/databases/testdb' })
17 | );
18 | global.fetch = jest.fn();
19 |
20 | beforeEach(() => {
21 | //@ts-expect-error - fetch
22 | global.fetch.mockClear();
23 | });
24 |
25 | [
26 | 'me',
27 | 'users',
28 | 'users/${userId}',
29 | 'tenants',
30 | 'tenants/{tenantId}',
31 | 'auth/signin',
32 | 'tenants/${tenantId}/users/${userId}',
33 | 'tenants/{tenantId}/users',
34 | 'users/${userId}/tenants',
35 | ].forEach((key) => {
36 | it(`matches ${key} `, async () => {
37 | const headersArray: { key: string; value: string }[] = [];
38 | let params: Request = {} as Request;
39 |
40 | const req = {
41 | nextUrl: {
42 | pathname: `/api/${key}`,
43 | },
44 | method: 'GET',
45 | headers: new Headers({
46 | [X_NILE_TENANT]: '123',
47 | host: 'http://localhost:3000',
48 | }),
49 | url: `http://localhost:3001/api/${key}`,
50 | clone: jest.fn(() => ({ body: '{}' })),
51 | };
52 |
53 | //@ts-expect-error - fetch
54 | global.fetch = jest.fn((url, p) => {
55 | if (p) {
56 | params = new Request(url, p);
57 | }
58 | return Promise.resolve({ status: 200 });
59 | });
60 |
61 | const fn = await apiGet(req as unknown as Request);
62 |
63 | expect(fn).toBeTruthy();
64 | expect(fn?.status).toEqual(200);
65 | params.headers.forEach((value, key) => {
66 | if (key !== 'content-type') {
67 | headersArray.push({ key, value });
68 | }
69 | });
70 | expect(headersArray).toEqual([
71 | { key: 'host', value: 'localhost:3001' },
72 | { key: X_NILE_ORIGIN, value: 'http://localhost:3001' },
73 | { key: X_NILE_SECURECOOKIES, value: 'false' },
74 | { key: X_NILE_TENANT, value: '123' },
75 | ]);
76 | });
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/packages/server/src/api/handlers/POST.test.ts:
--------------------------------------------------------------------------------
1 | import { appRoutes } from '../utils/routes';
2 | import { Config } from '../../utils/Config';
3 | import {
4 | X_NILE_ORIGIN,
5 | X_NILE_SECURECOOKIES,
6 | X_NILE_TENANT,
7 | } from '../../utils/constants';
8 |
9 | import POSTER from './POST';
10 |
11 | jest.mock('../utils/auth', () => () => 'a session, relax');
12 | describe('poster', () => {
13 | const apiGet = POSTER(
14 | appRoutes(),
15 | new Config({ apiUrl: 'http://thenile.dev/v2/databases/testdb' })
16 | );
17 | global.fetch = jest.fn();
18 |
19 | beforeEach(() => {
20 | //@ts-expect-error - fetch
21 | global.fetch.mockClear();
22 | });
23 |
24 | [
25 | 'auth/signin',
26 | 'tenants',
27 | 'tenants/{tenantId}',
28 | 'tenants/{tenantId}/users',
29 | 'tenants/${tenantId}/users/${userId}',
30 | 'users',
31 | 'users/${userId}',
32 | 'users/${userId}/tenants',
33 | ].forEach((key) => {
34 | it(`matches ${key} `, async () => {
35 | const headersArray: { key: string; value: string }[] = [];
36 | let params: Request = {} as Request;
37 |
38 | const req = {
39 | method: 'POST',
40 | [X_NILE_TENANT]: '123',
41 | nextUrl: {
42 | pathname: `/api/${key}`,
43 | },
44 | headers: new Headers({ host: 'http://localhost:3000' }),
45 | url: `http://localhost:3001/api/${key}`,
46 | clone: jest.fn(() => ({ body: '{}' })),
47 | };
48 |
49 | //@ts-expect-error - fetch
50 | global.fetch = jest.fn((url, p) => {
51 | if (p) {
52 | params = new Request(url, p);
53 | }
54 | return Promise.resolve({ status: 200 });
55 | });
56 |
57 | const fn = await apiGet(req as unknown as Request);
58 |
59 | expect(fn).toBeTruthy();
60 | expect(fn?.status).toEqual(200);
61 | params.headers.forEach((value, key) => {
62 | if (key !== 'content-type') {
63 | headersArray.push({ key, value });
64 | }
65 | });
66 | expect(headersArray).toEqual([
67 | { key: 'host', value: 'localhost:3001' },
68 | { key: X_NILE_ORIGIN, value: 'http://localhost:3001' },
69 | { key: X_NILE_SECURECOOKIES, value: 'false' },
70 | ]);
71 | });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/packages/server/src/api/handlers/PUT.test.ts:
--------------------------------------------------------------------------------
1 | import { appRoutes } from '../utils/routes';
2 | import { Config } from '../../utils/Config';
3 | import {
4 | X_NILE_ORIGIN,
5 | X_NILE_SECURECOOKIES,
6 | X_NILE_TENANT,
7 | } from '../../utils/constants';
8 |
9 | import PUTTER from './PUT';
10 |
11 | jest.mock('../utils/auth', () => () => 'a session, relax');
12 | describe('Putter', () => {
13 | const apiGet = PUTTER(
14 | appRoutes(),
15 | new Config({ apiUrl: 'http://thenile.dev/v2/databases/testdb' })
16 | );
17 | global.fetch = jest.fn();
18 |
19 | beforeEach(() => {
20 | //@ts-expect-error - fetch
21 | global.fetch.mockClear();
22 | });
23 |
24 | [
25 | 'tenants',
26 | 'tenants/{tenantId}',
27 | 'tenants/{tenantId}/users',
28 | 'tenants/${tenantId}/users/${userId}',
29 | 'users',
30 | 'users/${userId}',
31 | 'users/${userId}/tenants',
32 | ].forEach((key) => {
33 | it(`matches ${key} `, async () => {
34 | const headersArray: { key: string; value: string }[] = [];
35 | let params: Request = {} as Request;
36 |
37 | const req = {
38 | method: 'POST',
39 | [X_NILE_TENANT]: '123',
40 | nextUrl: {
41 | pathname: `/api/${key}`,
42 | },
43 | headers: new Headers({ host: 'http://localhost:3000' }),
44 | url: `http://localhost:3001/api/${key}`,
45 | clone: jest.fn(() => ({ body: '{}' })),
46 | };
47 |
48 | //@ts-expect-error - fetch
49 | global.fetch = jest.fn((url, p) => {
50 | if (p) {
51 | params = new Request(url, p);
52 | }
53 | return Promise.resolve({ status: 200 });
54 | });
55 |
56 | const fn = await apiGet(req as unknown as Request);
57 |
58 | expect(fn).toBeTruthy();
59 | expect(fn?.status).toEqual(200);
60 | params.headers.forEach((value, key) => {
61 | if (key !== 'content-type') {
62 | headersArray.push({ key, value });
63 | }
64 | });
65 | expect(headersArray).toEqual([
66 | { key: 'host', value: 'localhost:3001' },
67 | { key: X_NILE_ORIGIN, value: 'http://localhost:3001' },
68 | { key: X_NILE_SECURECOOKIES, value: 'false' },
69 | ]);
70 | });
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/packages/server/src/api/handlers/PUT.ts:
--------------------------------------------------------------------------------
1 | import Logger from '../../utils/Logger';
2 | import users, { matches as matchesUsers } from '../routes/users';
3 | import tenants, { matches as matchesTenants } from '../routes/tenants';
4 | import me, { matches as matchesMe } from '../routes/me';
5 | import tenantUsers, {
6 | matches as matchesTenantUsers,
7 | } from '../routes/tenants/[tenantId]/users';
8 | import tenantUser, {
9 | matches as matchesTenantUser,
10 | } from '../routes/tenants/[tenantId]/users/[userId]';
11 | import { handlePasswordReset, matchesPasswordReset } from '../routes/auth';
12 | import { Routes } from '../types';
13 | import { Config } from '../../utils/Config';
14 |
15 | export default function PUTER(configRoutes: Routes, config: Config) {
16 | const { info, warn } = Logger(config, '[PUT MATCHER]');
17 | return async function PUT(req: Request) {
18 | if (matchesTenantUser(configRoutes, req)) {
19 | info('matches tenant user');
20 | return tenantUser(req, config);
21 | }
22 | if (matchesTenantUsers(configRoutes, req)) {
23 | info('matches tenant users');
24 | return tenantUsers(req, config);
25 | }
26 | if (matchesUsers(configRoutes, req)) {
27 | info('matches users');
28 | return users(req, config);
29 | }
30 | if (matchesMe(configRoutes, req)) {
31 | info('matches me');
32 | return me(req, config);
33 | }
34 | if (matchesTenants(configRoutes, req)) {
35 | info('matches tenants');
36 | return tenants(req, config);
37 | }
38 | if (matchesPasswordReset(configRoutes, req)) {
39 | info('matches reset password');
40 | return handlePasswordReset(req, config);
41 | }
42 | warn('No PUT routes matched');
43 | return new Response(null, { status: 404 });
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/packages/server/src/api/handlers/index.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../../utils/Config';
2 | import { Routes } from '../types';
3 |
4 | import getter from './GET';
5 | import poster from './POST';
6 | import deleter from './DELETE';
7 | import puter from './PUT';
8 |
9 | export default function Handlers(configRoutes: Routes, config: Config) {
10 | const GET = getter(configRoutes, config);
11 | const POST = poster(configRoutes, config);
12 | const DELETE = deleter(configRoutes, config);
13 | const PUT = puter(configRoutes, config);
14 | return {
15 | GET,
16 | POST,
17 | DELETE,
18 | PUT,
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/packages/server/src/api/handlers/withContext/index.ts:
--------------------------------------------------------------------------------
1 | import { Server } from '../../../Server';
2 | import { NileConfig } from '../../../types';
3 | import { Config } from '../../../utils/Config';
4 | import getter from '../GET';
5 | import poster from '../POST';
6 | import deleter from '../DELETE';
7 | import puter from '../PUT';
8 |
9 | export type CTXHandlerType = {
10 | GET: (req: Request) => Promise<{ response: void | Response; nile: Server }>;
11 | POST: (req: Request) => Promise<{ response: void | Response; nile: Server }>;
12 | DELETE: (
13 | req: Request
14 | ) => Promise<{ response: void | Response; nile: Server }>;
15 | PUT: (req: Request) => Promise<{ response: void | Response; nile: Server }>;
16 | };
17 | export function handlersWithContext(config: Config): CTXHandlerType {
18 | const GET = getter(config.routes, config);
19 | const POST = poster(config.routes, config);
20 | const DELETE = deleter(config.routes, config);
21 | const PUT = puter(config.routes, config);
22 | return {
23 | GET: async (req) => {
24 | const response = await GET(req);
25 | const updatedConfig = updateConfig(response, config);
26 | return { response, nile: new Server(updatedConfig) };
27 | },
28 | POST: async (req) => {
29 | const response = await POST(req);
30 | const updatedConfig = updateConfig(response, config);
31 | return { response, nile: new Server(updatedConfig) };
32 | },
33 | DELETE: async (req) => {
34 | const response = await DELETE(req);
35 | const updatedConfig = updateConfig(response, config);
36 | return { response, nile: new Server(updatedConfig) };
37 | },
38 | PUT: async (req) => {
39 | const response = await PUT(req);
40 | const updatedConfig = updateConfig(response, config);
41 | return { response, nile: new Server(updatedConfig) };
42 | },
43 | };
44 | }
45 |
46 | export function updateConfig(
47 | response: Response | void,
48 | config: Config
49 | ): NileConfig {
50 | let origin = 'http://localhost:3000';
51 | let headers: Headers | null = null;
52 |
53 | if (response?.status === 302) {
54 | const location = response.headers.get('location');
55 | if (location) {
56 | const urlLocation = new URL(location);
57 | origin = urlLocation.origin;
58 | }
59 | }
60 |
61 | const setCookies: string[] = [];
62 |
63 | // Headers are iterable
64 | if (response?.headers) {
65 | for (const [key, value] of response.headers) {
66 | if (key.toLowerCase() === 'set-cookie') {
67 | setCookies.push(value);
68 | }
69 | }
70 | }
71 | if (setCookies.length > 0) {
72 | const cookieHeader = setCookies
73 | .map((cookieStr) => cookieStr.split(';')[0])
74 | .join('; ');
75 |
76 | headers = new Headers({ cookie: cookieHeader });
77 | }
78 |
79 | return {
80 | ...config,
81 | origin,
82 | headers: headers ?? undefined,
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/packages/server/src/api/handlers/withContext/withContext.test.ts:
--------------------------------------------------------------------------------
1 | import { Server } from '../../../Server';
2 | import { Config } from '../../../utils/Config';
3 |
4 | import { handlersWithContext } from '.';
5 |
6 | jest.mock('../../../Server', () => ({
7 | Server: jest.fn().mockImplementation((config) => ({
8 | config,
9 | })),
10 | }));
11 |
12 | const mockGET = jest.fn();
13 | const mockPOST = jest.fn();
14 | const mockDELETE = jest.fn();
15 | const mockPUT = jest.fn();
16 |
17 | jest.mock('../GET', () => jest.fn(() => mockGET));
18 | jest.mock('../POST', () => jest.fn(() => mockPOST));
19 | jest.mock('../DELETE', () => jest.fn(() => mockDELETE));
20 | jest.mock('../PUT', () => jest.fn(() => mockPUT));
21 |
22 | describe('handlersWithContext', () => {
23 | const config: Config = new Config({
24 | origin: 'https://api.example.com',
25 | });
26 |
27 | beforeEach(() => {
28 | jest.clearAllMocks();
29 | });
30 |
31 | it('returns all four HTTP method handlers', () => {
32 | const handlers = handlersWithContext(config);
33 | expect(handlers).toHaveProperty('GET');
34 | expect(handlers).toHaveProperty('POST');
35 | expect(handlers).toHaveProperty('DELETE');
36 | expect(handlers).toHaveProperty('PUT');
37 | });
38 |
39 | it('returns GET handler that wraps the response and creates a new Server', async () => {
40 | const mockResponse = new Response(null, { status: 200 });
41 | mockGET.mockResolvedValueOnce(mockResponse);
42 |
43 | const handlers = handlersWithContext(config);
44 | const result = await handlers.GET(new Request('http://localhost'));
45 |
46 | expect(mockGET).toHaveBeenCalled();
47 | expect(result.response).toBe(mockResponse);
48 |
49 | // Server constructor should have been called with updated origin
50 | expect(Server).toHaveBeenCalledWith({
51 | ...config,
52 | headers: undefined,
53 | origin: 'http://localhost:3000',
54 | });
55 | //@ts-expect-error - internal inspection
56 | expect(result.nile.config.origin).toBe('http://localhost:3000');
57 | });
58 |
59 | it('returns POST handler that matches input', async () => {
60 | const handlers = handlersWithContext(config);
61 | const mockReq = new Request('http://localhost', { method: 'POST' });
62 | await handlers.POST(mockReq);
63 | expect(mockPOST).toHaveBeenCalledWith(mockReq);
64 | });
65 |
66 | it('returns DELETE handler that matches input', async () => {
67 | const handlers = handlersWithContext(config);
68 | const mockReq = new Request('http://localhost', { method: 'DELETE' });
69 | await handlers.DELETE(mockReq);
70 | expect(mockDELETE).toHaveBeenCalledWith(mockReq);
71 | });
72 |
73 | it('returns PUT handler that matches input', async () => {
74 | const handlers = handlersWithContext(config);
75 | const mockReq = new Request('http://localhost', { method: 'PUT' });
76 | await handlers.PUT(mockReq);
77 | expect(mockPUT).toHaveBeenCalledWith(mockReq);
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/packages/server/src/api/openapi/swagger-doc.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiFolder": "src/api",
3 | "outputFile": "openapi/swagger.json",
4 | "definition": {
5 | "openapi": "3.0.0",
6 | "info": {
7 | "title": "Niledatabase regional APIs",
8 | "version": "2.0"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/auth/callback.ts:
--------------------------------------------------------------------------------
1 | import { NileAuthRoutes, proxyRoutes, urlMatches } from '../../utils/routes';
2 | import request from '../../utils/request';
3 | import { Routes } from '../../types';
4 | import { Config } from '../../../utils/Config';
5 | import Logger from '../../../utils/Logger';
6 | import { ProviderName } from '../../utils/auth';
7 |
8 | const key = 'CALLBACK';
9 |
10 | export default async function route(req: Request, config: Config) {
11 | const { error } = Logger(
12 | { ...config, debug: config.debug } as Config,
13 | `[ROUTES][${key}]`
14 | );
15 | const [provider] = new URL(req.url).pathname.split('/').reverse();
16 | try {
17 | const passThroughUrl = new URL(req.url);
18 | const params = new URLSearchParams(passThroughUrl.search);
19 | const url = `${proxyRoutes(config)[key]}/${provider}${
20 | params.toString() !== '' ? `?${params.toString()}` : ''
21 | }`;
22 |
23 | const res = await request(
24 | url,
25 | {
26 | request: req,
27 | method: req.method,
28 | },
29 | config
30 | ).catch((e) => {
31 | error('an error as occurred', e);
32 | });
33 |
34 | const location = res?.headers.get('location');
35 | if (location) {
36 | return new Response(res?.body, {
37 | status: 302,
38 | headers: res?.headers,
39 | });
40 | }
41 | return new Response(res?.body, {
42 | status: res?.status,
43 | headers: res?.headers,
44 | });
45 | } catch (e) {
46 | error(e);
47 | }
48 | return new Response('An unexpected error has occurred.', { status: 400 });
49 | }
50 | export function matches(configRoutes: Routes, request: Request): boolean {
51 | return urlMatches(request.url, configRoutes.CALLBACK);
52 | }
53 |
54 | // this is for the the credential provider, among other things
55 | export async function fetchCallback(
56 | config: Config,
57 | provider: ProviderName,
58 | body?: string,
59 | request?: Request,
60 | method: 'POST' | 'GET' = 'POST'
61 | ): Promise {
62 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${
63 | NileAuthRoutes.CALLBACK
64 | }/${provider}${request ? `?${new URL(request.url).searchParams}` : ''}`;
65 | const req = new Request(clientUrl, {
66 | method,
67 | headers: config.headers,
68 | body,
69 | });
70 |
71 | return (await config.handlers.POST(req)) as Response;
72 | }
73 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/auth/csrf.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '../../types';
2 | import { NileAuthRoutes, proxyRoutes, urlMatches } from '../../utils/routes';
3 | import request from '../../utils/request';
4 | import { Config } from '../../../utils/Config';
5 |
6 | export default async function route(req: Request, config: Config) {
7 | return request(
8 | proxyRoutes(config).CSRF,
9 | {
10 | method: req.method,
11 | request: req,
12 | },
13 | config
14 | );
15 | }
16 | export function matches(configRoutes: Routes, request: Request): boolean {
17 | return urlMatches(request.url, configRoutes.CSRF);
18 | }
19 |
20 | export async function fetchCsrf(config: Config): Promise {
21 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.CSRF}`;
22 | const req = new Request(clientUrl, {
23 | method: 'GET',
24 | headers: config.headers,
25 | });
26 |
27 | return (await config.handlers.GET(req)) as Response;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/auth/error.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '../../types';
2 | import { urlMatches, proxyRoutes } from '../../utils/routes';
3 | import request from '../../utils/request';
4 | import { Config } from '../../../utils/Config';
5 |
6 | const key = 'ERROR';
7 | export default async function route(req: Request, config: Config) {
8 | return request(
9 | proxyRoutes(config)[key],
10 | {
11 | method: req.method,
12 | request: req,
13 | },
14 | config
15 | );
16 | }
17 | export function matches(configRoutes: Routes, request: Request): boolean {
18 | return urlMatches(request.url, configRoutes[key]);
19 | }
20 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/auth/index.ts:
--------------------------------------------------------------------------------
1 | export { default as handleSignIn, matches as matchSignIn } from './signin';
2 | export { default as handleSession, matches as matchSession } from './session';
3 | export {
4 | default as handleProviders,
5 | matches as matchProviders,
6 | } from './providers';
7 |
8 | export { default as handleCsrf, matches as matchCsrf } from './csrf';
9 | export {
10 | default as handleCallback,
11 | matches as matchCallback,
12 | } from './callback';
13 |
14 | export { default as handleSignOut, matches as matchSignOut } from './signout';
15 | export { default as handleError, matches as matchError } from './error';
16 | export {
17 | default as handleVerifyRequest,
18 | matches as matchesVerifyRequest,
19 | } from './verify-request';
20 | export {
21 | default as handlePasswordReset,
22 | matches as matchesPasswordReset,
23 | } from './password-reset';
24 | export {
25 | default as handleVerifyEmail,
26 | matches as matchesVerifyEmail,
27 | } from './verify-email';
28 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/auth/password-reset.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '../../types';
2 | import { urlMatches, proxyRoutes, NileAuthRoutes } from '../../utils/routes';
3 | import request from '../../utils/request';
4 | import { Config } from '../../../utils/Config';
5 |
6 | const key = 'PASSWORD_RESET';
7 | export default async function route(req: Request, config: Config) {
8 | const url = proxyRoutes(config)[key];
9 |
10 | const res = await request(
11 | url,
12 | {
13 | method: req.method,
14 | request: req,
15 | },
16 | config
17 | );
18 |
19 | const location = res?.headers.get('location');
20 | if (location) {
21 | return new Response(res?.body, {
22 | status: 302,
23 | headers: res?.headers,
24 | });
25 | }
26 | return new Response(res?.body, {
27 | status: res?.status,
28 | headers: res?.headers,
29 | });
30 | }
31 | export function matches(configRoutes: Routes, request: Request): boolean {
32 | return urlMatches(request.url, configRoutes.PASSWORD_RESET);
33 | }
34 |
35 | export async function fetchResetPassword(
36 | config: Config,
37 | method: 'POST' | 'GET' | 'PUT',
38 | body: null | string,
39 | params?: URLSearchParams,
40 | useJson = true
41 | ) {
42 | const authParams = new URLSearchParams(params ?? {});
43 | if (useJson) {
44 | authParams?.set('json', 'true');
45 | }
46 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${
47 | NileAuthRoutes.PASSWORD_RESET
48 | }?${authParams?.toString()}`;
49 | const init: RequestInit = {
50 | method,
51 | headers: config.headers,
52 | };
53 | if (body && method !== 'GET') {
54 | init.body = body;
55 | }
56 | const req = new Request(clientUrl, init);
57 | return (await config.handlers[method](req)) as Response;
58 | }
59 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/auth/providers.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '../../types';
2 | import { NileAuthRoutes, proxyRoutes, urlMatches } from '../../utils/routes';
3 | import request from '../../utils/request';
4 | import { Config } from '../../../utils/Config';
5 |
6 | export default async function route(req: Request, config: Config) {
7 | return request(
8 | proxyRoutes(config).PROVIDERS,
9 | {
10 | method: req.method,
11 | request: req,
12 | },
13 | config
14 | );
15 | }
16 | export function matches(configRoutes: Routes, request: Request): boolean {
17 | return urlMatches(request.url, configRoutes.PROVIDERS);
18 | }
19 |
20 | export async function fetchProviders(config: Config): Promise {
21 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.PROVIDERS}`;
22 | const req = new Request(clientUrl, {
23 | method: 'GET',
24 | headers: config.headers,
25 | });
26 |
27 | return (await config.handlers.GET(req)) as Response;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/auth/session.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '../../types';
2 | import { NileAuthRoutes, proxyRoutes, urlMatches } from '../../utils/routes';
3 | import request from '../../utils/request';
4 | import { Config } from '../../../utils/Config';
5 |
6 | export default async function route(req: Request, config: Config) {
7 | return request(
8 | proxyRoutes(config).SESSION,
9 | {
10 | method: req.method,
11 | request: req,
12 | },
13 | config
14 | );
15 | }
16 | export function matches(configRoutes: Routes, request: Request): boolean {
17 | return urlMatches(request.url, configRoutes.SESSION);
18 | }
19 |
20 | export async function fetchSession(config: Config): Promise {
21 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.SESSION}`;
22 | const req = new Request(clientUrl, {
23 | method: 'GET',
24 | headers: config.headers,
25 | });
26 |
27 | return (await config.handlers.GET(req)) as Response;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/auth/signin.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @swagger
3 | * /api/auth/signin:
4 | * get:
5 | * tags:
6 | * - authentication
7 | * summary: lists users in the tenant
8 | * description: Returns information about the users within the tenant
9 | * provided
10 | * operationId: signin
11 | * parameters:
12 | * - name: tenantId
13 | * in: path
14 | * required: true
15 | * schema:
16 | * type: string
17 | * responses:
18 | * "200":
19 | * description: A list of users
20 | * content:
21 | * application/json:
22 | * schema:
23 | * $ref: '#/components/schemas/User'
24 | * "404":
25 | * description: Not found
26 | * content: {}
27 | * "401":
28 | * description: Unauthorized
29 | * content: {}
30 | */
31 |
32 | import { Routes } from '../../types';
33 | import { Config } from '../../../utils/Config';
34 | import { NileAuthRoutes, proxyRoutes, urlMatches } from '../../utils/routes';
35 | import request from '../../utils/request';
36 |
37 | const key = 'SIGNIN';
38 |
39 | export default async function route(req: Request, config: Config) {
40 | let url = proxyRoutes(config)[key];
41 |
42 | const init: RequestInit = {
43 | method: req.method,
44 | headers: req.headers,
45 | };
46 | if (req.method === 'POST') {
47 | const [provider] = new URL(req.url).pathname.split('/').reverse();
48 |
49 | url = `${proxyRoutes(config)[key]}/${provider}`;
50 | }
51 |
52 | const passThroughUrl = new URL(req.url);
53 | const params = new URLSearchParams(passThroughUrl.search);
54 |
55 | url = `${url}${params.toString() !== '' ? `?${params.toString()}` : ''}`;
56 | const res = await request(url, { ...init, request: req }, config);
57 |
58 | return res;
59 | }
60 | export function matches(configRoutes: Routes, request: Request): boolean {
61 | return urlMatches(request.url, configRoutes[key]);
62 | }
63 |
64 | // this is not for the the credential provider STILL NEED TO FIGURE THIS OUT I THINK? or remove.
65 | export async function fetchSignIn(
66 | config: Config,
67 | provider: string,
68 | body: URLSearchParams
69 | ): Promise {
70 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.SIGNIN}/${provider}`;
71 | const req = new Request(clientUrl, {
72 | method: 'POST',
73 | headers: config.headers,
74 | body,
75 | });
76 |
77 | return (await config.handlers.POST(req)) as Response;
78 | }
79 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/auth/signout.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '../../types';
2 | import { NileAuthRoutes, proxyRoutes, urlMatches } from '../../utils/routes';
3 | import fetch from '../../utils/request';
4 | import { Config } from '../../../utils/Config';
5 |
6 | const key = 'SIGNOUT';
7 | export default async function route(request: Request, config: Config) {
8 | let url = proxyRoutes(config)[key];
9 |
10 | const init: RequestInit = {
11 | method: request.method,
12 | };
13 | if (request.method === 'POST') {
14 | init.body = request.body;
15 | const [provider] = new URL(request.url).pathname.split('/').reverse();
16 | url = `${proxyRoutes(config)[key]}${
17 | provider !== 'signout' ? `/${provider}` : ''
18 | }`;
19 | }
20 |
21 | const res = await fetch(url, { ...init, request }, config);
22 | return res;
23 | }
24 | export function matches(configRoutes: Routes, request: Request): boolean {
25 | return urlMatches(request.url, configRoutes[key]);
26 | }
27 |
28 | export async function fetchSignOut(
29 | config: Config,
30 | body: string
31 | ): Promise {
32 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.SIGNOUT}`;
33 | const req = new Request(clientUrl, {
34 | method: 'POST',
35 | body,
36 | headers: config.headers,
37 | });
38 |
39 | return (await config.handlers.POST(req)) as Response;
40 | }
41 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/auth/verify-email.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '../../types';
2 | import { urlMatches, proxyRoutes, NileAuthRoutes } from '../../utils/routes';
3 | import request from '../../utils/request';
4 | import { Config } from '../../../utils/Config';
5 |
6 | const key = 'VERIFY_EMAIL';
7 | export default async function route(req: Request, config: Config) {
8 | const url = proxyRoutes(config)[key];
9 |
10 | const res = await request(
11 | url,
12 | {
13 | method: req.method,
14 | request: req,
15 | },
16 | config
17 | );
18 |
19 | const location = res?.headers.get('location');
20 | if (location) {
21 | return new Response(res?.body, {
22 | status: 302,
23 | headers: res?.headers,
24 | });
25 | }
26 | return new Response(res?.body, {
27 | status: res?.status,
28 | headers: res?.headers,
29 | });
30 | }
31 | export function matches(configRoutes: Routes, request: Request): boolean {
32 | return urlMatches(request.url, configRoutes[key]);
33 | }
34 |
35 | export async function fetchVerifyEmail(
36 | config: Config,
37 | method: 'POST' | 'GET',
38 | body?: string
39 | ): Promise {
40 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.VERIFY_EMAIL}`;
41 | const init: RequestInit = {
42 | method,
43 | headers: config.headers,
44 | };
45 | if (body) {
46 | init.body = body;
47 | }
48 | const req = new Request(clientUrl, init);
49 |
50 | return (await config.handlers[method](req)) as Response;
51 | }
52 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/auth/verify-request.ts:
--------------------------------------------------------------------------------
1 | import { urlMatches, proxyRoutes } from '../../utils/routes';
2 | import request from '../../utils/request';
3 | import { Routes } from '../../types';
4 | import { Config } from '../../../utils/Config';
5 |
6 | const key = 'VERIFY_REQUEST';
7 |
8 | export default async function route(req: Request, config: Config) {
9 | return request(
10 | proxyRoutes(config)[key],
11 | {
12 | method: req.method,
13 | request: req,
14 | },
15 | config
16 | );
17 | }
18 | export function matches(configRoutes: Routes, request: Request): boolean {
19 | return urlMatches(request.url, configRoutes[key]);
20 | }
21 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/signup/POST.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../../../utils/Config';
2 | import request from '../../utils/request';
3 | import { apiRoutes } from '../../utils/routes';
4 |
5 | /**
6 | * @swagger
7 | * /api/signup:
8 | * post:
9 | * tags:
10 | * - users
11 | * summary: signs a user up
12 | * description: signs a user up and logs them in. Expects a email and password combo
13 | * operationId: signUp
14 | * parameters:
15 | * - name: tenantId
16 | * description: A tenant id to add the user to when they are created
17 | * in: query
18 | * schema:
19 | * type: string
20 | * - name: newTenantName
21 | * description: A tenant name to create, then the user to when they are created
22 | * in: query
23 | * schema:
24 | * type: string
25 | * requestBody:
26 | * description: |-
27 | * The email and password combination the user will use to authenticate.
28 | * The `name` is optional; if provided it will be recorded in the `users` table.
29 | * The `newTenant` is optional; if provided, it is used as the name of a new tenant record associated with the newly created user.
30 | * content:
31 | * application/json:
32 | * schema:
33 | * $ref: '#/components/schemas/CreateBasicUserRequest'
34 | * examples:
35 | * Create User Request:
36 | * summary: Creates a user with basic credentials
37 | * description: Create User Request
38 | * value:
39 | * email: a.user@somedomain.com
40 | * password: somepassword
41 | * name: A. User
42 | * Create User Request with Tenant:
43 | * summary: Creates a user and a new tenant for that user
44 | * description: Create User Request with Tenant
45 | * value:
46 | * email: a.user@somedomain.com
47 | * password: somepassword
48 | * name: A. User
49 | * newTenant: My Sandbox
50 | * responses:
51 | * "201":
52 | * description: User and session created
53 | * content:
54 | * application/json:
55 | * schema:
56 | * $ref: "#/components/schemas/User"
57 | * "400":
58 | * description: API/Database failures
59 | * content:
60 | * text/plain:
61 | * schema:
62 | * type: string
63 | * "401":
64 | * description: Unauthorized
65 | * content: {}
66 | */
67 | export async function POST(
68 | config: Config,
69 | init: RequestInit & { request: Request }
70 | ) {
71 | init.body = init.request.body;
72 | init.method = 'POST';
73 | const url = `${apiRoutes(config).SIGNUP}`;
74 |
75 | return await request(url, init, config);
76 | }
77 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/signup/index.tsx:
--------------------------------------------------------------------------------
1 | import { Config } from '../../../utils/Config';
2 | import { Routes } from '../../types';
3 | import { urlMatches, DefaultNileAuthRoutes } from '../../utils/routes';
4 |
5 | import { POST } from './POST';
6 |
7 | const key = 'SIGNUP';
8 |
9 | export default async function route(request: Request, config: Config) {
10 | switch (request.method) {
11 | case 'POST':
12 | return await POST(config, { request });
13 |
14 | default:
15 | return new Response('method not allowed', { status: 405 });
16 | }
17 | }
18 |
19 | export function matches(configRoutes: Routes, request: Request): boolean {
20 | return urlMatches(request.url, configRoutes[key]);
21 | }
22 |
23 | export async function fetchSignUp(
24 | config: Config,
25 | payload: {
26 | body?: string;
27 | params?: { newTenantName?: string; tenantId?: string };
28 | }
29 | ): Promise {
30 | const { body, params } = payload ?? {};
31 | const q = new URLSearchParams();
32 | if (params?.newTenantName) {
33 | q.set('newTenantName', params.newTenantName);
34 | }
35 | if (params?.tenantId) {
36 | q.set('tenantId', params.tenantId);
37 | }
38 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${
39 | DefaultNileAuthRoutes.SIGNUP
40 | }${q.size > 0 ? `?${q}` : ''}`;
41 | const req = new Request(clientUrl, {
42 | method: 'POST',
43 | headers: config.headers,
44 | body,
45 | });
46 |
47 | return (await config.handlers.POST(req)) as Response;
48 | }
49 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/signup/signup.test.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../../../utils/Config';
2 | import fetch from '../../utils/request';
3 |
4 | import route from '.';
5 |
6 | const utilRequest = fetch as jest.Mock;
7 |
8 | jest.mock('../../utils/request', () => jest.fn());
9 | jest.mock('../../utils/auth', () => () => ({
10 | id: 'something',
11 | }));
12 |
13 | describe('signup route', () => {
14 | afterEach(() => {
15 | jest.clearAllMocks();
16 | });
17 |
18 | it('should post sign up', async () => {
19 | const _res = new Request('http://thenile.dev', {
20 | method: 'POST',
21 | });
22 | await route(
23 | _res,
24 | new Config({
25 | apiUrl: 'http://thenile.dev/v2/databases/testdb',
26 | })
27 | );
28 | expect(utilRequest).toHaveBeenCalledWith(
29 | 'http://thenile.dev/v2/databases/testdb/signup',
30 | expect.objectContaining({ method: 'POST' }),
31 | expect.objectContaining({})
32 | );
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/tenants/GET.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../../../utils/Config';
2 | import { ActiveSession } from '../../utils/auth';
3 | import request from '../../utils/request';
4 | import { apiRoutes } from '../../utils/routes';
5 |
6 | /**
7 | * @swagger
8 | * /api/tenants:
9 | * get:
10 | * tags:
11 | * - tenants
12 | * summary: list tenants by user
13 | * description: Creates a user in the database
14 | * operationId: listTenants
15 | * responses:
16 | * "200":
17 | * description: a list of tenants
18 | * content:
19 | * application/json:
20 | * schema:
21 | * type: array
22 | * items:
23 | * $ref: "#/components/schemas/Tenant"
24 | * "400":
25 | * description: API/Database failures
26 | * content:
27 | * text/plain:
28 | * schema:
29 | * type: string
30 | * "401":
31 | * description: Unauthorized
32 | * content: {}
33 | */
34 | export async function GET(
35 | config: Config,
36 | session: ActiveSession,
37 | init: RequestInit & { request: Request }
38 | ) {
39 | let url = `${apiRoutes(config).USER_TENANTS(session.id)}`;
40 | if (typeof session === 'object' && 'user' in session && session.user) {
41 | url = `${apiRoutes(config).USER_TENANTS(session.user.id)}`;
42 | }
43 |
44 | const res = await request(url, init, config);
45 | return res;
46 | }
47 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/tenants/POST.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../../../utils/Config';
2 | import request from '../../utils/request';
3 | import { apiRoutes } from '../../utils/routes';
4 |
5 | /**
6 | * @swagger
7 | * /api/tenants:
8 | * post:
9 | * tags:
10 | * - tenants
11 | * summary: Create a tenant
12 | * description: Creates a new tenant in a database.
13 | * operationId: createTenant
14 | * requestBody:
15 | * description: A wrapper for the tenant name.
16 | * content:
17 | * application/json:
18 | * schema:
19 | * $ref: '#/components/schemas/CreateTenantRequest'
20 | * examples:
21 | * Create Tenant Request:
22 | * summary: Creates a named tenant
23 | * description: Create Tenant Request
24 | * value:
25 | * name: My Sandbox
26 | * responses:
27 | * "201":
28 | * description: Tenant created
29 | * content:
30 | * application/json:
31 | * schema:
32 | * $ref: '#/components/schemas/Tenant'
33 | * "401":
34 | * description: Unauthorized
35 | * content:
36 | * application/json:
37 | * schema:
38 | * $ref: '#/components/schemas/APIError'
39 | * "404":
40 | * description: Database not found
41 | * content:
42 | * application/json:
43 | * schema:
44 | * $ref: '#/components/schemas/APIError'
45 | */
46 | export async function POST(
47 | config: Config,
48 | init: RequestInit & { request: Request }
49 | ) {
50 | init.body = init.request.body;
51 | init.method = 'POST';
52 | const url = `${apiRoutes(config).TENANTS}`;
53 |
54 | return await request(url, init, config);
55 | }
56 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/tenants/[tenantId]/DELETE.ts:
--------------------------------------------------------------------------------
1 | import { apiRoutes } from '../../../utils/routes';
2 | import { Config } from '../../../../utils/Config';
3 | import fetch from '../../../utils/request';
4 |
5 | /**
6 | * @swagger
7 | * /api/tenants/{tenantId}:
8 | * delete:
9 | * tags:
10 | * - tenants
11 | * summary: Deletes a tenant.
12 | * operationId: deleteTenant
13 | * parameters:
14 | * - name: tenantId
15 | * in: path
16 | * required: true
17 | * schema:
18 | * type: string
19 | * responses:
20 | * "204":
21 | * description: Tenant deleted
22 | * "401":
23 | * description: Unauthorized
24 | * content:
25 | * application/json:
26 | * schema:
27 | * $ref: '#/components/schemas/APIError'
28 | * "404":
29 | * description: Tenant not found
30 | * content:
31 | * application/json:
32 | * schema:
33 | * $ref: '#/components/schemas/APIError'
34 | */
35 | export async function DELETE(
36 | config: Config,
37 | init: RequestInit & { request: Request }
38 | ) {
39 | const yurl = new URL(init.request.url);
40 | const [tenantId] = yurl.pathname.split('/').reverse();
41 | if (!tenantId) {
42 | return new Response(null, { status: 404 });
43 | }
44 |
45 | init.method = 'DELETE';
46 | const url = `${apiRoutes(config).TENANT(tenantId)}`;
47 |
48 | return await fetch(url, init, config);
49 | }
50 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/tenants/[tenantId]/GET.ts:
--------------------------------------------------------------------------------
1 | import { apiRoutes } from '../../../utils/routes';
2 | import { Config } from '../../../../utils/Config';
3 | import request from '../../../utils/request';
4 |
5 | /**
6 | * @swagger
7 | * /api/tenants/{tenantId}:
8 | * get:
9 | * tags:
10 | * - tenants
11 | * summary: Obtains a specific tenant.
12 | * operationId: getTenant
13 | * parameters:
14 | * - name: tenantId
15 | * in: path
16 | * required: true
17 | * schema:
18 | * type: string
19 | * responses:
20 | * "200":
21 | * description: the desired tenant
22 | * content:
23 | * application/json:
24 | * schema:
25 | * $ref: '#/components/schemas/Tenant'
26 | * "401":
27 | * description: Unauthorized
28 | * content:
29 | * application/json:
30 | * schema:
31 | * $ref: '#/components/schemas/APIError'
32 | * "404":
33 | * description: Tenant not found
34 | * content:
35 | * application/json:
36 | * schema:
37 | * $ref: '#/components/schemas/APIError'
38 | */
39 | export async function GET(
40 | config: Config,
41 | init: RequestInit & { request: Request },
42 | log: (message: string | unknown, meta?: Record) => void
43 | ) {
44 | const yurl = new URL(init.request.url);
45 | const [tenantId] = yurl.pathname.split('/').reverse();
46 | if (!tenantId) {
47 | log('[GET] No tenant id provided.');
48 | return new Response(null, { status: 404 });
49 | }
50 |
51 | init.method = 'GET';
52 | const url = `${apiRoutes(config).TENANT(tenantId)}`;
53 |
54 | return await request(url, init, config);
55 | }
56 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/tenants/[tenantId]/PUT.ts:
--------------------------------------------------------------------------------
1 | import { apiRoutes } from '../../../utils/routes';
2 | import { Config } from '../../../../utils/Config';
3 | import fetch from '../../../utils/request';
4 |
5 | /**
6 | * @swagger
7 | * /api/tenants/{tenantId}:
8 | * put:
9 | * tags:
10 | * - tenants
11 | * summary: Obtains a specific tenant.
12 | * operationId: updateTenant
13 | * parameters:
14 | * - name: tenantId
15 | * in: path
16 | * required: true
17 | * schema:
18 | * type: string
19 | * responses:
20 | * "201":
21 | * description: update an existing tenant
22 | * content:
23 | * application/json:
24 | * schema:
25 | * $ref: '#/components/schemas/Tenant'
26 | * "401":
27 | * description: Unauthorized
28 | * content:
29 | * application/json:
30 | * schema:
31 | * $ref: '#/components/schemas/APIError'
32 | * "404":
33 | * description: Tenant not found
34 | * content:
35 | * application/json:
36 | * schema:
37 | * $ref: '#/components/schemas/APIError'
38 | */
39 | export async function PUT(
40 | config: Config,
41 | init: RequestInit & { request: Request }
42 | ) {
43 | const yurl = new URL(init.request.url);
44 | const [tenantId] = yurl.pathname.split('/').reverse();
45 | if (!tenantId) {
46 | return new Response(null, { status: 404 });
47 | }
48 | init.body = init.request.body;
49 | init.method = 'PUT';
50 | const url = `${apiRoutes(config).TENANT(tenantId)}`;
51 |
52 | return await fetch(url, init, config);
53 | }
54 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/tenants/[tenantId]/users/GET.ts:
--------------------------------------------------------------------------------
1 | import { apiRoutes } from '../../../../utils/routes';
2 | import { Config } from '../../../../../utils/Config';
3 | import request from '../../../../utils/request';
4 |
5 | /**
6 | * @swagger
7 | * /api/tenants/{tenantId}/users:
8 | * get:
9 | * tags:
10 | * - users
11 | * summary: List tenant users
12 | * description: Lists users that are associated with the specified tenant.
13 | * operationId: listTenantUsers
14 | * parameters:
15 | * - name: tenantId
16 | * in: path
17 | * required: true
18 | * schema:
19 | * type: string
20 | * responses:
21 | * "200":
22 | * description: Users found
23 | * content:
24 | * application/json:
25 | * schema:
26 | * type: array
27 | * items:
28 | * $ref: '#/components/schemas/User'
29 | * "401":
30 | * description: Unauthorized
31 | * content:
32 | * application/json:
33 | * schema:
34 | * $ref: '#/components/schemas/APIError'
35 | */
36 | export async function GET(
37 | config: Config,
38 | init: RequestInit & { request: Request }
39 | ) {
40 | const yurl = new URL(init.request.url);
41 | const [, tenantId] = yurl.pathname.split('/').reverse();
42 |
43 | const url = `${apiRoutes(config).TENANT_USERS(tenantId)}`;
44 | return await request(url, init, config);
45 | }
46 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/tenants/[tenantId]/users/POST.ts:
--------------------------------------------------------------------------------
1 | import { ActiveSession } from '../../../../utils/auth';
2 | import fetch from '../../../../utils/request';
3 | import { apiRoutes } from '../../../../utils/routes';
4 | import { Config } from '../../../../../utils/Config';
5 |
6 | /**
7 | * @swagger
8 | * /api/tenants/{tenantId}/users:
9 | * post:
10 | * tags:
11 | * - users
12 | * summary: Create a user in a tenant
13 | * description: Creates a new user and associates that user with the specified
14 | * tenant.
15 | * operationId: createTenantUser
16 | * parameters:
17 | * - name: tenantId
18 | * in: path
19 | * required: true
20 | * schema:
21 | * type: string
22 | * requestBody:
23 | * description: |
24 | * The email and password combination the user will use to authenticate.
25 | * The `name` is optional; if provided it will be recorded in the `users` table.
26 | * content:
27 | * application/json:
28 | * schema:
29 | * $ref: '#/components/schemas/CreateBasicUserRequest'
30 | * examples:
31 | * Create User Request:
32 | * summary: Creates a user with basic credentials
33 | * description: Create User Request
34 | * value:
35 | * email: a.user@somedomain.com
36 | * password: somepassword
37 | * name: A. User
38 | * responses:
39 | * "201":
40 | * description: User created
41 | * content:
42 | * application/json:
43 | * schema:
44 | * $ref: '#/components/schemas/User'
45 | */
46 | export async function POST(
47 | config: Config,
48 | session: ActiveSession,
49 | init: RequestInit & { request: Request }
50 | ) {
51 | const yurl = new URL(init.request.url);
52 | const [, tenantId] = yurl.pathname.split('/').reverse();
53 | init.body = JSON.stringify({ email: session.email });
54 | init.method = 'POST';
55 | const url = apiRoutes(config).TENANT_USERS(tenantId);
56 |
57 | return await fetch(url, init, config);
58 | }
59 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/tenants/[tenantId]/users/[userId]/DELETE.ts:
--------------------------------------------------------------------------------
1 | import { apiRoutes } from '../../../../../utils/routes';
2 | import fetch from '../../../../../utils/request';
3 | import { Config } from '../../../../../../utils/Config';
4 |
5 | /**
6 | * @swagger
7 | * /api/tenants/{tenantId}/users/{userId}/link:
8 | * delete:
9 | * tags:
10 | * - tenants
11 | * summary: removes a user from a tenant
12 | * description: removes an associated user from a specified
13 | * tenant.
14 | * operationId: leaveTenant
15 | * parameters:
16 | * - name: tenantId
17 | * in: path
18 | * required: true
19 | * schema:
20 | * type: string
21 | * - name: userId
22 | * in: path
23 | * required: true
24 | * schema:
25 | * type: string
26 | * - name: email
27 | * in: path
28 | * required: true
29 | * schema:
30 | * type: string
31 |
32 | * responses:
33 | * "204":
34 | * description: User removed
35 | */
36 |
37 | export async function DELETE(
38 | config: Config,
39 | init: RequestInit & { request: Request }
40 | ) {
41 | const yurl = new URL(init.request.url);
42 |
43 | const [, userId, , tenantId] = yurl.pathname.split('/').reverse();
44 | config.tenantId = tenantId;
45 | config.userId = userId;
46 |
47 | init.method = 'DELETE';
48 | const url = `${apiRoutes(config).TENANT_USER}/link`;
49 |
50 | return await fetch(url, init, config);
51 | }
52 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/tenants/[tenantId]/users/[userId]/PUT.ts:
--------------------------------------------------------------------------------
1 | import { apiRoutes } from '../../../../../utils/routes';
2 | import fetch from '../../../../../utils/request';
3 | import { Config } from '../../../../../../utils/Config';
4 | /**
5 | * @swagger
6 | * /api/tenants/{tenantId}/users/{userId}/link:
7 | * put:
8 | * tags:
9 | * - tenants
10 | * summary: associates an existing user with the tenant
11 | * operationId: linkUser
12 | * parameters:
13 | * - name: tenantId
14 | * in: path
15 | * required: true
16 | * schema:
17 | * type: string
18 | * - name: userId
19 | * in: path
20 | * required: true
21 | * schema:
22 | * type: string
23 |
24 | * requestBody:
25 | * description: |
26 | * The email of the user you want to add to a tenant.
27 | * content:
28 | * application/json:
29 | * schema:
30 | * $ref: '#/components/schemas/AssociateUserRequest'
31 | * responses:
32 | * "201":
33 | * description: add user to tenant
34 | */
35 |
36 | export async function PUT(
37 | config: Config,
38 | init: RequestInit & { request: Request }
39 | ) {
40 | const yurl = new URL(init.request.url);
41 |
42 | const [, userId, , tenantId] = yurl.pathname.split('/').reverse();
43 | config.tenantId = tenantId;
44 | config.userId = userId;
45 |
46 | init.method = 'PUT';
47 | const url = `${apiRoutes(config).TENANT_USER}/link`;
48 |
49 | return await fetch(url, init, config);
50 | }
51 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/tenants/[tenantId]/users/[userId]/index.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../../../../../../utils/Config';
2 | import { DefaultNileAuthRoutes, urlMatches } from '../../../../../utils/routes';
3 | import { Routes } from '../../../../../types';
4 | import auth from '../../../../../utils/auth';
5 | import Logger from '../../../../../../utils/Logger';
6 |
7 | import { DELETE } from './DELETE';
8 | import { PUT } from './PUT';
9 |
10 | const key = 'TENANT_USER';
11 |
12 | export default async function route(request: Request, config: Config) {
13 | const { info } = Logger(
14 | { ...config, debug: config.debug } as Config,
15 | `[ROUTES][${key}]`
16 | );
17 | const session = await auth(request, config);
18 |
19 | if (!session) {
20 | info('401');
21 | return new Response(null, { status: 401 });
22 | }
23 | const yurl = new URL(request.url);
24 | const [, userId] = yurl.pathname.split('/').reverse();
25 |
26 | if (!userId) {
27 | info('No tenant id found in path');
28 | return new Response(null, { status: 404 });
29 | }
30 |
31 | switch (request.method) {
32 | case 'PUT':
33 | return await PUT(config, { request });
34 | case 'DELETE':
35 | return await DELETE(config, { request });
36 |
37 | default:
38 | return new Response('method not allowed', { status: 405 });
39 | }
40 | }
41 |
42 | export function matches(configRoutes: Routes, request: Request): boolean {
43 | const url = new URL(request.url);
44 | const [, userId, possibleTenantId, tenantId] = url.pathname
45 | .split('/')
46 | .reverse();
47 | let route = configRoutes[key]
48 | .replace('{tenantId}', tenantId)
49 | .replace('{userId}', userId);
50 | if (userId === 'users') {
51 | route = configRoutes[key].replace('{tenantId}', possibleTenantId);
52 | }
53 | return urlMatches(request.url, route);
54 | }
55 |
56 | export async function fetchTenantUser(
57 | config: Config,
58 | method: 'DELETE' | 'PUT'
59 | ) {
60 | if (!config.tenantId) {
61 | throw new Error(
62 | 'The tenantId context is missing. Call nile.setContext({ tenantId })'
63 | );
64 | }
65 |
66 | if (!config.userId) {
67 | throw new Error(
68 | 'the userId context is missing. Call nile.setContext({ userId })'
69 | );
70 | }
71 |
72 | const clientUrl = `${config.serverOrigin}${
73 | config.routePrefix
74 | }${DefaultNileAuthRoutes.TENANT_USER.replace(
75 | '{tenantId}',
76 | config.tenantId
77 | ).replace('{userId}', config.userId)}/link`;
78 | const req = new Request(clientUrl, {
79 | headers: config.headers,
80 | method,
81 | });
82 |
83 | return (await config.handlers[method](req)) as Response;
84 | }
85 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/tenants/apiTenants.test.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../../../utils/Config';
2 | import { X_NILE_TENANT } from '../../../utils/constants';
3 | import fetch from '../../utils/request';
4 |
5 | import route from '.';
6 |
7 | const utilRequest = fetch as jest.Mock;
8 |
9 | jest.mock('../../utils/request', () => jest.fn());
10 | jest.mock('../../utils/auth', () => () => ({
11 | id: 'something',
12 | }));
13 |
14 | describe('tenants route', () => {
15 | afterEach(() => {
16 | jest.clearAllMocks();
17 | });
18 |
19 | it('should post to v2 tenants', async () => {
20 | const _res = new Request('http://thenile.dev?tenantId=123', {
21 | method: 'POST',
22 | });
23 | await route(
24 | _res,
25 | new Config({
26 | apiUrl: 'http://thenile.dev/v2/databases/testdb',
27 | })
28 | );
29 | expect(utilRequest).toHaveBeenCalledWith(
30 | 'http://thenile.dev/v2/databases/testdb/tenants',
31 | expect.objectContaining({ method: 'POST' }),
32 | expect.objectContaining({})
33 | );
34 | });
35 | it('should GET to v2 tenant users with params', async () => {
36 | const _res = new Request('http://localhost:3000/users/${userId}/tenants', {
37 | method: 'GET',
38 | });
39 | await route(
40 | _res,
41 | new Config({
42 | apiUrl: 'http://thenile.dev/v2/databases/testdb',
43 | })
44 | );
45 |
46 | expect(utilRequest.mock.calls[0][0]).toEqual(
47 | 'http://thenile.dev/v2/databases/testdb/users/something/tenants'
48 | );
49 | });
50 | it('should GET to v2 tenant users with headers', async () => {
51 | const _res = new Request('http://localhost:3000', {
52 | headers: new Headers({
53 | [X_NILE_TENANT]: '123',
54 | }),
55 | });
56 | await route(
57 | _res,
58 | new Config({
59 | apiUrl: 'http://thenile.dev/v2/databases/testdb',
60 | })
61 | );
62 | expect(utilRequest.mock.calls[0][0]).toEqual(
63 | 'http://thenile.dev/v2/databases/testdb/users/something/tenants'
64 | );
65 | });
66 | it('should GET to v2 tenant users with cookies', async () => {
67 | const _res = new Request('http://localhost:3000', {
68 | headers: new Headers({
69 | cookie: `token=abunchofgarbage; ${X_NILE_TENANT}=456`,
70 | }),
71 | });
72 | await route(
73 | _res,
74 | new Config({
75 | apiUrl: 'http://thenile.dev/v2/databases/testdb',
76 | })
77 | );
78 | expect(utilRequest.mock.calls[0][0]).toEqual(
79 | 'http://thenile.dev/v2/databases/testdb/users/something/tenants'
80 | );
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/users/GET.ts:
--------------------------------------------------------------------------------
1 | import { getTenantFromHttp } from '../../../utils/fetch';
2 | import request from '../../utils/request';
3 | import { apiRoutes } from '../../utils/routes';
4 | import { Config } from '../../../utils/Config';
5 |
6 | /**
7 | * @swagger
8 | * /api/users:
9 | * get:
10 | * tags:
11 | * - users
12 | * summary: lists users in the tenant
13 | * description: Returns information about the users within the tenant
14 | * provided. You can also pass the a `nile.tenant_id` in the header or in a cookie.
15 | * operationId: listUsers
16 | * parameters:
17 | * - name: tenantId
18 | * in: query
19 | * schema:
20 | * type: string
21 | * responses:
22 | * "200":
23 | * description: A list of users
24 | * content:
25 | * application/json:
26 | * schema:
27 | * type: array
28 | * items:
29 | * $ref: '#/components/schemas/TenantUser'
30 | * "404":
31 | * description: Not found
32 | * content: {}
33 | * "401":
34 | * description: Unauthorized
35 | * content: {}
36 | */
37 | export async function GET(
38 | config: Config,
39 | init: RequestInit & { request: Request },
40 | log: (message: string | unknown, meta?: Record) => void
41 | ) {
42 | const yurl = new URL(init.request.url);
43 | const tenantId = yurl.searchParams.get('tenantId');
44 | const tenant = tenantId ?? getTenantFromHttp(init.request.headers);
45 |
46 | if (!tenant) {
47 | log('[GET] No tenant id provided.');
48 | return new Response(null, { status: 404 });
49 | }
50 | const url = apiRoutes(config).TENANT_USERS(tenant);
51 | init.method = 'GET';
52 | return await request(url, init, config);
53 | }
54 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/users/[userId]/PUT.ts:
--------------------------------------------------------------------------------
1 | import { apiRoutes } from '../../../utils/routes';
2 | import fetch from '../../../utils/request';
3 | import { ActiveSession } from '../../../utils/auth';
4 | import { Config } from '../../../../utils/Config';
5 |
6 | /**
7 | * @swagger
8 | * /api/users/{userid}:
9 | * put:
10 | * tags:
11 | * - users
12 | * summary: update a user
13 | * description: updates a user within the tenant
14 | * operationId: updateUser
15 | * parameters:
16 | * - name: userid
17 | * in: path
18 | * required: true
19 | * schema:
20 | * type: string
21 | * requestBody:
22 | * description: |-
23 | * Update a user
24 | * content:
25 | * application/json:
26 | * schema:
27 | * $ref: '#/components/schemas/UpdateUserRequest'
28 | * responses:
29 | * "200":
30 | * description: An updated user
31 | * content:
32 | * application/json:
33 | * schema:
34 | * $ref: '#/components/schemas/User'
35 | * "404":
36 | * description: Not found
37 | * content: {}
38 | * "401":
39 | * description: Unauthorized
40 | * content: {}
41 | */
42 |
43 | export async function PUT(
44 | config: Config,
45 | session: null | undefined | ActiveSession,
46 | init: RequestInit & { request: Request }
47 | ) {
48 | if (!session) {
49 | return new Response(null, { status: 401 });
50 | }
51 | init.body = init.request.body;
52 | init.method = 'PUT';
53 |
54 | // update the user
55 |
56 | const [userId] = new URL(init.request.url).pathname.split('/').reverse();
57 |
58 | const url = apiRoutes(config).USER(userId);
59 |
60 | return await fetch(url, init, config);
61 | }
62 |
--------------------------------------------------------------------------------
/packages/server/src/api/routes/users/index.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '../../types';
2 | import {
3 | DefaultNileAuthRoutes,
4 | isUUID,
5 | prefixAppRoute,
6 | urlMatches,
7 | } from '../../utils/routes';
8 | import auth from '../../utils/auth';
9 | import { Config } from '../../../utils/Config';
10 | import Logger from '../../../utils/Logger';
11 |
12 | import { POST } from './POST';
13 | import { GET } from './GET';
14 | import { PUT } from './[userId]/PUT';
15 |
16 | const key = 'USERS';
17 |
18 | export default async function route(request: Request, config: Config) {
19 | const { info } = Logger(
20 | { ...config, debug: config.debug } as Config,
21 | `[ROUTES][${key}]`
22 | );
23 | const session = await auth(request, config);
24 |
25 | switch (request.method) {
26 | case 'GET':
27 | return await GET(config, { request }, info);
28 | case 'POST':
29 | return await POST(config, { request });
30 | case 'PUT':
31 | return await PUT(config, session, { request });
32 |
33 | default:
34 | return new Response('method not allowed', { status: 405 });
35 | }
36 | }
37 | export function matches(configRoutes: Routes, request: Request): boolean {
38 | return urlMatches(request.url, configRoutes[key]);
39 | }
40 |
41 | export async function fetchUser(config: Config, method: 'PUT') {
42 | let clientUrl = `${prefixAppRoute(config)}${DefaultNileAuthRoutes.USERS}`;
43 |
44 | if (method === 'PUT')
45 | if (!config.userId) {
46 | throw new Error(
47 | 'Unable to update user, the userId context is missing. Call nile.setContext({ userId }), set nile.userId = "userId", or add it to the function call'
48 | );
49 | } else {
50 | clientUrl = `${prefixAppRoute(
51 | config
52 | )}${DefaultNileAuthRoutes.USER.replace('{userId}', config.userId)}`;
53 | }
54 | if (!isUUID(config.userId) && config.logger?.warn) {
55 | config.logger?.warn(
56 | 'nile.userId is not a valid UUID. This may lead to unexpected behavior in your application.'
57 | );
58 | }
59 |
60 | const init: RequestInit = {
61 | method,
62 | headers: config.headers,
63 | };
64 | const req = new Request(clientUrl, init);
65 |
66 | return (await config.handlers[method](req)) as Response;
67 | }
68 |
--------------------------------------------------------------------------------
/packages/server/src/api/types.ts:
--------------------------------------------------------------------------------
1 | import { ApiRoutePaths, ProxyPaths } from './utils/routes';
2 |
3 | export type Paths = ProxyPaths & ApiRoutePaths;
4 |
5 | export type Routes = {
6 | SIGNIN: string;
7 | SESSION: string;
8 | PROVIDERS: string;
9 | CSRF: string;
10 | CALLBACK: string;
11 | SIGNOUT: string;
12 | ERROR: string;
13 | ME: string;
14 | USER_TENANTS: string;
15 | USERS: string;
16 | TENANTS: string;
17 | TENANT: string;
18 | TENANT_USER: string;
19 | TENANT_USERS: string;
20 | SIGNUP: string;
21 | VERIFY_REQUEST: string;
22 | PASSWORD_RESET: string;
23 | LOG: string;
24 | VERIFY_EMAIL: string;
25 | };
26 |
--------------------------------------------------------------------------------
/packages/server/src/api/utils/auth.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../../utils/Config';
2 | import Logger from '../../utils/Logger';
3 |
4 | import request from './request';
5 |
6 | export type ProviderName =
7 | | 'discord'
8 | | 'github'
9 | | 'google'
10 | | 'hubspot'
11 | | 'linkedin'
12 | | 'slack'
13 | | 'twitter'
14 | | 'email' // magic link
15 | | 'credentials' // email + password
16 | | 'azure';
17 |
18 | export type Providers = {
19 | [providerName in ProviderName]: Provider;
20 | };
21 | export type Provider = {
22 | id: string;
23 | name: string;
24 | type: string;
25 | signinUrl: string;
26 | callbackUrl: string;
27 | };
28 |
29 | export type JWT = {
30 | email: string;
31 | sub: string;
32 | id: string;
33 | iat: number;
34 | exp: number;
35 | jti: string;
36 | };
37 |
38 | export type ActiveSession = {
39 | id: string;
40 | email: string;
41 | expires: string;
42 | user?: {
43 | id: string;
44 | name: string;
45 | image: string;
46 | email: string;
47 | emailVerified: void | Date;
48 | };
49 | };
50 | export default async function auth(
51 | req: Request,
52 | config: Config
53 | ): Promise {
54 | const { info, error } = Logger(config, '[nileauth]');
55 | info('checking auth');
56 |
57 | const sessionUrl = `${config.apiUrl}/auth/session`;
58 | info(`using session ${sessionUrl}`);
59 | // handle the pass through with posts
60 | req.headers.delete('content-length');
61 |
62 | const res = await request(sessionUrl, { request: req }, config);
63 | if (!res) {
64 | info('no session found');
65 | return undefined;
66 | }
67 | info('session active');
68 | try {
69 | const session = await new Response(res.body).json();
70 | if (Object.keys(session).length === 0) {
71 | return undefined;
72 | }
73 | return session;
74 | } catch (e) {
75 | error(e);
76 | return undefined;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/packages/server/src/auth/getCsrf.ts:
--------------------------------------------------------------------------------
1 | import { fetchCsrf } from '../api/routes/auth/csrf';
2 | import { updateHeaders } from '../utils/Event';
3 | import { Config } from '../utils/Config';
4 |
5 | import { parseCallback, parseCSRF, parseToken } from '.';
6 |
7 | export default async function getCsrf(
8 | config: Config,
9 | rawResponse = false
10 | ) {
11 | const res = await fetchCsrf(config);
12 | // we're gonna use it, so set the headers now.
13 | const csrfCook = parseCSRF(res.headers);
14 |
15 | // prefer the csrf from the headers over the saved one
16 | if (csrfCook) {
17 | const [, value] = csrfCook.split('=');
18 | const [token] = decodeURIComponent(value).split('|');
19 |
20 | const setCookie = res.headers.get('set-cookie');
21 | if (setCookie) {
22 | const cookie = [
23 | csrfCook,
24 | parseCallback(res.headers),
25 | parseToken(res.headers),
26 | ]
27 | .filter(Boolean)
28 | .join('; ');
29 | config.headers.set('cookie', cookie);
30 | updateHeaders(new Headers({ cookie }));
31 | }
32 | if (!rawResponse) {
33 | return { csrfToken: token };
34 | }
35 | } else {
36 | // for csrf, preserve the existing cookies
37 | const existingCookie = config.headers.get('cookie');
38 | const cookieParts = [];
39 | if (existingCookie) {
40 | cookieParts.push(
41 | parseToken(config.headers),
42 | parseCallback(config.headers)
43 | );
44 | }
45 | if (csrfCook) {
46 | cookieParts.push(csrfCook);
47 | } else {
48 | // use the one tha tis already there
49 | cookieParts.push(parseCSRF(config.headers));
50 | }
51 | const cookie = cookieParts.filter(Boolean).join('; ');
52 |
53 | // we need to do it in both places in case its the very first time
54 | config.headers.set('cookie', cookie);
55 | updateHeaders(new Headers({ cookie }));
56 | }
57 |
58 | if (rawResponse) {
59 | return res as T;
60 | }
61 |
62 | try {
63 | return (await res.clone().json()) as T;
64 | } catch {
65 | return res as T;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/server/src/db/DBManager.ts:
--------------------------------------------------------------------------------
1 | import pg from 'pg';
2 |
3 | import { Config } from '../utils/Config';
4 | import { watchEvictPool } from '../utils/Event';
5 | import Logger from '../utils/Logger';
6 | import { NileConfig } from '../types';
7 |
8 | import NileDatabase from './NileInstance';
9 |
10 | export default class DBManager {
11 | connections: Map;
12 | cleared: boolean;
13 | private poolWatcherFn: (id: undefined | null | string) => void;
14 |
15 | private makeId(
16 | tenantId?: string | undefined | null,
17 | userId?: string | undefined | null
18 | ) {
19 | if (tenantId && userId) {
20 | return `${tenantId}:${userId}`;
21 | }
22 | if (tenantId) {
23 | return `${tenantId}`;
24 | }
25 | return 'base';
26 | }
27 | constructor(config: NileConfig) {
28 | this.cleared = false;
29 | this.connections = new Map();
30 | this.poolWatcherFn = this.poolWatcher(config);
31 | watchEvictPool(this.poolWatcherFn);
32 | }
33 | poolWatcher = (config: NileConfig) => (id: undefined | null | string) => {
34 | const { info, warn } = Logger(config, '[DBManager]');
35 | if (id && this.connections.has(id)) {
36 | info(`Removing ${id} from db connection pool.`);
37 | const connection = this.connections.get(id);
38 | connection?.shutdown();
39 | this.connections.delete(id);
40 | } else {
41 | warn(`missed eviction of ${id}`);
42 | }
43 | };
44 |
45 | getConnection = (config: NileConfig): pg.Pool => {
46 | const { info } = Logger(config, '[DBManager]');
47 | const id = this.makeId(config.tenantId, config.userId);
48 |
49 | const existing = this.connections.get(id);
50 | info(`# of instances: ${this.connections.size}`);
51 | if (existing) {
52 | info(`returning existing ${id}`);
53 | existing.startTimeout();
54 | return existing.pool;
55 | }
56 | const newOne = new NileDatabase(new Config(config), id);
57 | this.connections.set(id, newOne);
58 | info(`created new ${id}`);
59 | info(`# of instances: ${this.connections.size}`);
60 | if (this.cleared) {
61 | this.cleared = false;
62 | }
63 | return newOne.pool;
64 | };
65 |
66 | clear = (config: NileConfig) => {
67 | const { info } = Logger(config, '[DBManager]');
68 | info(`Clearing all connections ${this.connections.size}`);
69 | this.cleared = true;
70 | this.connections.forEach((connection) => {
71 | connection.shutdown();
72 | });
73 | this.connections.clear();
74 | };
75 | }
76 |
--------------------------------------------------------------------------------
/packages/server/src/db/NileInstance.test.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../utils/Config';
2 | import { watchEvictPool } from '../utils/Event';
3 |
4 | import NileDatabase from './NileInstance';
5 |
6 | describe('nile instance', () => {
7 | it('evitcs pools', (done) => {
8 | const config = new Config({
9 | databaseId: 'databaseId',
10 | user: 'username',
11 | password: 'password',
12 | db: {
13 | idleTimeoutMillis: 1,
14 | },
15 | });
16 | new NileDatabase(config, 'someId');
17 | watchEvictPool((id) => {
18 | expect(id).toEqual('someId');
19 | done();
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/packages/server/src/db/PoolProxy.ts:
--------------------------------------------------------------------------------
1 | import pg from 'pg';
2 |
3 | import { Config } from '../utils/Config';
4 | import Logger from '../utils/Logger';
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
7 | type AllowAny = any;
8 |
9 | export function createProxyForPool(pool: pg.Pool, config: Config): pg.Pool {
10 | const { info, error } = Logger(config, '[pool]');
11 | return new Proxy(pool, {
12 | get(target: AllowAny, property) {
13 | if (property === 'query') {
14 | // give connection string a pass for these problems
15 | if (!config.db.connectionString) {
16 | if (!config.db.user || !config.db.password) {
17 | error(
18 | 'Cannot connect to the database. User and/or password are missing. Generate them at https://console.thenile.dev'
19 | );
20 | } else if (!config.db.database) {
21 | error(
22 | 'Unable to obtain database name. Is process.env.NILEDB_POSTGRES_URL set?'
23 | );
24 | }
25 | }
26 | const caller = target[property];
27 | return function query(...args: AllowAny) {
28 | info('query', ...args);
29 | // @ts-expect-error - not mine
30 | const called = caller.apply(this, args);
31 | return called;
32 | };
33 | }
34 | return target[property];
35 | },
36 | }) as pg.Pool;
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server/src/db/db.test.ts:
--------------------------------------------------------------------------------
1 | import NileDB from './index';
2 |
3 | const properties = [
4 | 'connections',
5 | 'clear',
6 | 'cleared',
7 | 'getConnection',
8 | 'poolWatcher',
9 | 'poolWatcherFn',
10 | ];
11 | describe('db', () => {
12 | it('has expected properties', () => {
13 | const db = new NileDB({
14 | databaseId: 'databaseId',
15 | databaseName: 'databaseName',
16 | user: 'username',
17 | password: 'password',
18 | debug: false,
19 | db: {
20 | port: 4433,
21 | },
22 | tenantId: null,
23 | userId: null,
24 | });
25 | expect(Object.keys(db).sort()).toEqual(properties.sort());
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/packages/server/src/db/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './DBManager';
2 |
--------------------------------------------------------------------------------
/packages/server/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './users/types';
3 | export * from './tenants/types';
4 | export { JWT, ActiveSession, Providers } from './api/utils/auth';
5 |
6 | export { create as Nile, Server } from './Server';
7 | export { parseCSRF, parseCallback, parseToken } from './auth';
8 |
--------------------------------------------------------------------------------
/packages/server/src/lib/express.test.ts:
--------------------------------------------------------------------------------
1 | import { Server } from '../Server';
2 |
3 | import { expressPaths } from './express';
4 |
5 | describe('express', () => {
6 | it('cleans express paths', () => {
7 | const nile = new Server();
8 | const { paths } = expressPaths(nile);
9 | expect(Object.keys(paths)).toEqual(['get', 'post', 'put', 'delete']);
10 | expect(paths.delete).toEqual([
11 | '/api/tenants/:tenantId/users/:userId',
12 | '/api/tenants/:tenantId',
13 | ]);
14 | expect(paths.post).toEqual([
15 | '/api/tenants/:tenantId/users',
16 | '/api/signup',
17 | '/api/users',
18 | '/api/tenants',
19 | '/api/auth/session',
20 | '/api/auth/signin/:provider',
21 | '/api/auth/reset-password',
22 | '/api/auth/providers',
23 | '/api/auth/csrf',
24 | '/api/auth/callback/:provider',
25 | '/api/auth/signout',
26 | ]);
27 | expect(paths.put).toEqual([
28 | '/api/tenants/:tenantId/users',
29 | '/api/users',
30 | '/api/tenants/:tenantId',
31 | '/api/auth/reset-password',
32 | ]);
33 | expect(paths.get).toEqual([
34 | '/api/me',
35 | '/api/tenants/:tenantId/users',
36 | '/api/tenants',
37 | '/api/tenants/:tenantId',
38 | '/api/auth/session',
39 | '/api/auth/signin',
40 | '/api/auth/providers',
41 | '/api/auth/csrf',
42 | '/api/auth/reset-password',
43 | '/api/auth/callback',
44 | '/api/auth/signout',
45 | '/api/auth/verify-request',
46 | '/api/auth/error',
47 | ]);
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/packages/server/src/lib/nitro.ts:
--------------------------------------------------------------------------------
1 | import { EventHandlerRequest, getRequestURL, H3Event, readRawBody } from 'h3';
2 |
3 | import { Server } from '../Server';
4 |
5 | const convertHeader = ([key, value]: [
6 | string,
7 | string | string[] | undefined
8 | ]) => [
9 | key.toLowerCase(),
10 | Array.isArray(value) ? value.join(', ') : String(value),
11 | ];
12 | export async function convertToRequest(
13 | event: H3Event,
14 | nile: Server
15 | ) {
16 | const { handlers } = nile;
17 | const url = getRequestURL(event);
18 | const reqHeaders = event.node.req.headers;
19 | const headers: HeadersInit = reqHeaders
20 | ? Object.fromEntries(Object.entries(reqHeaders).map(convertHeader))
21 | : {};
22 | const method = event.node.req.method || 'GET';
23 | const body =
24 | method !== 'GET' && method !== 'HEAD' ? await readRawBody(event) : null;
25 |
26 | const request = new Request(url, {
27 | method,
28 | headers,
29 | body: body ? JSON.stringify(body) : null,
30 | });
31 | switch (request.method) {
32 | case 'GET':
33 | return handlers.GET(request);
34 | case 'POST':
35 | return handlers.POST(request);
36 | case 'PUT':
37 | return handlers.PUT(request);
38 | case 'DELETE':
39 | return handlers.DELETE(request);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/server/src/tenants/types.ts:
--------------------------------------------------------------------------------
1 | export type Tenant = {
2 | id: string;
3 | name: string;
4 | };
5 |
--------------------------------------------------------------------------------
/packages/server/src/users/types.ts:
--------------------------------------------------------------------------------
1 | export interface CreateBasicUserRequest {
2 | email: string;
3 | password: string;
4 | name?: string;
5 | familyName?: string;
6 | givenName?: string;
7 | picture?: string;
8 | // create a tenant for the new user to an existing tenant
9 | newTenantName?: string;
10 | // add the new user to an existing tenant
11 | tenantId?: string;
12 | }
13 | export interface CreateTenantUserRequest {
14 | email: string;
15 | password: string;
16 | name?: string;
17 | familyName?: string;
18 | givenName?: string;
19 | picture?: string;
20 | }
21 | export const LoginUserResponseTokenTypeEnum = {
22 | AccessToken: 'ACCESS_TOKEN',
23 | RefreshToken: 'REFRESH_TOKEN',
24 | IdToken: 'ID_TOKEN',
25 | } as const;
26 | export type LoginUserResponseTokenTypeEnum =
27 | (typeof LoginUserResponseTokenTypeEnum)[keyof typeof LoginUserResponseTokenTypeEnum];
28 |
29 | export interface LoginUserResponseToken {
30 | jwt: string;
31 | maxAge: number;
32 | type: LoginUserResponseTokenTypeEnum;
33 | }
34 | export interface LoginUserResponse {
35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
36 | [key: string]: any;
37 | id: string;
38 | token: LoginUserResponseToken;
39 | }
40 | export interface User {
41 | id: string;
42 | email: string;
43 | name?: string | null;
44 | familyName?: string | null;
45 | givenName?: string | null;
46 | picture?: string | null;
47 | created: string;
48 | updated?: string;
49 | emailVerified?: string | null;
50 | tenants: string[];
51 | }
52 |
--------------------------------------------------------------------------------
/packages/server/src/utils/Config/envVars.test.ts:
--------------------------------------------------------------------------------
1 | import { getPassword, getUsername } from './envVars';
2 |
3 | describe('env vars', () => {
4 | const prevURL = process.env.NILEDB_POSTGRES_URL;
5 | const prevUser = process.env.NILEDB_USER;
6 | const prevPass = process.env.NILEDB_PASSWORD;
7 | afterAll(() => {
8 | process.env.NILEDB_POSTGRES_URL = prevURL;
9 | process.env.NILEDB_USER = prevUser;
10 | process.env.NILEDB_PASSWORD = prevPass;
11 | });
12 | it('prefers the username/password from the env vars over NILEDB_POSTGRES_URL', () => {
13 | const password = 'password';
14 | const username = 'username';
15 | process.env.NILEDB_POSTGRES_URL = `postgres://${username}:${password}@us-west-2.db.dev.thenile.dev/niledb_cyan_tree`;
16 | expect(getPassword({})).toEqual(prevPass);
17 | expect(getUsername({})).toEqual(prevUser);
18 | });
19 | it('gets a username/password from NILEDB_POSTGRES_URL', () => {
20 | const password = 'password';
21 | const username = 'username';
22 | process.env.NILEDB_PASSWORD = '';
23 | process.env.NILEDB_USER = '';
24 | process.env.NILEDB_POSTGRES_URL = `postgres://${username}:${password}@us-west-2.db.dev.thenile.dev/niledb_cyan_tree`;
25 | expect(getPassword({})).toEqual(password);
26 | expect(getUsername({})).toEqual(username);
27 | });
28 | it('leaves username/password alone if it is not in the NILEDB_POSTGRES_URL', () => {
29 | const password = 'password';
30 | const username = 'username';
31 | process.env.NILEDB_USER = 'username';
32 | process.env.NILEDB_PASSWORD = 'password';
33 | process.env.NILEDB_POSTGRES_URL =
34 | 'postgres://us-west-2.db.dev.thenile.dev/niledb_cyan_tree';
35 | expect(getPassword({})).toEqual(password);
36 | expect(getUsername({})).toEqual(username);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/packages/server/src/utils/Event/index.ts:
--------------------------------------------------------------------------------
1 | // Define a map of event names to value types
2 | type EventMap = {
3 | [Events.User]: string | null | undefined;
4 | [Events.Tenant]: string | null | undefined;
5 | [Events.Token]: string | null | undefined;
6 | [Events.EvictPool]: string | null | undefined;
7 | [Events.Headers]: Headers | null | undefined;
8 | };
9 |
10 | // Generic EventFn now uses the EventMap
11 | type EventFn = (
12 | params: EventMap[K]
13 | ) => void;
14 |
15 | enum Events {
16 | User = 'userId',
17 | Tenant = 'tenantId',
18 | Token = 'token',
19 | EvictPool = 'EvictPool',
20 | Headers = 'headers',
21 | }
22 |
23 | class Eventer> {
24 | private events: { [K in keyof E]?: Array<(value: E[K]) => void> } = {};
25 |
26 | publish(eventName: K, value: E[K]) {
27 | const callbacks = this.events[eventName];
28 | if (callbacks) {
29 | for (const callback of callbacks) {
30 | callback(value);
31 | }
32 | }
33 | }
34 |
35 | subscribe(eventName: K, callback: (value: E[K]) => void) {
36 | if (!this.events[eventName]) {
37 | this.events[eventName] = [];
38 | }
39 | this.events[eventName].push(callback);
40 | }
41 |
42 | unsubscribe(
43 | eventName: K,
44 | callback: (value: E[K]) => void
45 | ) {
46 | const callbacks = this.events[eventName];
47 | if (!callbacks) return;
48 |
49 | const index = callbacks.indexOf(callback);
50 | if (index !== -1) {
51 | callbacks.splice(index, 1);
52 | }
53 |
54 | if (callbacks.length === 0) {
55 | delete this.events[eventName];
56 | }
57 | }
58 | }
59 |
60 | const eventer = new Eventer();
61 |
62 | export const updateTenantId = (tenantId: EventMap[Events.Tenant]) => {
63 | eventer.publish(Events.Tenant, tenantId);
64 | };
65 | export const watchTenantId = (cb: EventFn) =>
66 | eventer.subscribe(Events.Tenant, cb);
67 |
68 | export const updateUserId = (userId: EventMap[Events.User]) => {
69 | eventer.publish(Events.User, userId);
70 | };
71 | export const watchUserId = (cb: EventFn) =>
72 | eventer.subscribe(Events.User, cb);
73 |
74 | export const updateToken = (token: EventMap[Events.Token]) => {
75 | eventer.publish(Events.Token, token);
76 | };
77 |
78 | export const evictPool = (val: EventMap[Events.EvictPool]) => {
79 | eventer.publish(Events.EvictPool, val);
80 | };
81 | export const watchEvictPool = (cb: EventFn) =>
82 | eventer.subscribe(Events.EvictPool, cb);
83 |
84 | export const updateHeaders = (val: EventMap[Events.Headers]) => {
85 | eventer.publish(Events.Headers, val);
86 | };
87 | export const watchHeaders = (cb: EventFn) =>
88 | eventer.subscribe(Events.Headers, cb);
89 |
--------------------------------------------------------------------------------
/packages/server/src/utils/Logger.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { Routes } from '../api/types';
3 | import { urlMatches } from '../api/utils/routes';
4 | import { NileConfig } from '../types';
5 |
6 | import { Config } from './Config';
7 |
8 | const red = '\x1b[31m';
9 | const yellow = '\x1b[38;2;255;255;0m';
10 | const purple = '\x1b[38;2;200;160;255m';
11 | const orange = '\x1b[38;2;255;165;0m';
12 | const reset = '\x1b[0m';
13 |
14 | const baseLogger = (config: void | NileConfig, ...params: unknown[]) => ({
15 | info(message: string | unknown, meta?: Record) {
16 | if (config?.debug) {
17 | console.info(
18 | `${orange}[niledb]${reset}${purple}[DEBUG]${reset}${params.join(
19 | ''
20 | )}${reset} ${message}`,
21 | meta ? `${JSON.stringify(meta)}` : ''
22 | );
23 | }
24 | },
25 | debug(message: string | unknown, meta?: Record) {
26 | if (config?.debug) {
27 | console.debug(
28 | `${orange}[niledb]${reset}${purple}[DEBUG]${reset}${params.join(
29 | ''
30 | )}${reset} ${message}`,
31 | meta ? `${JSON.stringify(meta)}` : ''
32 | );
33 | }
34 | },
35 | warn(message: string | unknown, meta?: Record) {
36 | if (config?.debug) {
37 | console.warn(
38 | `${orange}[niledb]${reset}${yellow}[WARN]${reset}${params.join(
39 | ''
40 | )}${reset} ${message}`,
41 | meta ? JSON.stringify(meta) : ''
42 | );
43 | }
44 | },
45 | error(message: string | unknown, meta?: Record) {
46 | console.error(
47 | `${orange}[niledb]${reset}${red}[ERROR]${reset}${params.join(
48 | ''
49 | )}${red} ${message}`,
50 | meta ? meta : '',
51 | `${reset}`
52 | );
53 | },
54 | });
55 |
56 | export type LogReturn = {
57 | info(message: string | unknown, meta?: Record): void;
58 | debug(message: string | unknown, meta?: Record): void;
59 | warn(message: string | unknown, meta?: Record): void;
60 | error(message: string | unknown, meta?: Record): void;
61 | };
62 | export default function Logger(
63 | config?: Config | NileConfig,
64 | ...params: unknown[]
65 | ): LogReturn {
66 | const base = baseLogger(config, params);
67 | const info = config?.logger?.info ?? base.info;
68 | const debug = config?.logger?.debug ?? base.debug;
69 | const warn = config?.logger?.warn ?? base.warn;
70 | const error = config?.logger?.error ?? base.error;
71 | return { info, warn, error, debug };
72 | }
73 |
74 | export function matchesLog(configRoutes: Routes, request: Request): boolean {
75 | return urlMatches(request.url, configRoutes.LOG);
76 | }
77 |
--------------------------------------------------------------------------------
/packages/server/src/utils/ResponseError.ts:
--------------------------------------------------------------------------------
1 | export class ResponseError {
2 | response: Response;
3 | constructor(body?: BodyInit | null, init?: ResponseInit) {
4 | this.response = new Response(body, init);
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/server/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | // these two are a pass though
2 | export const X_NILE_TENANT = 'nile-tenant-id';
3 | export const X_NILE_USER_ID = 'nile-user-id';
4 | export const X_NILE_ORIGIN = 'nile-origin';
5 | // this one is not
6 | export const X_NILE_SECURECOOKIES = 'nile-secure-cookies';
7 |
--------------------------------------------------------------------------------
/packages/server/src/utils/fetch.ts:
--------------------------------------------------------------------------------
1 | import { Config } from './Config';
2 | import { X_NILE_TENANT, X_NILE_USER_ID } from './constants';
3 |
4 | function getTokenFromCookie(headers: Headers, cookieKey: void | string) {
5 | const cookie = headers.get('cookie')?.split('; ');
6 | const _cookies: Record = {};
7 | if (cookie) {
8 | for (const parts of cookie) {
9 | const cookieParts = parts.split('=');
10 | const _cookie = cookieParts.slice(1).join('=');
11 | const name = cookieParts[0];
12 | _cookies[name] = _cookie;
13 | }
14 | }
15 |
16 | if (cookie) {
17 | for (const parts of cookie) {
18 | const cookieParts = parts.split('=');
19 | const _cookie = cookieParts.slice(1).join('=');
20 | const name = cookieParts[0];
21 | _cookies[name] = _cookie;
22 | }
23 | }
24 | if (cookieKey) {
25 | return _cookies[cookieKey];
26 | }
27 | return null;
28 | }
29 | export function getTenantFromHttp(headers: Headers, config?: Config) {
30 | const cookieTenant = getTokenFromCookie(headers, X_NILE_TENANT);
31 | return cookieTenant ?? headers?.get(X_NILE_TENANT) ?? config?.tenantId;
32 | }
33 |
34 | // do we do this any more?
35 | export function getUserFromHttp(headers: Headers, config: Config) {
36 | return headers?.get(X_NILE_USER_ID) ?? config.userId;
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server/test/configKeys.ts:
--------------------------------------------------------------------------------
1 | export const apiConfig = [
2 | 'basePath',
3 | 'origin',
4 | 'callbackUrl',
5 | 'cookieKey',
6 | 'routePrefix',
7 | 'routes',
8 | 'secureCookies',
9 | ];
10 | export const baseConfig = [
11 | 'user',
12 | 'password',
13 | 'api',
14 | 'databaseId',
15 | 'db',
16 | 'headers',
17 | 'debug',
18 | 'databaseName',
19 | 'logger',
20 | ];
21 |
--------------------------------------------------------------------------------
/packages/server/test/fetch.mock.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../src/utils/Config';
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
4 | type Something = any;
5 |
6 | export class FakeResponse {
7 | [key: string]: Something;
8 | payload: object | string;
9 | headers?: Headers;
10 | constructor(payload: object | string, config?: RequestInit) {
11 | this.payload = payload;
12 | if (config) {
13 | this.headers = new Headers(config.headers);
14 | }
15 | let pload = payload;
16 | if (typeof payload === 'string') {
17 | pload = JSON.parse(payload);
18 | }
19 |
20 | Object.keys(pload).map((key) => {
21 | this[key] = (pload as Record)[key];
22 | });
23 | }
24 | json = async () => {
25 | if (typeof this.payload === 'string') {
26 | return JSON.parse(this.payload);
27 | }
28 | return this.payload;
29 | };
30 | text = async () => {
31 | return this.payload;
32 | };
33 | }
34 |
35 | export class FakeRequest {
36 | [key: string]: Something;
37 | constructor(url: string, config?: RequestInit) {
38 | this.payload = config?.body;
39 | }
40 | json = async () => {
41 | return JSON.parse(this.payload);
42 | };
43 | text = async () => {
44 | return this.payload;
45 | };
46 | }
47 |
48 | export const _fetch = (payload?: Record) =>
49 | (async (config: Config, path: string, opts?: RequestInit) => {
50 | return new FakeResponse({
51 | ...payload,
52 | config,
53 | path,
54 | opts,
55 | status: 200,
56 | });
57 | }) as unknown as typeof fetch;
58 |
--------------------------------------------------------------------------------
/packages/server/test/jest.setup.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | require('dotenv').config({ path: '../.env' });
3 |
--------------------------------------------------------------------------------
/packages/server/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "include": ["src/**/*"],
4 | "compilerOptions": {
5 | "baseUrl": "./src"
6 | },
7 | "exclude": ["**/*test.ts", "openapi"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
4 | "include": ["src/**/*", "test/**/*"],
5 | "compilerOptions": {
6 | "importHelpers": true,
7 | // output .d.ts declaration files for consumers
8 | "declaration": true,
9 | // output .js.map sourcemap files for consumers
10 | "sourceMap": true,
11 | // stricter type-checking for stronger correctness. Recommended by TS
12 | "strict": true,
13 | // linter checks for common issues
14 | "noImplicitReturns": true,
15 | "noFallthroughCasesInSwitch": true,
16 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
17 | "noUnusedLocals": false,
18 | // set to false for code generation
19 | "noUnusedParameters": false,
20 | // interop between ESM and CJS modules. Recommended by TS
21 | "esModuleInterop": true,
22 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
23 | "skipLibCheck": true,
24 | // error out if import and file system have a casing mismatch. Recommended by TS
25 | "forceConsistentCasingInFileNames": true,
26 | // `dts build` ignores this option, but it is commonly used when type-checking separately with `tsc`
27 | "noEmit": false,
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/packages/server/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig({
4 | entry: {
5 | index: 'src/index.ts',
6 | express: 'src/lib/express.ts',
7 | nitro: 'src/lib/nitro.ts',
8 | },
9 | format: ['esm', 'cjs'],
10 | outDir: 'dist',
11 | dts: true,
12 | splitting: false,
13 | sourcemap: true,
14 | clean: true,
15 | minify: false,
16 | treeshake: true,
17 | });
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "moduleResolution": "node",
5 | "target": "esnext",
6 | "lib": ["esnext", "dom"],
7 | "jsx": "preserve",
8 | "forceConsistentCasingInFileNames": true,
9 | "strict": true,
10 | "noEmit": true,
11 | "experimentalDecorators": true,
12 | "declaration": true,
13 | "declarationMap": true,
14 | "emitDeclarationOnly": false,
15 | "esModuleInterop": true,
16 | "baseUrl": ".",
17 | "allowSyntheticDefaultImports": true,
18 | "noErrorTruncation": false,
19 | "allowJs": true,
20 | "paths": {
21 | "@niledatabase/react": ["./packages/react/src"],
22 | "@niledatabase/react/*": ["./packages/react/src/*"],
23 | "@niledatabase/server": ["./packages/server/src"],
24 | "@niledatabase/server/*": ["./packages/server/src/*"],
25 | "@niledatabase/client": ["packages/client/src"]
26 |
27 | }
28 | },
29 | "exclude": ["**/node_modules", "**/.*/"],
30 | "types": ["node", "react", "react-is/next", "jest"]
31 | }
32 |
--------------------------------------------------------------------------------