├── .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 | Screen Shot 2024-09-18 at 9 20 04 AM 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 |
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( 37 | 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 |
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( 39 | 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 |
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( 37 | 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 |
11 |
Sign up
12 | 13 |
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 |
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( 34 | 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 | --------------------------------------------------------------------------------