├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── packages ├── server │ ├── .gitignore │ ├── src │ │ ├── db │ │ │ ├── index.ts │ │ │ ├── NileInstance.test.ts │ │ │ ├── db.test.ts │ │ │ └── PoolProxy.ts │ │ ├── utils │ │ │ ├── ResponseError.ts │ │ │ ├── constants.ts │ │ │ ├── qualifyDomain.ts │ │ │ └── fetch.ts │ │ ├── api │ │ │ ├── openapi │ │ │ │ └── swagger-doc.json │ │ │ ├── handlers │ │ │ │ └── index.ts │ │ │ ├── routes │ │ │ │ ├── auth │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── verify-request.ts │ │ │ │ │ ├── csrf.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── providers.ts │ │ │ │ │ ├── mfa.ts │ │ │ │ │ ├── signout.ts │ │ │ │ │ ├── verify-email.ts │ │ │ │ │ └── password-reset.ts │ │ │ │ ├── tenants │ │ │ │ │ ├── [tenantId] │ │ │ │ │ │ ├── invite │ │ │ │ │ │ │ ├── [inviteId] │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── DELETE.ts │ │ │ │ │ │ │ ├── GET.ts │ │ │ │ │ │ │ └── POST.ts │ │ │ │ │ │ ├── users │ │ │ │ │ │ │ ├── [userId] │ │ │ │ │ │ │ │ ├── DELETE.ts │ │ │ │ │ │ │ │ └── PUT.ts │ │ │ │ │ │ │ └── GET.ts │ │ │ │ │ │ ├── DELETE.ts │ │ │ │ │ │ ├── invites │ │ │ │ │ │ │ ├── GET.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── PUT.ts │ │ │ │ │ │ └── GET.ts │ │ │ │ │ ├── GET.ts │ │ │ │ │ └── POST.ts │ │ │ │ ├── signup │ │ │ │ │ ├── signup.test.ts │ │ │ │ │ └── index.tsx │ │ │ │ └── users │ │ │ │ │ ├── [userId] │ │ │ │ │ └── PUT.ts │ │ │ │ │ └── GET.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ ├── request-context.test.ts │ │ │ │ └── auth.ts │ │ ├── tenants │ │ │ └── types.ts │ │ ├── index.ts │ │ └── users │ │ │ └── types.ts │ ├── test │ │ ├── jest.setup.js │ │ ├── configKeys.ts │ │ └── fetch.mock.ts │ ├── openapitools.json │ ├── tsconfig.build.json │ ├── jest.config.js │ ├── tsup.config.ts │ ├── example.env │ ├── DEVELOPERS.md │ └── tsconfig.json ├── react │ ├── src │ │ ├── GoogleLoginButton │ │ │ ├── index.tsx │ │ │ ├── GoogleSSOButton.test.tsx │ │ │ └── GoogleLoginButton.stories.tsx │ │ ├── SignInForm │ │ │ ├── index.tsx │ │ │ ├── SignInForm.stories.tsx │ │ │ ├── SignInForm.tsx │ │ │ ├── types.ts │ │ │ ├── SignInForm.test.tsx │ │ │ ├── Form.tsx │ │ │ └── hooks.tsx │ │ ├── SignUpForm │ │ │ ├── index.tsx │ │ │ ├── SignUpForm.tsx │ │ │ ├── types.ts │ │ │ ├── SignUpForm.test.tsx │ │ │ ├── hooks.tsx │ │ │ └── Form.tsx │ │ ├── EmailSignIn │ │ │ ├── index.tsx │ │ │ ├── types.ts │ │ │ ├── hooks.ts │ │ │ ├── EmailSignInButton.stories.tsx │ │ │ └── Form.tsx │ │ ├── resetPassword │ │ │ ├── index.tsx │ │ │ ├── ForgotPassword │ │ │ │ ├── ForgotPassword.stories.tsx │ │ │ │ └── index.tsx │ │ │ ├── PasswordResetRequestForm │ │ │ │ ├── index.tsx │ │ │ │ ├── PasswordResetRequestForm.stories.tsx │ │ │ │ └── Form.tsx │ │ │ └── types.ts │ │ ├── SignOutButton │ │ │ ├── SignOutButton.test.tsx │ │ │ ├── SignOutButton.stories.tsx │ │ │ └── index.tsx │ │ ├── XSignInButton │ │ │ ├── XSignInButton.test.tsx │ │ │ ├── XSignInButton.stories.tsx │ │ │ └── index.tsx │ │ ├── AzureSignInButton │ │ │ ├── AzureSignInButton.test.tsx │ │ │ └── AzureSignInButton.stories.tsx │ │ ├── GitHubSignInButton │ │ │ ├── GitHubSignInButton.test.tsx │ │ │ └── GitHubSignInButton.stories.tsx │ │ ├── SlackSignInButton │ │ │ ├── SlackSignInButton.test.tsx │ │ │ └── SlackSignInButton.stories.tsx │ │ ├── DiscordSignInButton │ │ │ ├── DiscordSignInButton.test.tsx │ │ │ └── DiscordSignInButton.stories.tsx │ │ ├── HubSpotSignInButton │ │ │ ├── HubSpotSignInButton.test.tsx │ │ │ └── HubSpotSignInButton.stories.tsx │ │ ├── MultiFactor │ │ │ ├── index.tsx │ │ │ ├── ChallengeContent.tsx │ │ │ ├── SetupEmail.tsx │ │ │ ├── SetupAuthenticator.stories.tsx │ │ │ ├── ChallengeContent.stories.tsx │ │ │ ├── SetupAuthenticator.tsx │ │ │ └── types.ts │ │ ├── LinkedInSignInButton │ │ │ ├── LinkedInSignInButton.test.tsx │ │ │ └── LinkedInSignInButton.stories.tsx │ │ ├── OktaSignInButton │ │ │ ├── OktaSignInButton.test.tsx │ │ │ └── OktaSignInButton.stories.tsx │ │ ├── types.ts │ │ ├── TenantSelector │ │ │ ├── types.ts │ │ │ └── TenantSelector.stories.tsx │ │ ├── UserInfo │ │ │ ├── UserInfo.test.tsx │ │ │ ├── UserInfo.stories.tsx │ │ │ └── hooks.tsx │ │ ├── SignedOut │ │ │ └── index.tsx │ │ └── SignedIn │ │ │ └── index.tsx │ ├── .gitignore │ ├── jest.config.js │ ├── tsconfig.build.json │ ├── dts.config.js │ ├── .babelrc │ ├── tsconfig.json │ ├── tsup.config.js │ ├── .storybook │ │ ├── preview.ts │ │ └── main.ts │ ├── lib │ │ └── queryClient.ts │ ├── test │ │ ├── matchMedia.mock │ │ └── fetch.mock.ts │ ├── components.json │ ├── components │ │ └── ui │ │ │ ├── label.tsx │ │ │ ├── input.tsx │ │ │ └── tooltip.tsx │ └── LICENSE ├── nitro │ ├── README.md │ ├── tsup.config.js │ └── package.json ├── express │ ├── README.md │ ├── jest.config.js │ ├── tsup.config.js │ └── package.json ├── nextjs │ ├── README.md │ ├── tsup.config.js │ ├── package.json │ ├── tsconfig.json │ └── src │ │ └── index.ts └── client │ ├── .gitignore │ ├── jest.config.js │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── src │ ├── status.ts │ ├── index.ts │ └── broadcast.ts │ ├── .babelrc │ ├── tsup.config.js │ └── package.json ├── commitlint.config.js ├── apps └── nextjs-kitchensink │ ├── .eslintignore │ ├── app │ ├── favicon.ico │ ├── google-manual │ │ ├── csrf │ │ │ └── route.ts │ │ ├── providers │ │ │ └── route.ts │ │ ├── sso │ │ │ └── route.ts │ │ ├── [...callback] │ │ │ └── route.ts │ │ ├── localizedNile.ts │ │ └── loginButton.tsx │ ├── api │ │ └── [...nile] │ │ │ ├── route.ts │ │ │ └── nile.ts │ ├── mfa │ │ └── MfaSignIn.tsx │ ├── forgot-password │ │ ├── resetFormRequest.tsx │ │ ├── serverForgotPassword.tsx │ │ └── forgotForm.tsx │ ├── google │ │ ├── login │ │ │ └── page.tsx │ │ └── selectTodos.ts │ ├── react │ │ ├── standalone-hooks │ │ │ └── page.tsx │ │ ├── client-side │ │ │ └── page.tsx │ │ └── page.tsx │ ├── verify-email │ │ ├── UnVerifyEmail.tsx │ │ ├── VerifyEmail.tsx │ │ └── VerifyEmailForm.tsx │ ├── invites │ │ ├── InvitesIndex.tsx │ │ ├── EnsureSignedIn.tsx │ │ ├── user-switcher │ │ │ └── page.tsx │ │ ├── handle-invite │ │ │ └── route.ts │ │ ├── sign-up │ │ │ └── page.tsx │ │ ├── InviteUserToTenant.tsx │ │ ├── MembersTable.tsx │ │ └── InvitesTable.tsx │ ├── interactive-sign-in │ │ ├── page.tsx │ │ └── SignInSignOut.tsx │ ├── dashboard │ │ └── page.tsx │ ├── tenant-list │ │ ├── page.tsx │ │ └── ListTenants.tsx │ ├── reset-password │ │ ├── client.tsx │ │ ├── server.tsx │ │ ├── reset │ │ │ └── page.tsx │ │ └── resetForm.tsx │ ├── fancySignIn │ │ └── page.tsx │ ├── customLogin │ │ └── loginAction.ts │ ├── allClientSide │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx │ ├── postcss.config.mjs │ ├── public │ ├── vercel.svg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ └── next.svg │ ├── next.config.ts │ ├── lib │ └── utils.ts │ ├── components │ └── ui │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── appSidebar │ │ └── index.tsx │ │ └── input.tsx │ ├── components.json │ ├── hooks │ └── use-mobile.ts │ ├── tsconfig.json │ ├── .gitignore │ ├── scripts │ ├── fix-resolutions.mjs │ └── linker.sh │ └── README.md ├── .yarnrc.yml ├── .yarn └── sdks │ ├── integrations.yml │ ├── prettier │ ├── package.json │ ├── index.js │ └── bin-prettier.js │ ├── typescript │ ├── package.json │ ├── lib │ │ ├── typescript.js │ │ └── tsc.js │ └── bin │ │ ├── tsc │ │ └── tsserver │ └── eslint │ ├── package.json │ ├── lib │ ├── api.js │ └── unsupported-api.js │ └── bin │ └── eslint.js ├── .eslintignore ├── .vscode ├── extensions.json └── settings.json ├── .editorconfig ├── lerna.json ├── .gitignore ├── .github └── workflows │ └── test.yaml ├── tsconfig.json └── LICENSE /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /packages/server/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .env* 3 | public -------------------------------------------------------------------------------- /packages/server/src/db/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './DBManager'; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/.eslintignore: -------------------------------------------------------------------------------- 1 | components/ui/form.tsx 2 | components/snippets/index.ts 3 | -------------------------------------------------------------------------------- /packages/react/src/GoogleLoginButton/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './GoogleLoginButton'; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /packages/nitro/README.md: -------------------------------------------------------------------------------- 1 | # nitro 2 | 3 | An extension for nitro that allows the Nile SDK to work with it seamlessly. 4 | -------------------------------------------------------------------------------- /packages/express/README.md: -------------------------------------------------------------------------------- 1 | # express 2 | 3 | An extension for express that allows the Nile SDK to work with it seamlessly. 4 | -------------------------------------------------------------------------------- /packages/nextjs/README.md: -------------------------------------------------------------------------------- 1 | # nextjs 2 | 3 | An extension for NextJS that allows the Nile SDK to work with it seamlessly. 4 | -------------------------------------------------------------------------------- /packages/react/src/SignInForm/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './SignInForm'; 2 | export { useSignIn } from './hooks'; 3 | -------------------------------------------------------------------------------- /packages/react/src/SignUpForm/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './SignUpForm'; 2 | export { useSignUp } from './hooks'; 3 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niledatabase/nile-js/HEAD/apps/nextjs-kitchensink/app/favicon.ico -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /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/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/server/test/jest.setup.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | require('dotenv').config({ path: '../.env' }); 3 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.9.2.cjs 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib/nile/src/generated 3 | node_modules 4 | !.storybook 5 | storybook-static 6 | .log 7 | .yarn/* 8 | apps/nextjs-boilerplate/components/ui/form.tsx -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/client/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'jsdom', 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'jsdom', 5 | }; 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/google-manual/csrf/route.ts: -------------------------------------------------------------------------------- 1 | import { nile } from '../localizedNile'; 2 | 3 | export async function GET() { 4 | return await nile.auth.getCsrf(true); 5 | } 6 | -------------------------------------------------------------------------------- /packages/express/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 | }; 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/react/src/EmailSignIn/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './Form'; 2 | export { default as EmailSignInButton } from './EmailSignInButton'; 3 | export { useEmailSignIn } from './hooks'; 4 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/google-manual/providers/route.ts: -------------------------------------------------------------------------------- 1 | import { nile } from '../localizedNile'; 2 | 3 | export async function GET() { 4 | return await nile.auth.listProviders(true); 5 | } 6 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /packages/client/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": ["src/**/*"], 4 | "compilerOptions": { 5 | "baseUrl": "./src", 6 | "declaration": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/react/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": ["src/**/*"], 4 | "compilerOptions": { 5 | "baseUrl": "./src", 6 | "declaration": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /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/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": ["src/**/*"], 4 | "compilerOptions": { 5 | "baseUrl": "./src" 6 | }, 7 | "exclude": ["**/*test.ts", "openapi"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/api/[...nile]/route.ts: -------------------------------------------------------------------------------- 1 | import { Handlers } from '@niledatabase/nextjs'; 2 | 3 | import { handlers } from './nile'; 4 | 5 | export const { POST, GET, DELETE, PUT } = handlers as Handlers; 6 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/mfa/MfaSignIn.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { SignInForm } from '@niledatabase/react'; 3 | 4 | export default function MfaSignIn() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /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/express/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | minify: true, 5 | target: 'es2022', 6 | sourcemap: true, 7 | dts: true, 8 | format: ['esm', 'cjs'], 9 | }); 10 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/forgot-password/resetFormRequest.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PasswordResetRequestForm } from '@niledatabase/react'; 4 | 5 | export default function ResetFormRequest() { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/google-manual/sso/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from 'next/server'; 2 | 3 | import { nile } from '../localizedNile'; 4 | 5 | export async function POST(req: NextRequest) { 6 | return await nile.auth.signIn('google', req); 7 | } 8 | -------------------------------------------------------------------------------- /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/nitro/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 | }); 11 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/api/[...nile]/nile.ts: -------------------------------------------------------------------------------- 1 | import { Nile } from '@niledatabase/server'; 2 | import { nextJs } from '@niledatabase/nextjs'; 3 | 4 | export const nile = Nile({ 5 | debug: true, 6 | extensions: [nextJs], 7 | }); 8 | export const { handlers } = nile; 9 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/google-manual/[...callback]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from 'next/server'; 2 | 3 | import { nile } from '../localizedNile'; 4 | 5 | export async function GET(req: NextRequest) { 6 | return await nile.auth.callback('google', req); 7 | } 8 | -------------------------------------------------------------------------------- /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/utils/constants.ts: -------------------------------------------------------------------------------- 1 | // these two are a pass though 2 | export const TENANT_COOKIE = 'nile.tenant-id'; 3 | export const USER_COOKIE = 'nile.user-id'; 4 | export const HEADER_ORIGIN = 'nile-origin'; 5 | // this one is not 6 | export const HEADER_SECURE_COOKIES = 'nile-secure-cookies'; 7 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/google/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { Google } from '@niledatabase/react'; 2 | 3 | export default async function Home() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /packages/nextjs/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | minify: true, 5 | target: 'es2022', 6 | sourcemap: true, 7 | dts: true, 8 | format: ['esm', 'cjs'], 9 | external: ['next', 'next/headers', 'next/navigation', 'next/server'], 10 | }); 11 | -------------------------------------------------------------------------------- /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/server/src/tenants/types.ts: -------------------------------------------------------------------------------- 1 | export type Tenant = { 2 | id: string; 3 | name: string; 4 | }; 5 | 6 | export type Invite = { 7 | id: string; 8 | tenant_id: string; 9 | token: string; 10 | identifier: string; 11 | roles: null | string; 12 | created_by: string; 13 | expires: Date; 14 | }; 15 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/server/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: { 5 | index: 'src/index.ts', 6 | }, 7 | format: ['esm', 'cjs'], 8 | outDir: 'dist', 9 | dts: true, 10 | splitting: false, 11 | sourcemap: true, 12 | clean: true, 13 | minify: false, 14 | treeshake: true, 15 | }); 16 | -------------------------------------------------------------------------------- /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/.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/.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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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( 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/invites/InvitesIndex.tsx: -------------------------------------------------------------------------------- 1 | import { User } from '@niledatabase/server'; 2 | 3 | import { nile } from '../api/[...nile]/nile'; 4 | 5 | import EnsureSignedIn from './EnsureSignedIn'; 6 | import TenantsAndTables from './TenantsAndTables'; 7 | 8 | export default async function InvitesIndex() { 9 | const me = await nile.users.getSelf(); 10 | 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /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 { 8 | parseCSRF, 9 | parseCallback, 10 | parseToken, 11 | parseResetToken, 12 | parseTenantId, 13 | } from './auth'; 14 | export { 15 | TENANT_COOKIE, 16 | USER_COOKIE, 17 | HEADER_ORIGIN, 18 | HEADER_SECURE_COOKIES, 19 | } from './utils/constants'; 20 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/invites/EnsureSignedIn.tsx: -------------------------------------------------------------------------------- 1 | import { Google } from '@niledatabase/react'; 2 | import { User } from '@niledatabase/server'; 3 | 4 | export default async function EnsureSignedIn({ 5 | children, 6 | me, 7 | }: { 8 | children: React.ReactElement; 9 | me: User | Response; 10 | }) { 11 | if (me instanceof Response) { 12 | return ( 13 |
14 |
15 | You need to log in before use/see your invites. 16 |
17 | 18 |
19 | ); 20 | } 21 | return children; 22 | } 23 | -------------------------------------------------------------------------------- /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.apiUrl)[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/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.db, config.logger, 'someId'); 17 | watchEvictPool((id) => { 18 | expect(id).toEqual('someId'); 19 | done(); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/react/src/TenantSelector/types.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query'; 2 | 3 | import { Tenant } from '../../../server/src/tenants/types'; 4 | import { ComponentFetchProps } from '../../lib/utils'; 5 | 6 | export type HookProps = ComponentFetchProps & { 7 | client?: QueryClient; 8 | fetchUrl?: string; 9 | tenants?: Tenant[]; 10 | onError?: (e: Error) => void; 11 | onTenantChange?: (tenant: string) => void; 12 | activeTenant?: string | null | undefined; 13 | }; 14 | 15 | export type ComponentProps = HookProps & { 16 | useCookie?: boolean; 17 | className?: string; 18 | emptyText?: string; 19 | buttonText?: string; 20 | }; 21 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/react/client-side/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SessionProvider, UserInfo, useSession } from '@niledatabase/react'; 4 | 5 | export default function ClientSideReact() { 6 | return ( 7 | 8 |
9 | 10 | 11 |
12 |
13 | ); 14 | } 15 | 16 | function SessionDebugger() { 17 | const session = useSession(); 18 | return ( 19 |
20 | 21 |
{JSON.stringify(session, null, 2)}
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /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.apiUrl)[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/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/UserInfo/UserInfo.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | 4 | import Profile from './index'; 5 | 6 | describe('Profile info', () => { 7 | it('renders ', () => { 8 | render( 9 | 19 | ); 20 | screen.getByText('SpongeBob SquarePants'); 21 | screen.getByText('fake@fake.com'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState( 7 | undefined 8 | ); 9 | 10 | React.useEffect(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 14 | }; 15 | mql.addEventListener('change', onChange); 16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 17 | return () => mql.removeEventListener('change', onChange); 18 | }, []); 19 | 20 | return !!isMobile; 21 | } 22 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/google-manual/localizedNile.ts: -------------------------------------------------------------------------------- 1 | import { nextJs } from '@niledatabase/nextjs'; 2 | import { Server } from '@niledatabase/server'; 3 | 4 | // make a brand new server for nile since this is a special case where we are doing everything. 5 | // Normally, just handle routes as routes, but anything is possible 6 | export const nile = new Server({ 7 | debug: true, 8 | routePrefix: '/google-manual', 9 | // we also need to tell nile-auth about the origin of our FE, so it goes to the right place. 10 | origin: process.env.VERCEL_PROJECT_PRODUCTION_URL 11 | ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` 12 | : 'http://localhost:3000', 13 | extensions: [nextJs], 14 | }); 15 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/interactive-sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { revalidatePath } from 'next/cache'; 2 | 3 | import SignInSignOut from './SignInSignOut'; 4 | 5 | import Code from '@/components/ui/code'; 6 | 7 | export default async function InteractiveSingIn() { 8 | return ( 9 |
10 | This page lets you sign up or sign as random users. Sign up automatically 11 | creates the user and signs that user in. 12 | 13 | 14 |
15 | ); 16 | } 17 | async function revalidate() { 18 | 'use server'; 19 | revalidatePath('/interactive-sign-in'); 20 | } 21 | -------------------------------------------------------------------------------- /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 | INVITES: string; 26 | INVITE: string; 27 | MULTI_FACTOR: string; 28 | }; 29 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/invites/user-switcher/page.tsx: -------------------------------------------------------------------------------- 1 | import { Google, SignInForm } from '@niledatabase/react'; 2 | 3 | export default async function InvitesUserSwitcher() { 4 | return ( 5 |
6 | The signed in user does not match the user that the invite was sent to. 7 | It's fine, its not a security problem, but your current user 8 | isn't going to have access to the tenant, since they were not the 9 | ones invited. You'll need to switch users in order to do that, so 10 | sign in with the invited user. 11 |
12 | 13 |
14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | ## raw file outputs for showing the code 44 | components/snippets -------------------------------------------------------------------------------- /packages/react/src/UserInfo/UserInfo.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import UserInfo from '.'; 5 | 6 | const meta: Meta = { 7 | title: 'User information', 8 | component: UserInfo, 9 | }; 10 | 11 | export default meta; 12 | 13 | export function UserProfile() { 14 | return ( 15 |
16 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { nile } from '../api/[...nile]/nile'; 2 | 3 | export default async function Dashboard() { 4 | const self1 = await nile.users.getSelf(); 5 | const [tenants, me] = await nile.withContext(() => 6 | Promise.all([nile.tenants.list(), nile.users.getSelf()]) 7 | ); 8 | return ( 9 |
10 |
no context
11 |
12 |         {JSON.stringify(self1, null, 2)}
13 |       
14 |
context stuff
15 |
16 |         {JSON.stringify(me, null, 2)}
17 |       
18 |
Tenants:
19 |
20 |         {JSON.stringify(tenants, null, 2)}
21 |       
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/server/src/db/db.test.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../utils/Config'; 2 | 3 | import NileDB from './index'; 4 | 5 | const properties = [ 6 | 'connections', 7 | 'clear', 8 | 'cleared', 9 | 'getConnection', 10 | 'poolWatcher', 11 | 'poolWatcherFn', 12 | ]; 13 | describe('db', () => { 14 | it('has expected properties', () => { 15 | const db = new NileDB( 16 | new Config({ 17 | databaseId: 'databaseId', 18 | databaseName: 'databaseName', 19 | user: 'username', 20 | password: 'password', 21 | debug: false, 22 | db: { 23 | port: 4433, 24 | }, 25 | tenantId: null, 26 | userId: null, 27 | }) 28 | ); 29 | expect(Object.keys(db).sort()).toEqual(properties.sort()); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/tenant-list/page.tsx: -------------------------------------------------------------------------------- 1 | import ListTenants from './ListTenants'; 2 | 3 | import Code from '@/components/ui/code'; 4 | 5 | export default async function TenantList() { 6 | return ( 7 |
8 |
Tenant list
9 |
10 | A basic page to show a list of tenants. In some cases, you may want to 11 | make multiple requests through the nile sdk. To ensure the context is 12 | correct for every request, wrap the call in `nile.withContext`. This is 13 | especially important if you are not leveraging cookies in your 14 | application. 15 |
16 | 17 | 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/react/page.tsx: -------------------------------------------------------------------------------- 1 | import { SessionProvider } from '@niledatabase/react'; 2 | import Link from 'next/link'; 3 | 4 | import { nile } from '../api/[...nile]/nile'; 5 | 6 | import { Button } from '@/components/ui/button'; 7 | 8 | export default async function Providers() { 9 | const session = await nile.auth.getSession(); 10 | 11 | return ( 12 | 13 |
14 | 17 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/scripts/fix-resolutions.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | 7 | const rootDir = path.resolve(__dirname, '../../../'); // monorepo root 8 | const appDir = path.resolve(rootDir, 'apps/nextjs-kitchensink'); // path to your app 9 | const pkgPath = path.join(appDir, 'package.json'); 10 | const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); 11 | 12 | const deps = pkgJson.dependencies || {}; 13 | 14 | for (const [key] of Object.entries(deps)) { 15 | if (key?.startsWith('@niledatabase')) { 16 | deps[key] = 'workspace:*'; 17 | } 18 | } 19 | 20 | fs.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2)); 21 | console.log('✅ Rewrote workspace versions in', pkgPath); 22 | -------------------------------------------------------------------------------- /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/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/src/MultiFactor/ChallengeContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { MultiFactorVerify } from './MultiFactorVerify'; 4 | import { MfaSetup } from './types'; 5 | 6 | export function ChallengeContent({ 7 | payload, 8 | message, 9 | isEnrolled, 10 | onSuccess, 11 | }: { 12 | payload: MfaSetup; 13 | message?: string; 14 | isEnrolled?: boolean; 15 | onSuccess?: (scope: 'setup' | 'challenge') => void; 16 | }) { 17 | return ( 18 |
19 |

{message}

20 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Separator({ 9 | className, 10 | orientation = 'horizontal', 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ); 26 | } 27 | 28 | export { Separator }; 29 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/google-manual/loginButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { signIn } from '@niledatabase/client'; 3 | 4 | import { Button } from '@/components/ui/button'; 5 | 6 | export default function GoogleManualButton() { 7 | return ( 8 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/reset-password/client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { PasswordResetForm } from '@niledatabase/react'; 3 | import { useState } from 'react'; 4 | 5 | // Need the email in order to reset 6 | export default function ResetPasswordClientSide({ email }: { email: string }) { 7 | const [success, setSuccess] = useState(false); 8 | return ( 9 |
10 | { 13 | setSuccess(true); 14 | setTimeout(() => { 15 | window.location.reload(); 16 | }, 3000); 17 | }} 18 | /> 19 |
24 | Password updated 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /packages/react/src/resetPassword/ForgotPassword/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | import React from 'react'; 4 | 5 | import { Props } from '../types'; 6 | import { cn } from '../../../lib/utils'; 7 | 8 | import PasswordResetForm from './Form'; 9 | 10 | const queryClient = new QueryClient(); 11 | 12 | export default function ResetPasswordForm(params: Props) { 13 | const { client, ...props } = params; 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | function ResetForm({ className, ...props }: Props) { 22 | return ( 23 |
24 |

Reset password

25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/invites/handle-invite/route.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@niledatabase/server'; 2 | import { redirect } from 'next/navigation'; 3 | import { NextRequest } from 'next/server'; 4 | 5 | import { nile } from '@/app/api/[...nile]/nile'; 6 | 7 | export async function GET(req: NextRequest) { 8 | // you may have already been logged in, so we need to check 9 | const me = await nile.users.getSelf(); 10 | if (me instanceof Response) { 11 | // its a 404/401, which means the user needs to sign up before they can do any thing 12 | return redirect('/invites/sign-up'); 13 | } 14 | // we need to be sure the identifier matches the user. If not, we need to give them the option to switch users. 15 | const email = req.nextUrl.searchParams.get('email'); 16 | if (email !== me.email) { 17 | return redirect('/invites/user-switcher'); 18 | } 19 | return redirect('/invites'); 20 | } 21 | -------------------------------------------------------------------------------- /packages/react/src/MultiFactor/SetupEmail.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { EmailSetup } from './types'; 4 | import { MultiFactorVerify } from './MultiFactorVerify'; 5 | 6 | export function SetupEmail({ 7 | setup, 8 | onSuccess, 9 | }: { 10 | setup: EmailSetup; 11 | onSuccess: (scope: 'setup' | 'challenge') => void; 12 | }) { 13 | return ( 14 |
15 |

16 | {setup.maskedEmail 17 | ? `We sent a 6-digit code to ${setup.maskedEmail}. Enter it below to finish.` 18 | : 'Check your email for a 6-digit code and enter it below to finish.'} 19 |

20 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /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/server/src/api/routes/tenants/[tenantId]/invite/[inviteId]/index.ts: -------------------------------------------------------------------------------- 1 | import { urlMatches } from '../../../../../utils/routes'; 2 | import { Routes } from '../../../../../types'; 3 | import { Config } from '../../../../../../utils/Config'; 4 | 5 | import { DELETE } from './DELETE'; 6 | 7 | const key = 'INVITE'; 8 | export default async function route(request: Request, config: Config) { 9 | switch (request.method) { 10 | case 'DELETE': 11 | return await DELETE(config, { request }); 12 | default: 13 | return new Response('method not allowed', { status: 405 }); 14 | } 15 | } 16 | export function matches(configRoutes: Routes, request: Request): boolean { 17 | const url = new URL(request.url); 18 | const [inviteId, , tenantId] = url.pathname.split('/').reverse(); 19 | const route = configRoutes[key] 20 | .replace('{tenantId}', tenantId) 21 | .replace('{inviteId}', inviteId); 22 | return urlMatches(request.url, route); 23 | } 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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", "express"] 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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/invites/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Google, 3 | PasswordResetRequestForm, 4 | SignInForm, 5 | } from '@niledatabase/react'; 6 | 7 | export default async function InvitesSignUpPage() { 8 | return ( 9 |
10 | Looks like you're new around these parts. Someone must like you. 11 | Before you can see all the super cool stuff they have in store, 12 | you'll need an account. The easiest way is to use google. 13 |
14 | 15 |
16 |
17 | You may be some random email address we don't know about, so you 18 | can use that too, but you'll probably need to set a password. 19 | 20 |
21 |
22 | If you've done this all before, sign back in. 23 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/nitro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@niledatabase/nitro", 3 | "version": "5.2.0-alpha.0", 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 | "prettier": { 18 | "printWidth": 80, 19 | "semi": true, 20 | "singleQuote": true, 21 | "trailingComma": "es5" 22 | }, 23 | "scripts": { 24 | "build": "tsup src/index.ts" 25 | }, 26 | "author": "jrea", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/niledatabase/nile-js.git", 30 | "directory": "packages/nitro" 31 | }, 32 | "publishConfig": { 33 | "access": "public" 34 | }, 35 | "peerDependencies": { 36 | "@niledatabase/server": ">=5.0.0-alpha", 37 | "h3": "^1" 38 | }, 39 | "devDependencies": { 40 | "h3": "^1", 41 | "tsup": "^8.5.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/react/src/TenantSelector/TenantSelector.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import TenantSelector from '.'; 5 | 6 | const meta: Meta = { 7 | title: 'Tenant selector', 8 | component: TenantSelector, 9 | }; 10 | 11 | export default meta; 12 | 13 | export function SelectTenant() { 14 | document.cookie = 'nile.tenant_id=1'; 15 | const tenants = [ 16 | { id: '1', name: 'Tenant 1' }, 17 | { id: '2', name: 'Tenant 2' }, 18 | ]; 19 | return ( 20 |
21 | 22 |
23 | ); 24 | } 25 | export function EmptyState() { 26 | document.cookie = 'nile.tenant_id='; 27 | const tenants = [ 28 | { id: '1', name: 'Tenant 1' }, 29 | { id: '2', name: 'Tenant 2' }, 30 | ]; 31 | return ( 32 |
33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/fancySignIn/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | SignOutButton, 3 | SignUpForm, 4 | SignedIn, 5 | SignedOut, 6 | TenantSelector, 7 | UserInfo, 8 | } from '@niledatabase/react'; 9 | import { Tenant, User } from '@niledatabase/server'; 10 | import { NileSession } from '@niledatabase/client'; 11 | 12 | import { nile } from '../api/[...nile]/nile'; 13 | 14 | export default async function SignUpPage() { 15 | const [session, me, tenants] = await Promise.all([ 16 | nile.auth.getSession(), 17 | nile.users.getSelf(), 18 | nile.tenants.list(), 19 | ]); 20 | return ( 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /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 | import { ctx } from '../../utils/request-context'; 6 | 7 | export default async function route(req: Request, config: Config) { 8 | return request( 9 | proxyRoutes(config.apiUrl).CSRF, 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.CSRF); 19 | } 20 | 21 | export async function fetchCsrf(config: Config): Promise { 22 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.CSRF}`; 23 | const { headers } = ctx.get(); 24 | const req = new Request(clientUrl, { 25 | method: 'GET', 26 | headers, 27 | }); 28 | 29 | return (await config.handlers.GET(req)) as Response; 30 | } 31 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/scripts/linker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Usage: ./link-package.sh 4 | # Example: ./link-package.sh @myorg/lib-a ../monorepo/packages/lib-a 5 | 6 | set -e 7 | 8 | PACKAGE_NAME="$1" 9 | PACKAGE_ROOT_PATH="$2" 10 | NODE_MODULES_DIR="./node_modules" 11 | 12 | if [[ -z "$PACKAGE_NAME" || -z "$PACKAGE_ROOT_PATH" ]]; then 13 | echo "Usage: $0 " 14 | exit 1 15 | fi 16 | 17 | # Resolve full, absolute paths 18 | PACKAGE_ROOT_PATH="$(realpath "$PACKAGE_ROOT_PATH")" 19 | PACKAGE_DIR="$NODE_MODULES_DIR/$PACKAGE_NAME" 20 | 21 | echo "🔗 Linking $PACKAGE_NAME → $PACKAGE_ROOT_PATH" 22 | echo "📁 Target: $PACKAGE_DIR" 23 | 24 | # Create the parent folder (e.g. node_modules/@myorg) 25 | mkdir -p "$(dirname "$PACKAGE_DIR")" 26 | 27 | # Remove existing link or folder 28 | if [ -e "$PACKAGE_DIR" ] || [ -L "$PACKAGE_DIR" ]; then 29 | rm -rf "$PACKAGE_DIR" 30 | fi 31 | 32 | # Create the symlink 33 | ln -s "$PACKAGE_ROOT_PATH" "$PACKAGE_DIR" 34 | 35 | echo "✅ Linked $PACKAGE_NAME to $PACKAGE_DIR" 36 | -------------------------------------------------------------------------------- /packages/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@niledatabase/nextjs", 3 | "version": "5.2.0-alpha.0", 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 | "prettier": { 18 | "printWidth": 80, 19 | "semi": true, 20 | "singleQuote": true, 21 | "trailingComma": "es5" 22 | }, 23 | "sideEffects": false, 24 | "scripts": { 25 | "build": "tsup src/index.ts" 26 | }, 27 | "author": "jrea", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/niledatabase/nile-js.git", 31 | "directory": "packages/nextjs" 32 | }, 33 | "publishConfig": { 34 | "access": "public" 35 | }, 36 | "peerDependencies": { 37 | "@niledatabase/server": ">=5.0.0-alpha", 38 | "next": "^15" 39 | }, 40 | "devDependencies": { 41 | "next": "^15.3.3", 42 | "tsup": "^8.5.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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 | 29 | export { default as handleMfa, matches as matchesMfa } from './mfa'; 30 | -------------------------------------------------------------------------------- /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 | import { ctx } from '../../utils/request-context'; 6 | 7 | export default async function route(req: Request, config: Config) { 8 | return request( 9 | proxyRoutes(config.apiUrl).SESSION, 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.SESSION); 19 | } 20 | 21 | export async function fetchSession(config: Config): Promise { 22 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.SESSION}`; 23 | const { headers } = ctx.get(); 24 | const req = new Request(clientUrl, { 25 | method: 'GET', 26 | headers, 27 | }); 28 | 29 | return (await config.handlers.GET(req)) as Response; 30 | } 31 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/customLogin/loginAction.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | import { User } from '@niledatabase/server'; 3 | 4 | import { nile } from '../api/[...nile]/nile'; 5 | 6 | export type LoginResponse = { 7 | message?: string; 8 | user?: User; 9 | }; 10 | export async function login( 11 | prevState: LoginResponse | null, 12 | formData: FormData 13 | ): Promise { 14 | const email = formData.get('email'); 15 | const password = formData.get('password'); 16 | if ( 17 | typeof email !== 'string' || 18 | typeof password !== 'string' || 19 | !email || 20 | !password 21 | ) { 22 | throw new Error('Email and password are required'); 23 | } 24 | 25 | await nile.auth.signIn( 26 | 'credentials', 27 | { 28 | email, 29 | password, 30 | }, 31 | true 32 | ); 33 | 34 | const user = await nile.users.getSelf(); 35 | 36 | if (!user || user instanceof Response) { 37 | return { 38 | message: "User not found. It's probably because you're not authenticated", 39 | }; 40 | } 41 | 42 | return { user }; 43 | } 44 | -------------------------------------------------------------------------------- /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 | import { ctx } from '../../utils/request-context'; 6 | 7 | export default async function route(req: Request, config: Config) { 8 | return request( 9 | proxyRoutes(config.apiUrl).PROVIDERS, 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.PROVIDERS); 19 | } 20 | 21 | export async function fetchProviders(config: Config): Promise { 22 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.PROVIDERS}`; 23 | const { headers } = ctx.get(); 24 | const req = new Request(clientUrl, { 25 | method: 'GET', 26 | headers, 27 | }); 28 | 29 | return (await config.handlers.GET(req)) as Response; 30 | } 31 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/allClientSide/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { NileSession, getSession, signIn } from '@niledatabase/client'; 5 | 6 | import { Button } from '@/components/ui/button'; 7 | 8 | export default function AllClient() { 9 | const [session, setSession] = React.useState(null); 10 | 11 | React.useEffect(() => { 12 | let ignore = false; 13 | getSession() 14 | .then((result) => { 15 | if (!ignore) { 16 | setSession(result ?? null); 17 | } 18 | }) 19 | .catch(() => { 20 | if (!ignore) { 21 | setSession(null); 22 | } 23 | }); 24 | return () => { 25 | ignore = true; 26 | }; 27 | }, []); 28 | 29 | return ( 30 |
31 | Session data: {session ? JSON.stringify(session) : 'No active session'} 32 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/forgot-password/serverForgotPassword.tsx: -------------------------------------------------------------------------------- 1 | import { nile } from '../api/[...nile]/nile'; 2 | 3 | import { ForgotPasswordForm } from './forgotForm'; 4 | 5 | interface ResetResponse { 6 | ok: boolean; 7 | message?: string; 8 | } 9 | 10 | export default async function ForgotPasswordServer() { 11 | async function resetPassword( 12 | _: unknown, 13 | formData: FormData 14 | ): Promise { 15 | 'use server'; 16 | 17 | const email = formData.get('email') as string; 18 | const response = await nile.auth.forgotPassword({ 19 | email, 20 | callbackUrl: `/reset-password/reset?email=${email}`, 21 | }); 22 | 23 | if (response.ok) { 24 | return { 25 | ok: true, 26 | message: 'Check your email for instructions on resetting your password', 27 | }; 28 | } 29 | 30 | const message = await response.text(); 31 | return { ok: false, message }; 32 | } 33 | 34 | return ( 35 |
36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/react/src/EmailSignIn/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query'; 2 | import { signIn } from '@niledatabase/client'; 3 | 4 | import { useQueryClientOrDefault } from '../../lib/queryClient'; 5 | 6 | import { Props } from './types'; 7 | 8 | export function useEmailSignIn(params?: Props) { 9 | const { 10 | onSuccess, 11 | onError, 12 | beforeMutate, 13 | callbackUrl, 14 | redirect = false, 15 | init, 16 | client, 17 | } = params ?? {}; 18 | const queryClient = useQueryClientOrDefault(client); 19 | const mutation = useMutation( 20 | { 21 | mutationFn: async (_data) => { 22 | const d = { ..._data, callbackUrl, redirect }; 23 | const possibleData = beforeMutate && beforeMutate(d); 24 | const data = possibleData ?? d; 25 | const res = await signIn('email', { init, ...data }); 26 | if (res?.error) { 27 | throw new Error(res.error); 28 | } 29 | return res as unknown as Response; 30 | }, 31 | onSuccess, 32 | onError, 33 | }, 34 | queryClient 35 | ); 36 | return mutation.mutate; 37 | } 38 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/react/src/MultiFactor/SetupAuthenticator.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import { SetupAuthenticator } from './SetupAuthenticator'; 6 | import type { AuthenticatorSetup } from './types'; 7 | 8 | const setup: AuthenticatorSetup = { 9 | method: 'authenticator', 10 | token: 'mock-auth-token', 11 | scope: 'setup', 12 | otpauthUrl: 'otpauth://totp/Nile:demo?secret=JBSWY3DPEHPK3PXP', 13 | recoveryKeys: [ 14 | 'alpha-bravo-charlie', 15 | 'delta-echo-foxtrot', 16 | 'golf-hotel-india', 17 | 'juliet-kilo-lima', 18 | 'mike-november-oscar', 19 | ], 20 | }; 21 | 22 | const meta: Meta = { 23 | title: 'MultiFactor/SetupAuthenticator', 24 | component: SetupAuthenticator, 25 | args: { 26 | setup, 27 | onError: action('onError'), 28 | onSuccess: action('onSuccess'), 29 | }, 30 | render: (args) => , 31 | }; 32 | 33 | export default meta; 34 | 35 | type Story = StoryObj; 36 | 37 | export const Default: Story = {}; 38 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Geist, Geist_Mono } from 'next/font/google'; 3 | import './globals.css'; 4 | 5 | import { SidebarProvider } from '@/components/ui/sidebar'; 6 | import { AppSidebar } from '@/components/ui/appSidebar'; 7 | import '@niledatabase/react/styles.css'; 8 | 9 | const geistSans = Geist({ 10 | variable: '--font-geist-sans', 11 | subsets: ['latin'], 12 | }); 13 | 14 | const geistMono = Geist_Mono({ 15 | variable: '--font-geist-mono', 16 | subsets: ['latin'], 17 | }); 18 | 19 | export const metadata: Metadata = { 20 | title: 'Create Next App', 21 | description: 'Generated by create next app', 22 | }; 23 | 24 | export default function RootLayout({ 25 | children, 26 | }: Readonly<{ 27 | children: React.ReactNode; 28 | }>) { 29 | return ( 30 | 31 | 34 | 35 | 36 |
{children}
37 |
38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /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/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@niledatabase/express", 3 | "version": "5.2.0-alpha.0", 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 | "prettier": { 15 | "printWidth": 80, 16 | "semi": true, 17 | "singleQuote": true, 18 | "trailingComma": "es5" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "scripts": { 24 | "build": "tsup src/index.ts", 25 | "test": "jest" 26 | }, 27 | "author": "jrea", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/niledatabase/nile-js.git", 31 | "directory": "packages/express" 32 | }, 33 | "publishConfig": { 34 | "access": "public" 35 | }, 36 | "peerDependencies": { 37 | "@niledatabase/server": ">=5.0.0-alpha", 38 | "express": "^5.0.0 || ^4.0.0" 39 | }, 40 | "devDependencies": { 41 | "@types/express": "^5", 42 | "express": "^5.1.0", 43 | "jest": "^29.7.0", 44 | "ts-jest": "^29.3.4", 45 | "tsup": "^8.5.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /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/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 | import { useQueryClientOrDefault } from '../../lib/queryClient'; 6 | 7 | import { Props, SignUpInfo } from './types'; 8 | 9 | export function useSignUp( 10 | params: Props, 11 | client?: QueryClient 12 | ) { 13 | const { onSuccess, onError, beforeMutate, ...remaining } = params; 14 | const queryClient = useQueryClientOrDefault(client); 15 | 16 | const mutation = useMutation( 17 | { 18 | mutationFn: async (_data) => { 19 | const possibleData = beforeMutate && beforeMutate(_data); 20 | const payload: T = { ..._data, ...possibleData }; 21 | const { data, error } = await signUp({ 22 | ...remaining, 23 | ...payload, 24 | }); 25 | if (error) { 26 | throw new Error(error); 27 | } 28 | return data; 29 | }, 30 | 31 | onSuccess, 32 | onError, 33 | }, 34 | queryClient 35 | ); 36 | 37 | usePrefetch(params); 38 | 39 | return mutation.mutate; 40 | } 41 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/react/src/EmailSignIn/EmailSignInButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import EmailSignIn from './Form'; 4 | import EmailSignInButton from './EmailSignInButton'; 5 | 6 | const meta = { 7 | title: 'Social/Email', 8 | component: EmailSignIn, 9 | }; 10 | 11 | export default meta; 12 | 13 | export function SignInWithEmail() { 14 | return ( 15 |
16 | { 18 | // noop 19 | }} 20 | /> 21 |
22 | ); 23 | } 24 | 25 | export function VerifyEmailAddress() { 26 | return ( 27 |
28 | Hello user, before you continue, you need to verify your email address. 29 |
30 | { 35 | // do something 36 | }} 37 | onFailure={(e) => { 38 | alert(e?.error); 39 | }} 40 | > 41 | Verify my email address 42 | 43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/tenant-list/ListTenants.tsx: -------------------------------------------------------------------------------- 1 | import { ColumnDef } from '@tanstack/react-table'; 2 | import { TenantSelector, UserInfo } from '@niledatabase/react'; 3 | 4 | import { nile } from '../api/[...nile]/nile'; 5 | 6 | import { DataTable } from './table'; 7 | 8 | export default async function ListTenants() { 9 | const [tenants, me] = await nile.withContext(() => 10 | Promise.all([nile.tenants.list(), nile.users.getSelf()]) 11 | ); 12 | if (tenants instanceof Response || me instanceof Response) { 13 | return null; 14 | } 15 | return ( 16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 | ); 24 | } 25 | 26 | type Tenant = { 27 | id: string; 28 | name: string; 29 | }; 30 | const columns: ColumnDef[] = [ 31 | { 32 | accessorKey: 'id', 33 | header: 'ID', 34 | }, 35 | { 36 | accessorKey: 'name', 37 | header: 'Name', 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /packages/react/src/MultiFactor/ChallengeContent.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { action } from '@storybook/addon-actions'; 3 | 4 | import { ChallengeContent } from './ChallengeContent'; 5 | import type { MfaSetup } from './types'; 6 | 7 | const baseSetup: MfaSetup = { 8 | method: 'authenticator', 9 | token: 'mock-token', 10 | scope: 'challenge', 11 | }; 12 | 13 | const meta: Meta = { 14 | title: 'MultiFactor/ChallengeContent', 15 | component: ChallengeContent, 16 | args: { 17 | payload: baseSetup, 18 | message: 'Enter the 6-digit code from your authenticator app to continue.', 19 | isEnrolled: true, 20 | onSuccess: action('onSuccess'), 21 | }, 22 | }; 23 | 24 | export default meta; 25 | 26 | type Story = StoryObj; 27 | 28 | export const AuthenticatorChallenge: Story = {}; 29 | 30 | export const EmailChallenge: Story = { 31 | args: { 32 | payload: { 33 | method: 'email', 34 | token: 'mock-email-token', 35 | scope: 'challenge', 36 | maskedEmail: 'j***@example.com', 37 | }, 38 | message: 'Enter the 6-digit code we emailed you.', 39 | isEnrolled: true, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/components/ui/appSidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import { SignOutButton } from '@niledatabase/react'; 2 | import { User } from '@niledatabase/server'; 3 | 4 | import AppSidebarClient from './AppSidebarClient'; 5 | 6 | import { nile } from '@/app/api/[...nile]/nile'; 7 | import { 8 | Sidebar, 9 | SidebarContent, 10 | SidebarGroup, 11 | SidebarGroupContent, 12 | SidebarGroupLabel, 13 | } from '@/components/ui/sidebar'; 14 | 15 | export async function AppSidebar() { 16 | const me = await nile.users.getSelf(); 17 | return ( 18 | 19 | 20 | 21 | Application 22 | 23 | 24 | 25 | 26 | 27 | 28 | {me instanceof Response ? null : ( 29 | 30 |
31 | Signed in as {me.email} 32 |
33 | 34 |
35 | )} 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 20 | ); 21 | } 22 | ); 23 | 24 | Input.displayName = 'Input'; 25 | 26 | export { Input }; 27 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/verify-email/VerifyEmail.tsx: -------------------------------------------------------------------------------- 1 | import { Google, UserInfo } from '@niledatabase/react'; 2 | 3 | import { nile } from '../api/[...nile]/nile'; 4 | 5 | import VerifyEmailForm from './VerifyEmailForm'; 6 | 7 | export default async function VerifyEmail() { 8 | const me = await nile.users.getSelf(); 9 | if (me instanceof Response) { 10 | return ( 11 |
12 |
13 | You must be logged in to verify your email address. 14 |
15 |
16 | 17 |
18 |
19 | ); 20 | } 21 | return ( 22 | <> 23 | 24 | 25 | 26 | ); 27 | } 28 | export async function action() { 29 | 'use server'; 30 | const res = await nile.users.verifySelf({ 31 | callbackUrl: '/verify-email', 32 | }); 33 | 34 | if (res instanceof Response) { 35 | return { ok: false, message: await res.text() }; 36 | } 37 | 38 | return { 39 | ok: true, 40 | message: 41 | 'Check your email for instructions on how to verify your email address.', 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/server/src/api/routes/auth/mfa.ts: -------------------------------------------------------------------------------- 1 | import { urlMatches, proxyRoutes, NileAuthRoutes } from '../../utils/routes'; 2 | import request from '../../utils/request'; 3 | import { Routes } from '../../types'; 4 | import { Config } from '../../../utils/Config'; 5 | import { ctx } from '../../utils/request-context'; 6 | 7 | const key = 'MULTI_FACTOR'; 8 | 9 | export default async function route(req: Request, config: Config) { 10 | return request( 11 | proxyRoutes(config.apiUrl)[key], 12 | { 13 | method: req.method, 14 | request: req, 15 | }, 16 | config 17 | ); 18 | } 19 | export function matches(configRoutes: Routes, request: Request): boolean { 20 | return urlMatches(request.url, configRoutes[key]); 21 | } 22 | 23 | export async function fetchMfa( 24 | config: Config, 25 | method: 'DELETE' | 'PUT' | 'POST', 26 | body: string 27 | ): Promise { 28 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.MULTI_FACTOR}`; 29 | const { headers } = ctx.get(); 30 | const init: RequestInit = { 31 | headers, 32 | method, 33 | body, 34 | }; 35 | 36 | const req = new Request(clientUrl, init); 37 | return (await config.handlers[method](req)) as Response; 38 | } 39 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /packages/react/src/SignUpForm/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { useForm } from 'react-hook-form'; 5 | 6 | import { Email, Form, Password } from '../../components/ui/form'; 7 | import { Button } from '../../components/ui/button'; 8 | 9 | import { Props } from './types'; 10 | import { useSignUp } from './hooks'; 11 | 12 | const queryClient = new QueryClient(); 13 | export default function SignUpForm(props: Props) { 14 | const { client } = props ?? {}; 15 | return ( 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export function SignInForm(props: Props) { 23 | const signUp = useSignUp(props); 24 | const form = useForm({ defaultValues: { email: '', password: '' } }); 25 | 26 | return ( 27 |
28 | 30 | signUp({ email, password }) 31 | )} 32 | className="space-y-8" 33 | > 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/react/src/resetPassword/PasswordResetRequestForm/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | import { useForm } from 'react-hook-form'; 4 | import React from 'react'; 5 | 6 | import { Button } from '../../../components/ui/button'; 7 | import { Email, Form } from '../../../components/ui/form'; 8 | import { useResetPassword } from '../hooks'; 9 | import { Props } from '../types'; 10 | 11 | const queryClient = new QueryClient(); 12 | export default function ResetPasswordForm(props: Props) { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | function ResetForm(props: Props) { 21 | const { defaultValues, ...params } = props; 22 | const form = useForm({ defaultValues: { email: '', ...defaultValues } }); 23 | const resetPassword = useResetPassword({ ...params, redirect: true }); 24 | return ( 25 |
26 | { 29 | resetPassword({ email }); 30 | })} 31 | > 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /packages/react/src/SignInForm/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { useForm } from 'react-hook-form'; 5 | 6 | import { Button } from '../../components/ui/button'; 7 | import { Email, Form, Password } from '../../components/ui/form'; 8 | 9 | import { useSignIn } from './hooks'; 10 | import { Props } from './types'; 11 | 12 | const queryClient = new QueryClient(); 13 | 14 | export default function SigningIn(props: Props) { 15 | const { client, ...remaining } = props ?? {}; 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export function SignInForm(props: Props) { 24 | const signIn = useSignIn(props); 25 | const form = useForm({ defaultValues: { email: '', password: '' } }); 26 | 27 | return ( 28 |
29 | 32 | signIn && signIn({ provider: 'credentials', email, password }) 33 | )} 34 | className="space-y-8" 35 | > 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /packages/react/src/SignInForm/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query'; 2 | import { signIn } from '@niledatabase/client'; 3 | 4 | import { useQueryClientOrDefault } from '../../lib/queryClient'; 5 | 6 | import { Props, LoginInfo } from './types'; 7 | 8 | export function useSignIn(params?: Props) { 9 | const { 10 | onSuccess, 11 | onError, 12 | beforeMutate, 13 | callbackUrl, 14 | init, 15 | baseUrl, 16 | fetchUrl, 17 | resetUrl, 18 | auth, 19 | redirect, 20 | client, 21 | } = params ?? {}; 22 | const queryClient = useQueryClientOrDefault(client); 23 | const mutation = useMutation( 24 | { 25 | mutationFn: async (_data: LoginInfo) => { 26 | const d = { ..._data, callbackUrl }; 27 | const possibleData = beforeMutate && beforeMutate(d); 28 | const data = possibleData ?? d; 29 | const res = await signIn(data.provider, { 30 | init, 31 | auth, 32 | baseUrl, 33 | fetchUrl, 34 | redirect, 35 | resetUrl, 36 | ...data, 37 | }); 38 | if (!res?.ok && res?.error) { 39 | throw new Error(res.error); 40 | } 41 | return res; 42 | }, 43 | onSuccess, 44 | onError, 45 | }, 46 | queryClient 47 | ); 48 | return mutation.mutate; 49 | } 50 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/reset-password/server.tsx: -------------------------------------------------------------------------------- 1 | import { UserInfo } from '@niledatabase/react'; 2 | import { User } from '@niledatabase/server'; 3 | 4 | import { nile } from '../api/[...nile]/nile'; 5 | 6 | import { PasswordResetForm } from './resetForm'; 7 | 8 | type ServerResponse = { 9 | ok: boolean; 10 | message?: string; 11 | }; 12 | 13 | export default async function ResetPasswordServer() { 14 | const me = await nile.users.getSelf(); 15 | 16 | if (me instanceof Response) { 17 | return null; 18 | } 19 | async function resetPassword( 20 | _: unknown, 21 | formData: FormData 22 | ): Promise { 23 | 'use server'; 24 | 25 | const password = formData.get('password') as string; 26 | if (me instanceof Response) { 27 | return { ok: false, message: 'You are not logged in.' }; 28 | } 29 | const response = await nile.auth.resetPassword({ 30 | email: me.email, 31 | password, 32 | }); 33 | 34 | if (response.ok) { 35 | return { ok: true, message: 'Password reset' }; 36 | } 37 | 38 | const message = await response.text(); 39 | return { ok: false, message }; 40 | } 41 | 42 | return ( 43 |
44 | 45 | 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/react/src/UserInfo/hooks.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { QueryClient, useQuery } from '@tanstack/react-query'; 3 | import { ActiveSession } from '@niledatabase/client'; 4 | 5 | import { componentFetch, ComponentFetchProps } from '../../lib/utils'; 6 | import { User } from '../../../server/src/users/types'; 7 | import { useQueryClientOrDefault } from '../../lib/queryClient'; 8 | 9 | export type HookProps = ComponentFetchProps & { 10 | user?: User | undefined | null; 11 | baseUrl?: string; 12 | client?: QueryClient; 13 | fetchUrl?: string; 14 | }; 15 | 16 | export function useMe(props?: HookProps): User | null { 17 | const { baseUrl = '', fetchUrl, client, user, auth } = props ?? {}; 18 | const queryClient = useQueryClientOrDefault(client); 19 | const { data, isLoading } = useQuery( 20 | { 21 | queryKey: ['me', baseUrl], 22 | queryFn: async () => { 23 | const res = await componentFetch(fetchUrl ?? '/me', props); 24 | return await res.json(); 25 | }, 26 | enabled: user == null, 27 | }, 28 | queryClient 29 | ); 30 | 31 | if (user || data) { 32 | return user ?? data; 33 | } 34 | // we possibly have email, so return that while we wait for `me` to load 35 | if (auth && !(user && isLoading)) { 36 | return (auth.state?.session as ActiveSession)?.user ?? data; 37 | } 38 | return null; 39 | } 40 | -------------------------------------------------------------------------------- /packages/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 4 | "include": ["src/**/*"], 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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/invites/InviteUserToTenant.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useActionState } from 'react'; 4 | 5 | import { Button } from '@/components/ui/button'; 6 | import { Input } from '@/components/ui/input'; 7 | 8 | type ServerResponse = { 9 | ok: boolean; 10 | message?: string; 11 | }; 12 | 13 | type Props = { 14 | action: ( 15 | prevState: ServerResponse | null, 16 | formData: FormData 17 | ) => Promise; 18 | }; 19 | 20 | export function InviteUserToTenant({ action }: Props) { 21 | const [state, formAction, isPending] = useActionState< 22 | ServerResponse | null, 23 | FormData 24 | >(action, null); 25 | 26 | return ( 27 |
28 |
29 | 35 | 38 |
39 | {state?.message && ( 40 |

43 | {state.message} 44 |

45 | )} 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /packages/react/src/EmailSignIn/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { useForm } from 'react-hook-form'; 5 | import { Mail } from 'lucide-react'; 6 | 7 | import { Email, Form } from '../../components/ui/form'; 8 | import { Button } from '../../components/ui/button'; 9 | 10 | import { EmailSignInInfo, Props } from './types'; 11 | import { useEmailSignIn } from './hooks'; 12 | 13 | const queryClient = new QueryClient(); 14 | 15 | export default function EmailSigningIn(props: Props) { 16 | const { client, ...remaining } = props ?? {}; 17 | return ( 18 | 19 | 20 | 21 | ); 22 | } 23 | export function EmailSignInForm(props: Props & EmailSignInInfo) { 24 | const signIn = useEmailSignIn(props); 25 | const form = useForm({ defaultValues: { email: '' } }); 26 | return ( 27 |
28 | signIn && signIn({ email }))} 30 | className="space-y-8" 31 | > 32 | 33 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /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/server/src/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | import { PartialContext } from '../types'; 2 | 3 | import { TENANT_COOKIE, USER_COOKIE } from './constants'; 4 | 5 | function getTokenFromCookie(headers: Headers, cookieKey: void | string) { 6 | const cookie = headers.get('cookie')?.split('; '); 7 | const _cookies: Record = {}; 8 | if (cookie) { 9 | for (const parts of cookie) { 10 | const cookieParts = parts.split('='); 11 | const _cookie = cookieParts.slice(1).join('='); 12 | const name = cookieParts[0]; 13 | _cookies[name] = _cookie; 14 | } 15 | } 16 | 17 | if (cookie) { 18 | for (const parts of cookie) { 19 | const cookieParts = parts.split('='); 20 | const _cookie = cookieParts.slice(1).join('='); 21 | const name = cookieParts[0]; 22 | _cookies[name] = _cookie; 23 | } 24 | } 25 | if (cookieKey) { 26 | return _cookies[cookieKey]; 27 | } 28 | return null; 29 | } 30 | export function getTenantFromHttp(headers: Headers, context?: PartialContext) { 31 | const cookieTenant = getTokenFromCookie(headers, TENANT_COOKIE); 32 | 33 | return cookieTenant ? cookieTenant : context?.tenantId; 34 | } 35 | 36 | // do we do this any more? 37 | export function getUserFromHttp(headers: Headers, context?: PartialContext) { 38 | const userHeader = headers?.get(USER_COOKIE); 39 | return userHeader ? userHeader : context?.userId; 40 | } 41 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 4 | "include": ["src/**/*", "test/**/*", "../express/src/express.test.ts"], 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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/reset-password/reset/page.tsx: -------------------------------------------------------------------------------- 1 | import ResetPasswordClientSide from '../client'; 2 | 3 | export default async function ResetPasswordFromEmail({ 4 | searchParams, 5 | }: { 6 | searchParams: Promise<{ email: string }>; 7 | }) { 8 | const email = (await searchParams).email; 9 | return ( 10 |
11 |
12 |
Congratulations!
13 |
14 | You made it to the password reset page! This page only works client 15 | side, because there is a temporary signed cookie that can be exchanged 16 | for setting a new password. 17 |
18 |
19 | This means if you made it to this page without that reset token, you 20 | won't be able to reset your password in this way... unless you 21 | are signed in, in which case, resetting your password will work. 22 |
23 |
24 | One thing to note here is that passwords are currently stored as JWTs, 25 | so when you reset your password, that old JWT isn't invalidated, 26 | since we don't know what it is! This is one of the many reasons 27 | to use SSO over credentials. 28 |
29 | 30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/invites/MembersTable.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { ColumnDef } from '@tanstack/react-table'; 3 | import { User } from '@niledatabase/server'; 4 | import { useMemo } from 'react'; 5 | 6 | import { DataTable } from './InviteUserTable'; 7 | import { removeUser } from './actions'; 8 | 9 | import { Button } from '@/components/ui/button'; 10 | 11 | export default function MembersTable({ 12 | users, 13 | me, 14 | }: { 15 | users: User[]; 16 | me: User; 17 | }) { 18 | const columns = useMemo((): ColumnDef[] => { 19 | return [ 20 | { accessorKey: 'email', header: 'Email' }, 21 | { accessorKey: 'created', header: 'created' }, 22 | { accessorKey: 'name', header: 'name' }, 23 | { 24 | id: 'actions', 25 | header: 'Actions', 26 | cell: ({ row }) => { 27 | if (row.original.id === me.id) { 28 | return null; 29 | } 30 | return ( 31 |
32 | 38 |
39 | ); 40 | }, 41 | }, 42 | ]; 43 | }, [me]); 44 | return ( 45 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /packages/server/src/api/utils/request-context.test.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../utils/Config'; 2 | import { ExtensionState } from '../../types'; 3 | 4 | import { ctx, withNileContext } from './request-context'; 5 | 6 | describe('withNileContext', () => { 7 | it('applies extension overrides before running callback', async () => { 8 | const runExtensionsMock = jest.fn(async (state: ExtensionState) => { 9 | if (state === ExtensionState.withContext) { 10 | ctx.set({ 11 | headers: new Headers({ cookie: 'foo=bar' }), 12 | tenantId: 'ext-tenant', 13 | }); 14 | } 15 | }); 16 | 17 | const config = { 18 | context: { 19 | headers: new Headers(), 20 | tenantId: undefined, 21 | userId: undefined, 22 | }, 23 | extensions: [jest.fn()], 24 | extensionCtx: { runExtensions: runExtensionsMock }, 25 | } as unknown as Config; 26 | 27 | await ctx.run({ headers: new Headers() }, async () => { 28 | await withNileContext(config, async () => { 29 | const active = ctx.get(); 30 | expect(active.headers.get('cookie')).toBe('foo=bar'); 31 | expect(active.tenantId).toBe('ext-tenant'); 32 | }); 33 | }); 34 | 35 | const withContextCalls = runExtensionsMock.mock.calls.filter( 36 | ([state]) => state === ExtensionState.withContext 37 | ); 38 | expect(withContextCalls).toHaveLength(1); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/forgot-password/forgotForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useActionState } from 'react'; 4 | 5 | import { Button } from '@/components/ui/button'; 6 | import { Input } from '@/components/ui/input'; 7 | 8 | interface ResetResponse { 9 | ok: boolean; 10 | message?: string; 11 | } 12 | 13 | interface PasswordResetFormProps { 14 | action: ( 15 | prevState: ResetResponse | null, 16 | formData: FormData 17 | ) => Promise; 18 | } 19 | 20 | export function ForgotPasswordForm({ action }: PasswordResetFormProps) { 21 | const [state, formAction, isPending] = useActionState< 22 | ResetResponse | null, 23 | FormData 24 | >(action, null); 25 | 26 | return ( 27 |
28 |
Forgot password
29 |
30 | 31 | 37 |
38 | {state?.message && ( 39 |

42 | {state.message} 43 |

44 | )} 45 | 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /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 | 45 | init.method = 'DELETE'; 46 | const url = `${apiRoutes(config.apiUrl).TENANT_USER(tenantId, userId)}/link`; 47 | 48 | return await fetch(url, init, config); 49 | } 50 | -------------------------------------------------------------------------------- /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.apiUrl).TENANT_USERS(tenantId)}`; 44 | return await request(url, init, config); 45 | } 46 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/reset-password/resetForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useActionState } from 'react'; 4 | 5 | import { Button } from '@/components/ui/button'; 6 | import { Input } from '@/components/ui/input'; 7 | 8 | interface ResetResponse { 9 | ok: boolean; 10 | message?: string; 11 | } 12 | 13 | interface PasswordResetFormProps { 14 | action: ( 15 | prevState: ResetResponse | null, 16 | formData: FormData 17 | ) => Promise; 18 | } 19 | 20 | export function PasswordResetForm({ action }: PasswordResetFormProps) { 21 | const [state, formAction, isPending] = useActionState< 22 | ResetResponse | null, 23 | FormData 24 | >(action, null); 25 | 26 | return ( 27 |
28 |
Reset password form
29 |
30 | 31 | 38 |
39 | {state?.message && ( 40 |

43 | {state.message} 44 |

45 | )} 46 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /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.apiUrl).USER_TENANTS(session.id)}`; 40 | if (typeof session === 'object' && 'user' in session && session.user) { 41 | url = `${apiRoutes(config.apiUrl).USER_TENANTS(session.user.id)}`; 42 | } 43 | 44 | return await request(url, init, config); 45 | } 46 | -------------------------------------------------------------------------------- /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 | import { ctx } from '../../utils/request-context'; 6 | 7 | const key = 'SIGNOUT'; 8 | export default async function route(request: Request, config: Config) { 9 | let url = proxyRoutes(config.apiUrl)[key]; 10 | 11 | const init: RequestInit = { 12 | method: request.method, 13 | }; 14 | if (request.method === 'POST') { 15 | init.body = request.body; 16 | const [provider] = new URL(request.url).pathname.split('/').reverse(); 17 | url = `${proxyRoutes(config.apiUrl)[key]}${ 18 | provider !== 'signout' ? `/${provider}` : '' 19 | }`; 20 | } 21 | 22 | const res = await fetch(url, { ...init, request }, config); 23 | return res; 24 | } 25 | export function matches(configRoutes: Routes, request: Request): boolean { 26 | return urlMatches(request.url, configRoutes[key]); 27 | } 28 | 29 | export async function fetchSignOut( 30 | config: Config, 31 | body: string 32 | ): Promise { 33 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.SIGNOUT}`; 34 | const { headers } = ctx.get(); 35 | const req = new Request(clientUrl, { 36 | method: 'POST', 37 | body, 38 | headers, 39 | }); 40 | 41 | return (await config.handlers.POST(req)) as Response; 42 | } 43 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/interactive-sign-in/SignInSignOut.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SignInForm, SignUpForm } from '@niledatabase/react'; 4 | import { useState } from 'react'; 5 | 6 | export default function SignInSignOut({ 7 | revalidate, 8 | }: { 9 | revalidate: () => void; 10 | }) { 11 | const [msg, setMsg] = useState({ kind: '', msg: '' }); 12 | return ( 13 |
14 |
21 | {msg.msg} 22 |
23 |
24 | { 28 | setMsg({ kind: 'error', msg: e.message }); 29 | }} 30 | onSuccess={() => { 31 | revalidate(); 32 | setMsg({ kind: 'success', msg: 'Sign up success!' }); 33 | }} 34 | /> 35 | { 38 | setMsg({ kind: 'error', msg: e.message }); 39 | }} 40 | onSuccess={() => { 41 | revalidate(); 42 | setMsg({ kind: 'success', msg: 'Sign in success!' }); 43 | }} 44 | /> 45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /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.apiUrl).TENANT(tenantId)}`; 47 | 48 | return await fetch(url, init, config); 49 | } 50 | -------------------------------------------------------------------------------- /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 | 44 | init.method = 'PUT'; 45 | const url = `${apiRoutes(config.apiUrl).TENANT_USER(tenantId, userId)}/link`; 46 | 47 | return await fetch(url, init, config); 48 | } 49 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Google, LinkedIn, SignInForm, SignUpForm } from '@niledatabase/react'; 2 | 3 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 4 | 5 | export const dynamic = 'force-dynamic'; 6 | 7 | export default async function Home() { 8 | return ( 9 |
10 |
The Kitchen Sink
11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 | or 23 |
24 |
25 | 26 | 27 | Sign up 28 | Sign In 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /packages/server/src/api/routes/tenants/[tenantId]/invite/GET.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}/invite: 8 | * get: 9 | * tags: 10 | * - tenants 11 | * summary: Lists invites for a tenant. 12 | * operationId: listInvites 13 | * parameters: 14 | * - name: tenantId 15 | * in: path 16 | * required: true 17 | * schema: 18 | * type: string 19 | * responses: 20 | * "200": 21 | * description: A list of tenant invites 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 GET( 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 = 'GET'; 46 | const url = `${apiRoutes(config.apiUrl).INVITE(tenantId)}`; 47 | 48 | return await fetch(url, init, config); 49 | } 50 | -------------------------------------------------------------------------------- /packages/server/src/api/routes/tenants/[tenantId]/invites/GET.ts: -------------------------------------------------------------------------------- 1 | import request from '../../../../utils/request'; 2 | import { apiRoutes } from '../../../../utils/routes'; 3 | import { Config } from '../../../../../utils/Config'; 4 | /** 5 | * @swagger 6 | * /api/tenants/{tenantId}/invites: 7 | * get: 8 | * tags: 9 | * - tenants 10 | * summary: Lists invites for a tenant. 11 | * operationId: listInvites 12 | * parameters: 13 | * - name: tenantId 14 | * in: path 15 | * required: true 16 | * schema: 17 | * type: string 18 | * responses: 19 | * "200": 20 | * description: A list of tenant invites 21 | * "401": 22 | * description: Unauthorized 23 | * content: 24 | * application/json: 25 | * schema: 26 | * $ref: '#/components/schemas/APIError' 27 | * "404": 28 | * description: Tenant not found 29 | * content: 30 | * application/json: 31 | * schema: 32 | * $ref: '#/components/schemas/APIError' 33 | */ 34 | export async function GET( 35 | config: Config, 36 | init: RequestInit & { request: Request } 37 | ) { 38 | const yurl = new URL(init.request.url); 39 | const [, tenantId] = yurl.pathname.split('/').reverse(); 40 | if (!tenantId) { 41 | return new Response(null, { status: 404 }); 42 | } 43 | 44 | init.method = 'GET'; 45 | const url = `${apiRoutes(config.apiUrl).INVITES(tenantId)}`; 46 | 47 | return await request(url, init, config); 48 | } 49 | -------------------------------------------------------------------------------- /packages/react/src/SignOutButton/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { Slot } from '@radix-ui/react-slot'; 5 | import { LogOut } from 'lucide-react'; 6 | import { signOut } from '@niledatabase/client'; 7 | 8 | import { ComponentFetchProps } from '../../lib/utils'; 9 | import { buttonVariants, ButtonProps } from '../../components/ui/button'; 10 | 11 | type Props = ButtonProps & 12 | ComponentFetchProps & { 13 | redirect?: boolean; 14 | callbackUrl?: string; 15 | buttonText?: string; 16 | baseUrl?: string; 17 | fetchUrl?: string; 18 | basePath?: string; 19 | }; 20 | 21 | const SignOutButton = ({ 22 | callbackUrl, 23 | redirect, 24 | className, 25 | buttonText = 'Sign out', 26 | variant, 27 | size, 28 | baseUrl, 29 | fetchUrl, 30 | basePath, 31 | auth, 32 | asChild = false, 33 | ...props 34 | }: Props) => { 35 | const Comp = asChild ? Slot : 'button'; 36 | return ( 37 | { 41 | signOut({ callbackUrl, redirect, baseUrl, auth, fetchUrl, basePath }); 42 | }} 43 | {...props} 44 | > 45 | {props.children ? ( 46 | props.children 47 | ) : ( 48 |
49 | 50 | {buttonText} 51 |
52 | )} 53 |
54 | ); 55 | }; 56 | 57 | SignOutButton.displayName = 'SignOutButton'; 58 | export default SignOutButton; 59 | -------------------------------------------------------------------------------- /packages/react/src/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/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 | multiFactor?: string | null; 51 | tenants: string[]; 52 | } 53 | -------------------------------------------------------------------------------- /packages/server/src/api/routes/tenants/[tenantId]/invite/[inviteId]/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}/invite/{inviteId}: 8 | * delete: 9 | * tags: 10 | * - tenants 11 | * summary: Deletes an invite on a tenant. 12 | * operationId: deleteInvite 13 | * parameters: 14 | * - name: tenantId 15 | * in: path 16 | * required: true 17 | * schema: 18 | * type: string 19 | * responses: 20 | * "204": 21 | * description: Tenant invite 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 [inviteId, , 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.apiUrl).INVITE(tenantId)}/${inviteId}`; 47 | 48 | return await fetch(url, init, config); 49 | } 50 | -------------------------------------------------------------------------------- /packages/server/src/api/routes/signup/index.tsx: -------------------------------------------------------------------------------- 1 | import { Config } from '../../../utils/Config'; 2 | import { Routes } from '../../types'; 3 | import { ctx } from '../../utils/request-context'; 4 | import { urlMatches, DefaultNileAuthRoutes } from '../../utils/routes'; 5 | 6 | import { POST } from './POST'; 7 | 8 | const key = 'SIGNUP'; 9 | 10 | export default async function route(request: Request, config: Config) { 11 | switch (request.method) { 12 | case 'POST': 13 | return await POST(config, { request }); 14 | 15 | default: 16 | return new Response('method not allowed', { status: 405 }); 17 | } 18 | } 19 | 20 | export function matches(configRoutes: Routes, request: Request): boolean { 21 | return urlMatches(request.url, configRoutes[key]); 22 | } 23 | 24 | export async function fetchSignUp( 25 | config: Config, 26 | payload: { 27 | body?: string; 28 | params?: { newTenantName?: string; tenantId?: string }; 29 | } 30 | ): Promise { 31 | const { body, params } = payload ?? {}; 32 | const q = new URLSearchParams(); 33 | if (params?.newTenantName) { 34 | q.set('newTenantName', params.newTenantName); 35 | } 36 | if (params?.tenantId) { 37 | q.set('tenantId', params.tenantId); 38 | } 39 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${ 40 | DefaultNileAuthRoutes.SIGNUP 41 | }${q.size > 0 ? `?${q}` : ''}`; 42 | const { headers } = ctx.get(); 43 | const req = new Request(clientUrl, { 44 | method: 'POST', 45 | headers, 46 | body, 47 | }); 48 | 49 | return (await config.handlers.POST(req)) as Response; 50 | } 51 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /packages/server/src/api/routes/tenants/[tenantId]/invite/POST.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}/invite: 8 | * post: 9 | * tags: 10 | * - tenants 11 | * summary: Create an invite for a user in a tenant. 12 | * operationId: invite 13 | * parameters: 14 | * - name: tenantId 15 | * in: path 16 | * required: true 17 | * schema: 18 | * type: string 19 | * responses: 20 | * "201": 21 | * description: An email was sent to the user, inviting them to join the tenant 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 POST( 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 = 'POST'; 46 | init.body = init.request.body; 47 | const url = `${apiRoutes(config.apiUrl).INVITE(tenantId)}`; 48 | 49 | return await fetch(url, init, config); 50 | } 51 | -------------------------------------------------------------------------------- /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/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 | import { ctx } from '../../utils/request-context'; 6 | 7 | const key = 'VERIFY_EMAIL'; 8 | export default async function route(req: Request, config: Config) { 9 | const url = proxyRoutes(config.apiUrl)[key]; 10 | 11 | const res = await request( 12 | url, 13 | { 14 | method: req.method, 15 | request: req, 16 | }, 17 | config 18 | ); 19 | 20 | const location = res?.headers?.get('location'); 21 | if (location) { 22 | return new Response(res?.body, { 23 | status: 302, 24 | headers: res?.headers, 25 | }); 26 | } 27 | return new Response(res?.body, { 28 | status: res?.status, 29 | headers: res?.headers, 30 | }); 31 | } 32 | export function matches(configRoutes: Routes, request: Request): boolean { 33 | return urlMatches(request.url, configRoutes[key]); 34 | } 35 | 36 | export async function fetchVerifyEmail( 37 | config: Config, 38 | method: 'POST' | 'GET', 39 | body?: string 40 | ): Promise { 41 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${NileAuthRoutes.VERIFY_EMAIL}`; 42 | const { headers } = ctx.get(); 43 | const init: RequestInit = { 44 | method, 45 | headers, 46 | }; 47 | if (body) { 48 | init.body = body; 49 | } 50 | const req = new Request(clientUrl, init); 51 | 52 | return (await config.handlers[method](req)) as Response; 53 | } 54 | -------------------------------------------------------------------------------- /packages/react/src/MultiFactor/SetupAuthenticator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import QRCode from 'react-qr-code'; 3 | 4 | import { AuthenticatorSetupProps } from './types'; 5 | import RecoverKeys from './RecoveryKeys'; 6 | import { MultiFactorVerify } from './MultiFactorVerify'; 7 | 8 | export function SetupAuthenticator({ 9 | setup, 10 | onError, 11 | onSuccess, 12 | }: AuthenticatorSetupProps) { 13 | // react-qr-code exposes both default and named exports; normalize for bundlers. 14 | const QRComponent = (QRCode as unknown as { default?: unknown }).default 15 | ? (QRCode as unknown as { default: typeof QRCode }).default 16 | : QRCode; 17 | 18 | return ( 19 |
20 | {setup.otpauthUrl ? ( 21 | <> 22 |
23 | 28 |
29 |

30 | Scan the code with your authenticator app, then enter the 6-digit 31 | code below. 32 |

33 | 34 | ) : null} 35 | 36 | {setup.recoveryKeys ? ( 37 | 38 | ) : null} 39 | 40 | 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /packages/server/src/api/routes/users/[userId]/PUT.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/users/{userid}: 8 | * put: 9 | * tags: 10 | * - users 11 | * summary: update a user 12 | * description: updates a user within the tenant 13 | * operationId: updateUser 14 | * parameters: 15 | * - name: userid 16 | * in: path 17 | * required: true 18 | * schema: 19 | * type: string 20 | * requestBody: 21 | * description: |- 22 | * Update a user 23 | * content: 24 | * application/json: 25 | * schema: 26 | * $ref: '#/components/schemas/UpdateUserRequest' 27 | * responses: 28 | * "200": 29 | * description: An updated user 30 | * content: 31 | * application/json: 32 | * schema: 33 | * $ref: '#/components/schemas/User' 34 | * "404": 35 | * description: Not found 36 | * content: {} 37 | * "401": 38 | * description: Unauthorized 39 | * content: {} 40 | */ 41 | 42 | export async function PUT( 43 | config: Config, 44 | init: RequestInit & { request: Request } 45 | ) { 46 | init.body = init.request.body; 47 | init.method = 'PUT'; 48 | 49 | // update the user 50 | 51 | const [userId] = new URL(init.request.url).pathname.split('/').reverse(); 52 | 53 | const url = apiRoutes(config.apiUrl).USER(userId); 54 | 55 | return await fetch(url, init, config); 56 | } 57 | -------------------------------------------------------------------------------- /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.apiUrl).TENANT(tenantId)}`; 51 | 52 | return await fetch(url, init, config); 53 | } 54 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/invites/InvitesTable.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { ColumnDef, Row } from '@tanstack/react-table'; 3 | import { Invite } from '@niledatabase/server'; 4 | import { useState } from 'react'; 5 | 6 | import { DataTable } from './InviteUserTable'; 7 | import { deleteInvite, resend } from './actions'; 8 | 9 | import { Button } from '@/components/ui/button'; 10 | 11 | const Buttons = ({ row }: { row: Row }) => { 12 | const invite = row.original; 13 | const [loading, setLoading] = useState(false); 14 | 15 | return ( 16 |
17 | 28 | 39 |
40 | ); 41 | }; 42 | const columns: ColumnDef[] = [ 43 | { accessorKey: 'identifier', header: 'Email' }, 44 | { accessorKey: 'expires', header: 'expires' }, 45 | { 46 | id: 'actions', 47 | header: 'Actions', 48 | cell: ({ row }) => , 49 | }, 50 | ]; 51 | export default function InvitesTable({ invites }: { invites: Invite[] }) { 52 | return ( 53 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@niledatabase/client", 3 | "version": "5.2.0-alpha.0", 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/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.apiUrl).TENANT(tenantId)}`; 53 | 54 | return await request(url, init, config); 55 | } 56 | -------------------------------------------------------------------------------- /packages/server/src/api/routes/tenants/[tenantId]/invites/index.ts: -------------------------------------------------------------------------------- 1 | import { ctx } from '../../../../utils/request-context'; 2 | import { Config } from '../../../../../utils/Config'; 3 | import { DefaultNileAuthRoutes, isUUID } from '../../../../utils/routes'; 4 | import { Routes } from '../../../../types'; 5 | 6 | import { GET } from './GET'; 7 | 8 | const key = 'INVITES'; 9 | 10 | export default async function route(request: Request, config: Config) { 11 | switch (request.method) { 12 | case 'GET': 13 | return await GET(config, { request }); 14 | default: 15 | return new Response('method not allowed', { status: 405 }); 16 | } 17 | } 18 | 19 | export function matches(configRoutes: Routes, request: Request): boolean { 20 | const url = new URL(request.url); 21 | const [, tenantId] = url.pathname.split('/').reverse(); 22 | const route = configRoutes[key].replace('{tenantId}', tenantId); 23 | return url.pathname.endsWith(route); 24 | } 25 | 26 | export async function fetchInvites(config: Config) { 27 | const { tenantId, headers } = ctx.get(); 28 | if (!tenantId) { 29 | throw new Error( 30 | 'Unable to fetch invites for the tenant, the tenantId context is missing. Call nile.setContext({ tenantId })' 31 | ); 32 | } 33 | if (!isUUID(tenantId)) { 34 | config 35 | .logger('fetchInvites') 36 | .warn( 37 | 'nile.tenantId is not a valid UUID. This may lead to unexpected behavior in your application.' 38 | ); 39 | } 40 | const clientUrl = `${config.serverOrigin}${ 41 | config.routePrefix 42 | }${DefaultNileAuthRoutes.INVITES.replace('{tenantId}', tenantId)}`; 43 | 44 | const req = new Request(clientUrl, { headers }); 45 | 46 | return (await config.handlers.GET(req)) as Response; 47 | } 48 | -------------------------------------------------------------------------------- /packages/server/src/db/PoolProxy.ts: -------------------------------------------------------------------------------- 1 | import pg from 'pg'; 2 | 3 | import { NilePoolConfig } from '../types'; 4 | import { LogReturn } from '../utils/Logger'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | type AllowAny = any; 8 | 9 | export function createProxyForPool( 10 | pool: pg.Pool, 11 | config: NilePoolConfig, 12 | logger: LogReturn, 13 | context: string[] 14 | ): pg.Pool { 15 | const { info, error } = logger('[pool]'); 16 | return new Proxy(pool, { 17 | get(target: AllowAny, property) { 18 | if (property === 'query') { 19 | // give connection string a pass for these problems 20 | if (!config.connectionString) { 21 | if (!config.user || !config.password) { 22 | error( 23 | 'Cannot connect to the database. User and/or password are missing. Generate them at https://console.thenile.dev' 24 | ); 25 | } else if (!config.database) { 26 | error( 27 | 'Unable to obtain database name. Is process.env.NILEDB_POSTGRES_URL set?' 28 | ); 29 | } 30 | } 31 | const caller = target[property]; 32 | return function query(...args: AllowAny) { 33 | let log = '[QUERY]'; 34 | const [tenantId, userId] = context; 35 | if (tenantId) { 36 | log = `${log}[TENANT:${tenantId}]`; 37 | } 38 | if (userId) { 39 | log = `${log}[USER:${userId}]`; 40 | } 41 | info(log, ...args); 42 | // @ts-expect-error - not mine 43 | const called = caller.apply(this, args); 44 | return called; 45 | }; 46 | } 47 | return target[property]; 48 | }, 49 | }) as pg.Pool; 50 | } 51 | -------------------------------------------------------------------------------- /packages/react/src/XSignInButton/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { Slot } from '@radix-ui/react-slot'; 5 | import { signIn } from '@niledatabase/client'; 6 | 7 | import { cn } from '../../lib/utils'; 8 | import { buttonVariants, ButtonProps } from '../../components/ui/button'; 9 | import { SSOButtonProps } from '../types'; 10 | 11 | const XSignInButton = ({ 12 | callbackUrl, 13 | className, 14 | buttonText = 'Continue with X', 15 | variant, 16 | size, 17 | init, 18 | onClick, 19 | asChild = false, 20 | auth, 21 | fetchUrl, 22 | baseUrl, 23 | ...props 24 | }: ButtonProps & SSOButtonProps) => { 25 | const Comp = asChild ? Slot : 'button'; 26 | return ( 27 | { 34 | const res = await signIn('twitter', { 35 | callbackUrl, 36 | init, 37 | auth, 38 | fetchUrl, 39 | baseUrl, 40 | }); 41 | onClick && onClick(e, res); 42 | }} 43 | {...props} 44 | > 45 | 46 | {buttonText} 47 | 48 | ); 49 | }; 50 | 51 | XSignInButton.displayName = 'XSignInButton'; 52 | export default XSignInButton; 53 | 54 | const Icon = () => { 55 | return ( 56 | 57 | 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /packages/react/src/MultiFactor/types.ts: -------------------------------------------------------------------------------- 1 | export type MfaMethod = 'authenticator' | 'email'; 2 | export type ChallengeScope = 'setup' | 'challenge'; 3 | 4 | export type AuthenticatorSetup = { 5 | method: 'authenticator'; 6 | token: string; 7 | scope: ChallengeScope; 8 | otpauthUrl?: string; 9 | secret?: string; 10 | recoveryKeys?: string[]; 11 | }; 12 | 13 | export type EmailSetup = { 14 | method: 'email'; 15 | token: string; 16 | scope: ChallengeScope; 17 | maskedEmail?: string; 18 | }; 19 | 20 | export type CopyState = 'idle' | 'copied' | 'error'; 21 | 22 | export type EnrollMfaProps = { 23 | enrolled?: boolean; 24 | enrolledMethod?: MfaMethod | null; 25 | onRedirect?: (url: string) => void; 26 | onChallengeRedirect?: (params: ChallengeRedirect) => void; 27 | }; 28 | 29 | export type ChallengeRedirect = { 30 | token: string; 31 | method: MfaMethod; 32 | scope: ChallengeScope; 33 | destination?: string; 34 | }; 35 | 36 | export type MfaSetup = AuthenticatorSetup | EmailSetup; 37 | 38 | export type AuthenticatorSetupProps = { 39 | setup: AuthenticatorSetup; 40 | onError: (message: string | null) => void; 41 | onSuccess: (scope: 'setup' | 'challenge') => void; 42 | }; 43 | 44 | export type EnrollmentError = 45 | | 'setup' 46 | | 'disable' 47 | | 'parseSetup' 48 | | 'parseDisable' 49 | | null; 50 | 51 | export type UseMfaEnrollmentOptions = { 52 | method: MfaMethod; 53 | currentMethod: MfaMethod | null; 54 | onRedirect?: (url: string) => void; 55 | onChallengeRedirect?: (params: ChallengeRedirect) => void; 56 | }; 57 | 58 | export type UseMfaEnrollmentResult = { 59 | setup: TSetup | null; 60 | loading: boolean; 61 | errorType: EnrollmentError; 62 | startSetup: () => Promise; 63 | startDisable: () => Promise; 64 | }; 65 | -------------------------------------------------------------------------------- /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.apiUrl).TENANTS}`; 53 | 54 | return await request(url, init, config); 55 | } 56 | -------------------------------------------------------------------------------- /packages/nextjs/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parseCallback, 3 | parseResetToken, 4 | parseToken, 5 | TENANT_COOKIE, 6 | } from '@niledatabase/server'; 7 | import type { Extension, CTX } from '@niledatabase/server'; 8 | 9 | async function nextJsHeaders({ set }: CTX) { 10 | const { cookies, headers } = await import('next/headers'); 11 | const headersHelper = await headers(); 12 | const cooks = await cookies(); 13 | set({ headers: headersHelper }); 14 | const tenantCookie = cooks.get(TENANT_COOKIE); 15 | if (tenantCookie) { 16 | set({ tenantId: tenantCookie.value }); 17 | } 18 | } 19 | const nextJs: Extension = () => { 20 | return { 21 | id: 'next-js-cookies', 22 | // be sure any server side request gets the headers automatically 23 | withContext: async (ctx: CTX) => { 24 | await nextJsHeaders(ctx); 25 | }, 26 | // after the response, be sure additional calls have the correct cookies 27 | onResponse: async ({ response }, { set: setContext }) => { 28 | const resHeaders = response?.headers; 29 | if (resHeaders) { 30 | const token = parseToken(resHeaders); 31 | const reset = parseResetToken(resHeaders); 32 | const callback = parseCallback(resHeaders); 33 | const cookie = [token, reset, callback].filter(Boolean).join('; '); 34 | if (cookie.length) { 35 | // set this to true (only works once) since the request is 100% server side until the next request 36 | setContext({ 37 | headers: new Headers({ cookie }), 38 | }); 39 | } 40 | } 41 | }, 42 | }; 43 | }; 44 | 45 | export { nextJs }; 46 | 47 | export type Handlers = { 48 | GET: (req: Request) => Promise; 49 | POST: (req: Request) => Promise; 50 | DELETE: (req: Request) => Promise; 51 | PUT: (req: Request) => Promise; 52 | }; 53 | -------------------------------------------------------------------------------- /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 | import { ctx } from '../../utils/request-context'; 6 | 7 | const key = 'PASSWORD_RESET'; 8 | export default async function route(req: Request, config: Config) { 9 | const url = proxyRoutes(config.apiUrl)[key]; 10 | 11 | const res = await request( 12 | url, 13 | { 14 | method: req.method, 15 | request: req, 16 | }, 17 | config 18 | ); 19 | 20 | const location = res?.headers?.get('location'); 21 | if (location) { 22 | return new Response(res?.body, { 23 | status: 302, 24 | headers: res?.headers, 25 | }); 26 | } 27 | return new Response(res?.body, { 28 | status: res?.status, 29 | headers: res?.headers, 30 | }); 31 | } 32 | export function matches(configRoutes: Routes, request: Request): boolean { 33 | return urlMatches(request.url, configRoutes.PASSWORD_RESET); 34 | } 35 | 36 | export async function fetchResetPassword( 37 | config: Config, 38 | method: 'POST' | 'GET' | 'PUT', 39 | body: null | string, 40 | params?: URLSearchParams, 41 | useJson = true 42 | ) { 43 | const authParams = new URLSearchParams(params ?? {}); 44 | if (useJson) { 45 | authParams?.set('json', 'true'); 46 | } 47 | const { headers } = ctx.get(); 48 | const clientUrl = `${config.serverOrigin}${config.routePrefix}${ 49 | NileAuthRoutes.PASSWORD_RESET 50 | }?${authParams?.toString()}`; 51 | const init: RequestInit = { 52 | method, 53 | headers, 54 | }; 55 | if (body && method !== 'GET') { 56 | init.body = body; 57 | } 58 | const req = new Request(clientUrl, init); 59 | return (await config.handlers[method](req)) as Response; 60 | } 61 | -------------------------------------------------------------------------------- /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 = 45 | tenantId ?? getTenantFromHttp(init.request.headers, config.context); 46 | 47 | if (!tenant) { 48 | log('[GET] No tenant id provided.'); 49 | return new Response(null, { status: 404 }); 50 | } 51 | init.method = 'GET'; // for testing 52 | const url = apiRoutes(config.apiUrl).TENANT_USERS(tenant); 53 | return await request(url, init, config); 54 | } 55 | -------------------------------------------------------------------------------- /packages/server/src/api/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../../utils/Config'; 2 | 3 | import request from './request'; 4 | 5 | export type ProviderName = 6 | | 'discord' 7 | | 'github' 8 | | 'google' 9 | | 'hubspot' 10 | | 'linkedin' 11 | | 'slack' 12 | | 'twitter' 13 | | 'email' // magic link 14 | | 'credentials' // email + password 15 | | 'azure'; 16 | 17 | export type Providers = { 18 | [providerName in ProviderName]: Provider; 19 | }; 20 | export type Provider = { 21 | id: string; 22 | name: string; 23 | type: string; 24 | signinUrl: string; 25 | callbackUrl: string; 26 | }; 27 | 28 | export type JWT = { 29 | email: string; 30 | sub: string; 31 | id: string; 32 | iat: number; 33 | exp: number; 34 | jti: string; 35 | }; 36 | 37 | export type ActiveSession = { 38 | id: string; 39 | email: string; 40 | expires: string; 41 | user?: { 42 | id: string; 43 | name: string; 44 | image: string; 45 | email: string; 46 | emailVerified: void | Date; 47 | }; 48 | }; 49 | export default async function auth( 50 | req: Request, 51 | config: Config 52 | ): Promise { 53 | const { info, error } = config.logger('[nileauth]'); 54 | info('checking auth'); 55 | 56 | const sessionUrl = `${config.apiUrl}/auth/session`; 57 | info(`using session ${sessionUrl}`); 58 | // handle the pass through with posts 59 | req.headers.delete('content-length'); 60 | 61 | const res = await request(sessionUrl, { request: req }, config); 62 | const cloned = res.clone(); 63 | try { 64 | const session = await new Response(res.body).json(); 65 | if (Object.keys(session).length === 0) { 66 | info('no session found'); 67 | return undefined; 68 | } 69 | info('session active'); 70 | return session; 71 | } catch { 72 | error(cloned.text()); 73 | return undefined; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /apps/nextjs-kitchensink/app/verify-email/VerifyEmailForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import '@niledatabase/react/styles.css'; 4 | import { useForm } from 'react-hook-form'; 5 | import { useActionState, useEffect, useRef, useState } from 'react'; 6 | 7 | import { Button } from '@/components/ui/button'; 8 | import { Form } from '@/components/ui/form'; 9 | 10 | export type ServerResponse = { 11 | ok: boolean; 12 | message?: string; 13 | }; 14 | 15 | type Props = { 16 | action: ( 17 | prevState: ServerResponse | null, 18 | formData: FormData 19 | ) => Promise; 20 | }; 21 | export default function VerifyEmailForm({ action }: Props) { 22 | const [show, setShow] = useState(false); 23 | const timer = useRef(null); 24 | const form = useForm(); 25 | const [state, formAction, pending] = useActionState< 26 | ServerResponse | null, 27 | FormData 28 | >(action, { 29 | message: '', 30 | ok: true, 31 | }); 32 | 33 | useEffect(() => { 34 | setShow(true); 35 | timer.current = setTimeout(() => { 36 | setShow(false); 37 | }, 8000); 38 | return () => { 39 | if (timer.current) { 40 | clearTimeout(timer.current); 41 | } 42 | }; 43 | }, [pending]); 44 | 45 | return ( 46 |
47 |
48 | 49 | 54 | {state?.message} 55 | 56 |
57 | 60 |
61 |
62 | 63 |
64 | ); 65 | } 66 | --------------------------------------------------------------------------------