├── tsconfig.json ├── packages ├── auth-frontend │ ├── public │ │ ├── robots.txt │ │ └── favicon.ico │ ├── server │ │ └── tsconfig.json │ ├── app │ │ ├── css │ │ │ └── main.css │ │ ├── app.config.ts │ │ ├── app.vue │ │ ├── components │ │ │ ├── AuthIcon.vue │ │ │ ├── AuthAgreement.vue │ │ │ └── AuthSocialLogin.vue │ │ └── pages │ │ │ ├── signup.vue │ │ │ ├── login.vue │ │ │ └── forgot-password.vue │ ├── tsconfig.json │ ├── .gitignore │ ├── package.json │ ├── nuxt.config.ts │ ├── sst-env.d.ts │ └── README.md ├── example-client │ ├── public │ │ ├── robots.txt │ │ └── favicon.ico │ ├── app │ │ ├── app.vue │ │ ├── pages │ │ │ ├── callback.vue │ │ │ └── index.vue │ │ ├── middleware │ │ │ └── auth.ts │ │ └── utils │ │ │ └── auth.ts │ ├── server │ │ └── tsconfig.json │ ├── tsconfig.json │ ├── nuxt.config.ts │ ├── .gitignore │ ├── package.json │ ├── sst-env.d.ts │ └── README.md ├── emails │ ├── tsconfig.json │ ├── index.ts │ ├── package.json │ ├── sst-env.d.ts │ └── emails │ │ ├── Welcome.tsx │ │ └── VerificationCode.tsx └── functions │ ├── tsconfig.json │ ├── package.json │ ├── src │ ├── subjects.ts │ ├── emails.tsx │ └── auth.ts │ └── sst-env.d.ts ├── pnpm-workspace.yaml ├── images └── demo.png ├── .npmrc ├── .gitignore ├── package.json ├── sst-env.d.ts ├── LICENSE ├── sst.config.ts └── README.md /tsconfig.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/auth-frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/example-client/public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxflare/auth/HEAD/images/demo.png -------------------------------------------------------------------------------- /packages/example-client/app/app.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamelessly-hoist=true 2 | link-workspace-packages=true 3 | strict-peer-dependencies=false 4 | -------------------------------------------------------------------------------- /packages/auth-frontend/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/example-client/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/auth-frontend/app/css/main.css: -------------------------------------------------------------------------------- 1 | .text-muted { 2 | @apply text-gray-500 dark:text-gray-400; 3 | } 4 | -------------------------------------------------------------------------------- /packages/auth-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxflare/auth/HEAD/packages/auth-frontend/public/favicon.ico -------------------------------------------------------------------------------- /packages/example-client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxflare/auth/HEAD/packages/example-client/public/favicon.ico -------------------------------------------------------------------------------- /packages/auth-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /packages/example-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /packages/auth-frontend/app/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | ui: { 3 | primary: "blue", 4 | gray: "zinc", 5 | }, 6 | }); 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # sst 5 | .sst 6 | 7 | # tmp files 8 | .#* 9 | 10 | # env 11 | .env*.local 12 | .env 13 | 14 | # opennext 15 | .open-next 16 | 17 | # misc 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /packages/auth-frontend/app/app.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/auth-frontend/app/components/AuthIcon.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/emails/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "types": ["@cloudflare/workers-types"], 6 | "moduleResolution": "Bundler", 7 | "jsx": "react" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "types": ["@cloudflare/workers-types"], 6 | "moduleResolution": "Bundler", 7 | "jsx": "react" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/emails/index.ts: -------------------------------------------------------------------------------- 1 | import { template as WelcomeEmail } from "./emails/Welcome"; 2 | import { template as VerificationCodeEmail } from "./emails/VerificationCode"; 3 | 4 | export namespace Emails { 5 | export const Welcome = WelcomeEmail; 6 | export const VerificationCode = VerificationCodeEmail; 7 | } 8 | -------------------------------------------------------------------------------- /packages/example-client/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | compatibilityDate: "2024-11-01", 4 | devtools: { enabled: true }, 5 | 6 | future: { 7 | compatibilityVersion: 4, 8 | }, 9 | 10 | modules: ["@nuxt/ui"], 11 | }); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxflare-auth", 3 | "version": "0.0.0", 4 | "scripts": {}, 5 | "devDependencies": { 6 | "@tsconfig/node22": "^22", 7 | "prettier": "^3.4.2", 8 | "typescript": "^5" 9 | }, 10 | "dependencies": { 11 | "sst": "3.5.3" 12 | }, 13 | "packageManager": "pnpm@9.15.1" 14 | } 15 | -------------------------------------------------------------------------------- /packages/auth-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /packages/example-client/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /packages/auth-frontend/app/components/AuthAgreement.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | By continuing, you agree to our 4 | Terms 5 | and 6 | 7 | Privacy Policy. 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/auth-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "dependencies": { 13 | "@nuxt/ui": "2.20.0", 14 | "nuxt": "^3.15.0", 15 | "vue": "latest", 16 | "zod": "^3.24.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/emails/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxflare-auth/emails", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "email dev" 7 | }, 8 | "exports": { 9 | ".": "./index.ts" 10 | }, 11 | "devDependencies": { 12 | "react-email": "3.0.4" 13 | }, 14 | "dependencies": { 15 | "@react-email/components": "^0.0.31", 16 | "@types/react": "^19.0.2", 17 | "react": "^19.0.0", 18 | "react-dom": "19.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/auth-frontend/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | compatibilityDate: "2024-11-01", 4 | devtools: { enabled: true }, 5 | nitro: { 6 | prerender: { 7 | autoSubfolderIndex: true, 8 | routes: ["/login", "/signup", "/forgot-password"], 9 | }, 10 | }, 11 | css: ["~/css/main.css"], 12 | future: { 13 | compatibilityVersion: 4, 14 | }, 15 | modules: ["@nuxt/ui"], 16 | }); 17 | -------------------------------------------------------------------------------- /packages/example-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxflare-auth/example-client", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "dependencies": { 13 | "@nuxflare-auth/functions": "*", 14 | "@nuxt/ui": "2.20.0", 15 | "@openauthjs/openauth": "^0.3.2", 16 | "nuxt": "^3.15.1", 17 | "vue": "latest" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxflare-auth/functions", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "exports": { 6 | "./subjects": "./src/subjects.ts" 7 | }, 8 | "dependencies": { 9 | "@cloudflare/workers-types": "^4.20241230.0", 10 | "@nuxflare-auth/emails": "*", 11 | "@openauthjs/openauth": "^0.3.5", 12 | "@paralleldrive/cuid2": "^2.2.2", 13 | "hono": "^4.6.16", 14 | "ofetch": "^1.4.1", 15 | "resend": "^4.0.1", 16 | "sst": "3.5.3", 17 | "valibot": "1.0.0-beta.9" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/example-client/app/pages/callback.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 11 | 12 | 27 | -------------------------------------------------------------------------------- /packages/functions/src/subjects.ts: -------------------------------------------------------------------------------- 1 | import { 2 | unknown, 3 | array, 4 | object, 5 | string, 6 | InferOutput, 7 | intersect, 8 | } from "valibot"; 9 | import { createSubjects } from "@openauthjs/openauth/subject"; 10 | 11 | const baseUser = object({ 12 | id: string(), 13 | email: string(), 14 | name: string(), 15 | image: string(), 16 | }); 17 | 18 | const fullUser = intersect([ 19 | baseUser, 20 | object({ 21 | createdAt: string(), 22 | updatedAt: string(), 23 | accounts: array( 24 | object({ 25 | linkedAt: string(), 26 | type: string(), 27 | data: unknown(), 28 | }), 29 | ), 30 | }), 31 | ]); 32 | 33 | export const subjects = createSubjects({ 34 | user: baseUser, 35 | }); 36 | 37 | export type SubjectUser = InferOutput; 38 | 39 | export type FullUser = InferOutput; 40 | -------------------------------------------------------------------------------- /packages/example-client/app/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware(async (to) => { 2 | const { 3 | callback, 4 | sessionState, 5 | subjects, 6 | client, 7 | getTokens, 8 | setTokens, 9 | redirect, 10 | } = useAuth(); 11 | const { accessToken, refreshToken } = getTokens(); 12 | 13 | if (!accessToken || !refreshToken) { 14 | return navigateTo(await login(), { external: true }); 15 | } 16 | const verified = await client.verify(subjects, accessToken, { 17 | refresh: refreshToken, 18 | }); 19 | if (!verified.err) { 20 | if (verified.tokens) 21 | setTokens(verified.tokens.access, verified.tokens.refresh); 22 | sessionState.value = { user: verified.subject.properties }; 23 | } else { 24 | return navigateTo(await login(), { external: true }); 25 | } 26 | 27 | async function login() { 28 | redirect.value = to.path; 29 | const { url } = await client.authorize(callback, "code"); 30 | return url; 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /packages/auth-frontend/app/components/AuthSocialLogin.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 19 | 20 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /* This file is auto-generated by SST. Do not edit. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | /* deno-fmt-ignore-file */ 5 | import "sst"; 6 | export {}; 7 | declare module "sst" { 8 | export interface Resource { 9 | CloudflareAuth: { 10 | type: "sst.cloudflare.Worker"; 11 | url: string; 12 | }; 13 | CloudflareAuthKV: { 14 | type: "sst.cloudflare.Kv"; 15 | }; 16 | Emails: { 17 | type: "sst.cloudflare.Worker"; 18 | }; 19 | GithubClientID: { 20 | type: "sst.sst.Secret"; 21 | value: string; 22 | }; 23 | GithubClientSecret: { 24 | type: "sst.sst.Secret"; 25 | value: string; 26 | }; 27 | GoogleClientID: { 28 | type: "sst.sst.Secret"; 29 | value: string; 30 | }; 31 | GoogleClientSecret: { 32 | type: "sst.sst.Secret"; 33 | value: string; 34 | }; 35 | ResendApiKey: { 36 | type: "sst.sst.Secret"; 37 | value: string; 38 | }; 39 | Static: { 40 | type: "sst.cloudflare.StaticSite"; 41 | url: string; 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/emails/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /* This file is auto-generated by SST. Do not edit. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | /* deno-fmt-ignore-file */ 5 | import "sst"; 6 | export {}; 7 | declare module "sst" { 8 | export interface Resource { 9 | CloudflareAuth: { 10 | type: "sst.cloudflare.Worker"; 11 | url: string; 12 | }; 13 | CloudflareAuthKV: { 14 | type: "sst.cloudflare.Kv"; 15 | }; 16 | Emails: { 17 | type: "sst.cloudflare.Worker"; 18 | }; 19 | GithubClientID: { 20 | type: "sst.sst.Secret"; 21 | value: string; 22 | }; 23 | GithubClientSecret: { 24 | type: "sst.sst.Secret"; 25 | value: string; 26 | }; 27 | GoogleClientID: { 28 | type: "sst.sst.Secret"; 29 | value: string; 30 | }; 31 | GoogleClientSecret: { 32 | type: "sst.sst.Secret"; 33 | value: string; 34 | }; 35 | ResendApiKey: { 36 | type: "sst.sst.Secret"; 37 | value: string; 38 | }; 39 | Static: { 40 | type: "sst.cloudflare.StaticSite"; 41 | url: string; 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/auth-frontend/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /* This file is auto-generated by SST. Do not edit. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | /* deno-fmt-ignore-file */ 5 | import "sst"; 6 | export {}; 7 | declare module "sst" { 8 | export interface Resource { 9 | CloudflareAuth: { 10 | type: "sst.cloudflare.Worker"; 11 | url: string; 12 | }; 13 | CloudflareAuthKV: { 14 | type: "sst.cloudflare.Kv"; 15 | }; 16 | Emails: { 17 | type: "sst.cloudflare.Worker"; 18 | }; 19 | GithubClientID: { 20 | type: "sst.sst.Secret"; 21 | value: string; 22 | }; 23 | GithubClientSecret: { 24 | type: "sst.sst.Secret"; 25 | value: string; 26 | }; 27 | GoogleClientID: { 28 | type: "sst.sst.Secret"; 29 | value: string; 30 | }; 31 | GoogleClientSecret: { 32 | type: "sst.sst.Secret"; 33 | value: string; 34 | }; 35 | ResendApiKey: { 36 | type: "sst.sst.Secret"; 37 | value: string; 38 | }; 39 | Static: { 40 | type: "sst.cloudflare.StaticSite"; 41 | url: string; 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/example-client/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /* This file is auto-generated by SST. Do not edit. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | /* deno-fmt-ignore-file */ 5 | import "sst"; 6 | export {}; 7 | declare module "sst" { 8 | export interface Resource { 9 | CloudflareAuth: { 10 | type: "sst.cloudflare.Worker"; 11 | url: string; 12 | }; 13 | CloudflareAuthKV: { 14 | type: "sst.cloudflare.Kv"; 15 | }; 16 | Emails: { 17 | type: "sst.cloudflare.Worker"; 18 | }; 19 | GithubClientID: { 20 | type: "sst.sst.Secret"; 21 | value: string; 22 | }; 23 | GithubClientSecret: { 24 | type: "sst.sst.Secret"; 25 | value: string; 26 | }; 27 | GoogleClientID: { 28 | type: "sst.sst.Secret"; 29 | value: string; 30 | }; 31 | GoogleClientSecret: { 32 | type: "sst.sst.Secret"; 33 | value: string; 34 | }; 35 | ResendApiKey: { 36 | type: "sst.sst.Secret"; 37 | value: string; 38 | }; 39 | Static: { 40 | type: "sst.cloudflare.StaticSite"; 41 | url: string; 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nuxflare 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/functions/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /* This file is auto-generated by SST. Do not edit. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | /* deno-fmt-ignore-file */ 5 | import "sst"; 6 | export {}; 7 | import "sst"; 8 | declare module "sst" { 9 | export interface Resource { 10 | GithubClientID: { 11 | type: "sst.sst.Secret"; 12 | value: string; 13 | }; 14 | GithubClientSecret: { 15 | type: "sst.sst.Secret"; 16 | value: string; 17 | }; 18 | GoogleClientID: { 19 | type: "sst.sst.Secret"; 20 | value: string; 21 | }; 22 | GoogleClientSecret: { 23 | type: "sst.sst.Secret"; 24 | value: string; 25 | }; 26 | ResendApiKey: { 27 | type: "sst.sst.Secret"; 28 | value: string; 29 | }; 30 | Static: { 31 | type: "sst.cloudflare.StaticSite"; 32 | url: string; 33 | }; 34 | } 35 | } 36 | // cloudflare 37 | import * as cloudflare from "@cloudflare/workers-types"; 38 | declare module "sst" { 39 | export interface Resource { 40 | CloudflareAuth: cloudflare.Service; 41 | CloudflareAuthKV: cloudflare.KVNamespace; 42 | Emails: cloudflare.Service; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/auth-frontend/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # npm 11 | npm install 12 | 13 | # pnpm 14 | pnpm install 15 | 16 | # yarn 17 | yarn install 18 | 19 | # bun 20 | bun install 21 | ``` 22 | 23 | ## Development Server 24 | 25 | Start the development server on `http://localhost:3000`: 26 | 27 | ```bash 28 | # npm 29 | npm run dev 30 | 31 | # pnpm 32 | pnpm run dev 33 | 34 | # yarn 35 | yarn dev 36 | 37 | # bun 38 | bun run dev 39 | ``` 40 | 41 | ## Production 42 | 43 | Build the application for production: 44 | 45 | ```bash 46 | # npm 47 | npm run build 48 | 49 | # pnpm 50 | pnpm run build 51 | 52 | # yarn 53 | yarn build 54 | 55 | # bun 56 | bun run build 57 | ``` 58 | 59 | Locally preview production build: 60 | 61 | ```bash 62 | # npm 63 | npm run preview 64 | 65 | # pnpm 66 | pnpm run preview 67 | 68 | # yarn 69 | yarn preview 70 | 71 | # bun 72 | bun run preview 73 | ``` 74 | 75 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 76 | -------------------------------------------------------------------------------- /packages/example-client/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # npm 11 | npm install 12 | 13 | # pnpm 14 | pnpm install 15 | 16 | # yarn 17 | yarn install 18 | 19 | # bun 20 | bun install 21 | ``` 22 | 23 | ## Development Server 24 | 25 | Start the development server on `http://localhost:3000`: 26 | 27 | ```bash 28 | # npm 29 | npm run dev 30 | 31 | # pnpm 32 | pnpm run dev 33 | 34 | # yarn 35 | yarn dev 36 | 37 | # bun 38 | bun run dev 39 | ``` 40 | 41 | ## Production 42 | 43 | Build the application for production: 44 | 45 | ```bash 46 | # npm 47 | npm run build 48 | 49 | # pnpm 50 | pnpm run build 51 | 52 | # yarn 53 | yarn build 54 | 55 | # bun 56 | bun run build 57 | ``` 58 | 59 | Locally preview production build: 60 | 61 | ```bash 62 | # npm 63 | npm run preview 64 | 65 | # pnpm 66 | pnpm run preview 67 | 68 | # yarn 69 | yarn preview 70 | 71 | # bun 72 | bun run preview 73 | ``` 74 | 75 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 76 | -------------------------------------------------------------------------------- /packages/functions/src/emails.tsx: -------------------------------------------------------------------------------- 1 | import { Resend } from "resend"; 2 | import { Emails } from "@nuxflare-auth/emails"; 3 | import { Resource } from "sst/resource"; 4 | 5 | const templates = { 6 | Welcome: Emails.Welcome, 7 | VerificationCode: Emails.VerificationCode, 8 | }; 9 | 10 | export const sendEmail = async ( 11 | from: string, 12 | to: string, 13 | template: Type, 14 | props: Parameters<(typeof templates)[Type]>[0], 15 | ) => { 16 | const { body, subject } = templates[template](props as any); 17 | const resend = new Resend(Resource.ResendApiKey.value); 18 | try { 19 | await resend.emails.send({ 20 | from, 21 | to, 22 | subject, 23 | react: body, 24 | }); 25 | } catch (error) { 26 | console.error("Failed to send email:", error); 27 | } 28 | }; 29 | 30 | export type SendEmailType = typeof sendEmail; 31 | 32 | export default { 33 | async fetch(req: Request) { 34 | try { 35 | const [from, to, template, props] = (await req.json()) as any[]; 36 | await sendEmail(from, to, template, props); 37 | } catch (err) { 38 | console.error("err", err); 39 | } 40 | return new Response(null, { status: 200 }); 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /packages/example-client/app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | {{ user?.name }} 15 | {{ user?.email }} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Welcome back! 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 43 | -------------------------------------------------------------------------------- /sst.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export default $config({ 3 | app(input) { 4 | return { 5 | name: "nuxmono", 6 | removal: input?.stage === "production" ? "retain" : "remove", 7 | protect: ["production"].includes(input?.stage), 8 | home: "cloudflare", 9 | }; 10 | }, 11 | async run() { 12 | const fromEmail = "hi@nuxflare.com"; 13 | const resendApiKey = new sst.Secret("ResendApiKey"); 14 | const clientSecrets = [ 15 | new sst.Secret("GoogleClientID"), 16 | new sst.Secret("GoogleClientSecret"), 17 | new sst.Secret("GithubClientID"), 18 | new sst.Secret("GithubClientSecret"), 19 | ]; 20 | 21 | const kv = new sst.cloudflare.Kv("CloudflareAuthKV"); 22 | const emails = new sst.cloudflare.Worker("Emails", { 23 | handler: "packages/functions/src/emails.tsx", 24 | link: [resendApiKey], 25 | }); 26 | const staticSite = new sst.cloudflare.StaticSite("Static", { 27 | path: "packages/auth-frontend", 28 | build: { 29 | command: "pnpm run generate", 30 | output: ".output/public", 31 | }, 32 | }); 33 | const auth = new sst.cloudflare.Worker("CloudflareAuth", { 34 | handler: "packages/functions/src/auth.ts", 35 | link: [kv, emails, staticSite.nodes.router, ...clientSecrets], 36 | url: true, 37 | ...($app.stage === "production" 38 | ? { 39 | domain: "authdemo.nuxflare.com", 40 | } 41 | : {}), 42 | environment: { 43 | ...($dev ? { DEV_URL: "http://localhost:3001" } : {}), 44 | FROM_EMAIL: fromEmail, 45 | }, 46 | }); 47 | return { 48 | url: auth.url, 49 | }; 50 | }, 51 | }); 52 | -------------------------------------------------------------------------------- /packages/emails/emails/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Body, 4 | Container, 5 | Head, 6 | Heading, 7 | Html, 8 | Img, 9 | Preview, 10 | Section, 11 | Tailwind, 12 | Text, 13 | } from "@react-email/components"; 14 | 15 | interface WelcomeProps { 16 | username: string; 17 | } 18 | 19 | export const WelcomeEmail = ({ username }: WelcomeProps) => { 20 | const previewText = `Welcome aboard, ${username}!`; 21 | return ( 22 | 23 | 24 | {previewText} 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | Welcome 37 | 38 | 39 | Hi {username}, 40 | 41 | 42 | Welcome aboard! We're excited to have you join us. 43 | 44 | 45 | If you have any questions, please don't hesitate to reach out. 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default WelcomeEmail; 55 | 56 | export const template = (props: WelcomeProps) => ({ 57 | subject: `Welcome to Nuxflare ${props.username}!`, 58 | body: , 59 | }); 60 | -------------------------------------------------------------------------------- /packages/emails/emails/VerificationCode.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Container, 4 | Head, 5 | Heading, 6 | Html, 7 | Img, 8 | Preview, 9 | Section, 10 | Text, 11 | Tailwind, 12 | } from "@react-email/components"; 13 | import * as React from "react"; 14 | 15 | interface VerificationEmailProps { 16 | code: string; 17 | username?: string; 18 | } 19 | 20 | export const VerificationEmail = ({ 21 | code = "123456", 22 | username = "there", 23 | }: VerificationEmailProps) => { 24 | const previewText = "Your verification code"; 25 | return ( 26 | 27 | 28 | {previewText} 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | Verification Code 41 | 42 | 43 | Hi {username}, 44 | 45 | 46 | Here is your verification code: 47 | 48 | 49 | {code} 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default VerificationEmail; 59 | 60 | export const template = ({ code, username }: VerificationEmailProps) => ({ 61 | subject: "Your Verification Code - Nuxflare", 62 | body: , 63 | }); 64 | -------------------------------------------------------------------------------- /packages/example-client/app/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import { subjects, type SubjectUser } from "@nuxflare-auth/functions/subjects"; 2 | import { createClient } from "@openauthjs/openauth/client"; 3 | 4 | type UserSession = { 5 | user?: SubjectUser; 6 | }; 7 | 8 | const useAccessTokenCookie = () => 9 | useCookie("nuxflare-access-token", { 10 | sameSite: "lax", 11 | }); 12 | const useRefreshTokenCookie = () => 13 | useCookie("nuxflare-refresh-token", { 14 | sameSite: "lax", 15 | }); 16 | const useAuthRedirect = () => 17 | useCookie("nuxflare-redirect", { 18 | sameSite: "lax", 19 | }); 20 | const useSessionState = () => 21 | useState("nuxflare-session", () => ({})); 22 | 23 | const client = createClient({ 24 | clientID: "nuxt", 25 | issuer: "https://authdemo.nuxflare.com", 26 | }); 27 | 28 | const setTokens = (access: string, refresh: string) => { 29 | const accessToken = useAccessTokenCookie(); 30 | const refreshToken = useRefreshTokenCookie(); 31 | accessToken.value = access; 32 | refreshToken.value = refresh; 33 | }; 34 | 35 | const getTokens = () => { 36 | return { 37 | accessToken: useAccessTokenCookie().value, 38 | refreshToken: useRefreshTokenCookie().value, 39 | }; 40 | }; 41 | 42 | export const useSession = () => { 43 | const sessionState = useSessionState(); 44 | const accessToken = useAccessTokenCookie(); 45 | const refreshToken = useRefreshTokenCookie(); 46 | const clear = () => { 47 | sessionState.value = {}; 48 | accessToken.value = null; 49 | refreshToken.value = null; 50 | }; 51 | return { 52 | loggedIn: computed(() => !!sessionState.value.user), 53 | user: computed(() => sessionState.value.user || null), 54 | session: sessionState, 55 | clear, 56 | }; 57 | }; 58 | 59 | export const useAuth = () => { 60 | const callback = useRequestURL().origin + "/callback"; 61 | const redirect = useAuthRedirect(); 62 | const sessionState = useSessionState(); 63 | return { 64 | subjects, 65 | client, 66 | callback, 67 | redirect, 68 | sessionState, 69 | setTokens, 70 | getTokens, 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | # Nuxflare Auth 4 | 5 | A modern, lightweight, self-hosted auth server built with [Cloudflare](https://cloudflare.com), [Nuxt](https://nuxt.com), and [OpenAuth.js](https://openauth.js.org/). 6 | 7 | ## What's This? 8 | 9 | Nuxflare Auth lets you add authentication to your apps without the headache. It's a monorepo that bundles everything you need: 10 | 11 | - A slick auth UI built with Nuxt 3 and [@nuxt/ui](https://ui.nuxt.com) 12 | - Backend auth magic running on Cloudflare Workers 13 | - A ready-to-use example so you can see how it all fits together 14 | 15 | ## Features 16 | 17 | - 🔒 Complete authentication UI including: 18 | - Code-based login 19 | - Password-based login 20 | - Forgot password flow 21 | - User registration 22 | - 🔑 OAuth2 authentication with GitHub and Google (easily add more providers) 23 | - ✉️ Emails using Resend (or use any other provider) 24 | - ⚡ Lightning-fast, powered by Cloudflare's edge network 25 | 26 | ## Project Layout 27 | 28 | ``` 29 | packages/ 30 | ├── auth-frontend/ # auth UI components 31 | ├── emails/ # react email templates 32 | ├── example-client/ # example nuxt client 33 | └── functions/ # cloudflare workers 34 | ``` 35 | 36 | ## Prerequisites 37 | 38 | Before getting started, you'll need: 39 | 40 | - pnpm 41 | - A Cloudflare account 42 | - OAuth credentials from Google and GitHub 43 | - A [Resend](https://resend.com) API key for sending emails 44 | 45 | ## Getting Started 46 | 47 | 1. Clone the repository and install dependencies: 48 | 49 | ```bash 50 | git clone https://github.com/nuxflare/auth nuxflare-auth 51 | cd nuxflare-auth 52 | pnpm install 53 | ``` 54 | 55 | 2. **Create and Configure API Token:** 56 | 57 | a. Create a Cloudflare API token with the required permissions using [this link](https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22:%22workers_r2%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22workers_kv_storage%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22workers_scripts%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22memberships%22,%22type%22:%22read%22%7D,%7B%22key%22:%22user_details%22,%22type%22:%22read%22%7D%5D&name=Nuxflare%20Auth).\ 58 | b. Set the `CLOUDFLARE_API_TOKEN` environment variable: 59 | 60 | ```bash 61 | export CLOUDFLARE_API_TOKEN=GahXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 62 | ``` 63 | 64 | 3. Configure your secrets: 65 | 66 | ```bash 67 | # OAuth stuff 68 | pnpm sst secret set GoogleClientID your_client_id 69 | pnpm sst secret set GoogleClientSecret your_client_secret 70 | pnpm sst secret set GithubClientID your_client_id 71 | pnpm sst secret set GithubClientSecret your_client_secret 72 | 73 | # For emails 74 | pnpm sst secret set ResendApiKey your_resend_api_key 75 | ``` 76 | 77 | 4. Configure your `fromEmail` in `sst.config.ts`: 78 | 79 | ```typescript 80 | async run() { 81 | const fromEmail = "hi@nuxflare.com"; 82 | // ... 83 | } 84 | ``` 85 | 86 | 5. Start local development: 87 | 88 | ```bash 89 | pnpm dev 90 | ``` 91 | 92 | 6. Deploy to production: 93 | ```bash 94 | pnpm sst deploy --stage production 95 | ``` 96 | 97 | ## Architecture 98 | 99 | ### Frontend (`packages/auth-frontend`) 100 | 101 | Login, signup, sign in with code, and forgot password flows built with Nuxt and Nuxt UI. 102 | 103 | ### Backend (`packages/functions`) 104 | 105 | The backend consists of two main components: 106 | 107 | - `auth.ts`: Core authentication logic handler 108 | - `emails.tsx`: Sending emails with Resend 109 | 110 | Everything is stored in Cloudflare KV—sessions, users, etc. Plus, it all runs on the edge, so it's super fast. 111 | 112 | ## Example Implementation 113 | 114 | Check out `packages/example-client` to see how to add auth to your own app. 115 | 116 | ## Support 117 | 118 | Found a bug? Please [open an issue](https://github.com/nuxflare/auth/issues) on our GitHub repository. 119 | -------------------------------------------------------------------------------- /packages/auth-frontend/app/pages/signup.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sign Up 8 | {{ 9 | state === "code" 10 | ? "Enter the verification code sent to your email." 11 | : "Create an account now." 12 | }} 13 | 14 | 21 | 28 | 35 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 60 | 61 | 62 | 67 | 68 | 69 | 70 | 71 | {{ state === "code" ? "Verify Email" : "Sign Up" }} 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | Already have an account? 98 | Login. 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 173 | -------------------------------------------------------------------------------- /packages/auth-frontend/app/pages/login.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Welcome back! 8 | 9 | {{ 10 | state === "code" 11 | ? "Enter the verification code sent to your email." 12 | : "Sign in to your account." 13 | }} 14 | 15 | 16 | 23 | 30 | 37 | 48 | 49 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | 67 | 68 | 69 | 75 | 76 | Forgot password? 84 | 85 | 86 | 87 | 88 | 95 | {{ 96 | state === "code" 97 | ? "Verify Code" 98 | : data.password 99 | ? "Sign In" 100 | : "Send Code" 101 | }} 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | Didn't get the code? 119 | Resend. 123 | 124 | 125 | 126 | Don't have an account? 128 | Sign Up. 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 192 | -------------------------------------------------------------------------------- /packages/auth-frontend/app/pages/forgot-password.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Forgot Password 8 | {{ 9 | state === "code" 10 | ? "Enter the verification code sent to your email." 11 | : state === "update" 12 | ? "Enter your new password." 13 | : "Enter your email to reset your password." 14 | }} 15 | 16 | 17 | 24 | 25 | 32 | 33 | 40 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 69 | 70 | 76 | 77 | 78 | 84 | 85 | 86 | 87 | 88 | {{ 89 | state === "code" 90 | ? "Verify Code" 91 | : state === "update" 92 | ? "Update Password" 93 | : "Send Code" 94 | }} 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | Didn't get the code? 104 | 105 | Resend. 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | Remember your password? 114 | Login. 123 | 124 | 125 | 126 | 127 | 128 | 129 | 207 | -------------------------------------------------------------------------------- /packages/functions/src/auth.ts: -------------------------------------------------------------------------------- 1 | import { issuer } from "@openauthjs/openauth"; 2 | import { createClient } from "@openauthjs/openauth/client"; 3 | import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"; 4 | import { PasswordProvider } from "@openauthjs/openauth/provider/password"; 5 | import { CodeProvider } from "@openauthjs/openauth/provider/code"; 6 | import { GithubProvider } from "@openauthjs/openauth/provider/github"; 7 | import { GoogleProvider } from "@openauthjs/openauth/provider/google"; 8 | import { ofetch } from "ofetch"; 9 | import { createId } from "@paralleldrive/cuid2"; 10 | import { Resource } from "sst/resource"; 11 | import { subjects } from "./subjects"; 12 | import { createMiddleware } from "hono/factory"; 13 | import type { 14 | Service, 15 | ExecutionContext, 16 | KVNamespace, 17 | } from "@cloudflare/workers-types"; 18 | import type { FullUser, SubjectUser } from "./subjects"; 19 | import type { SendEmailType } from "./emails"; 20 | 21 | let _clients: Record> = {}; 22 | const getClient = (iss: string, hono: ReturnType) => { 23 | if (_clients[iss]) return _clients[iss]; 24 | return createClient({ 25 | issuer: iss, 26 | clientID: "same", 27 | // @ts-expect-error passing arguments like this isn't typesafe 28 | fetch: async (...args) => await hono.request(...args), 29 | }); 30 | }; 31 | 32 | let fromEmail = ""; 33 | 34 | const sendEmail: SendEmailType = async (...args) => { 35 | try { 36 | await Resource.Emails.fetch("https://emails.internal", { 37 | method: "POST", 38 | headers: { 39 | "Content-Type": "application/json", 40 | }, 41 | body: JSON.stringify(args), 42 | }); 43 | } catch (err) { 44 | console.error("sending email err", err); 45 | } 46 | }; 47 | 48 | async function sendVerificationCode(email: string, code: string) { 49 | await sendEmail(fromEmail, email, "VerificationCode", { 50 | code, 51 | }); 52 | } 53 | 54 | interface Env { 55 | CloudflareAuthKV: KVNamespace; 56 | DEV_URL?: string; 57 | FROM_EMAIL: string; 58 | } 59 | 60 | export default { 61 | async fetch(request: Request, env: Env, ctx: ExecutionContext) { 62 | fromEmail = env.FROM_EMAIL; 63 | const storage = CloudflareStorage({ 64 | namespace: env.CloudflareAuthKV, 65 | }); 66 | const origin = new URL(request.url).origin; 67 | const devUrl = env.DEV_URL || ""; 68 | const hono = issuer({ 69 | storage, 70 | subjects, 71 | async select(_providers, req) { 72 | const redirect = new URL(devUrl || origin); 73 | redirect.pathname = "/login"; 74 | if (devUrl) 75 | redirect.searchParams.set("origin", new URL(req.url).origin); 76 | return new Response("", { 77 | status: 302, 78 | headers: { 79 | Location: redirect.toString(), 80 | }, 81 | }); 82 | }, 83 | providers: { 84 | password: PasswordProvider({ 85 | async sendCode(email, code) { 86 | await sendVerificationCode(email, code); 87 | }, 88 | async login(req, form, err) { 89 | const redirect = new URL(devUrl || origin); 90 | redirect.pathname = "/login"; 91 | if (devUrl) 92 | redirect.searchParams.set("origin", new URL(req.url).origin); 93 | const email = form?.get("email"); 94 | if (email) redirect.searchParams.set("email", email); 95 | if (err?.type) redirect.searchParams.set("err", err?.type); 96 | return new Response("", { 97 | status: 302, 98 | headers: { 99 | Location: redirect.toString(), 100 | }, 101 | }); 102 | }, 103 | async register(req, state, form, err) { 104 | const redirect = new URL(devUrl || origin); 105 | redirect.pathname = "/signup"; 106 | if (devUrl) 107 | redirect.searchParams.set("origin", new URL(req.url).origin); 108 | const email = form?.get("email"); 109 | if (email) redirect.searchParams.set("email", email); 110 | if (state?.type) redirect.searchParams.set("state", state?.type); 111 | if (err?.type) redirect.searchParams.set("err", err?.type); 112 | return new Response("", { 113 | status: 302, 114 | headers: { 115 | Location: redirect.toString(), 116 | }, 117 | }); 118 | }, 119 | async change(req, state, form, err) { 120 | const redirect = new URL(devUrl || origin); 121 | redirect.pathname = "/forgot-password"; 122 | if (devUrl) 123 | redirect.searchParams.set("origin", new URL(req.url).origin); 124 | const email = form?.get("email"); 125 | if (email) redirect.searchParams.set("email", email); 126 | if (state?.type) redirect.searchParams.set("state", state?.type); 127 | if (err?.type) redirect.searchParams.set("err", err?.type); 128 | return new Response("", { 129 | status: 302, 130 | headers: { 131 | Location: redirect.toString(), 132 | }, 133 | }); 134 | }, 135 | }), 136 | code: CodeProvider({ 137 | async request(req, state, form, err) { 138 | const redirect = new URL(devUrl || origin); 139 | redirect.pathname = "/login"; 140 | if (devUrl) 141 | redirect.searchParams.set("origin", new URL(req.url).origin); 142 | const email = form?.get("email") || (state as any)?.claims?.email; 143 | if (email) redirect.searchParams.set("email", email); 144 | if (state?.type) redirect.searchParams.set("state", state?.type); 145 | if (err?.type) redirect.searchParams.set("err", err?.type); 146 | return new Response("", { 147 | status: 302, 148 | headers: { 149 | Location: redirect.toString(), 150 | }, 151 | }); 152 | }, 153 | async sendCode(claims, code) { 154 | await sendVerificationCode(claims.email, code); 155 | }, 156 | }), 157 | github: GithubProvider({ 158 | clientID: Resource.GithubClientID.value, 159 | clientSecret: Resource.GithubClientSecret.value, 160 | scopes: ["read:user", "user:email"], 161 | }), 162 | google: GoogleProvider({ 163 | clientID: Resource.GoogleClientID.value, 164 | clientSecret: Resource.GoogleClientSecret.value, 165 | scopes: ["openid", "email", "profile"], 166 | }), 167 | }, 168 | success: async (ctx, value) => { 169 | async function getUserObject(): Promise<{ 170 | email: string; 171 | name?: string; 172 | image?: string; 173 | data?: object; 174 | }> { 175 | if (value.provider === "code") { 176 | return { email: value.claims.email }; 177 | } else if (value.provider === "google") { 178 | const accessToken = value.tokenset.access; 179 | const data = await ofetch( 180 | "https://www.googleapis.com/oauth2/v3/userinfo", 181 | { 182 | headers: { Authorization: `Bearer ${accessToken}` }, 183 | }, 184 | ); 185 | return { 186 | email: data.email as string, 187 | name: data.name as string, 188 | image: data.picture as string, 189 | data, 190 | }; 191 | } else if (value.provider === "github") { 192 | const accessToken = value.tokenset.access; 193 | const data = await ofetch("https://api.github.com/user", { 194 | headers: { 195 | Authorization: `Bearer ${accessToken}`, 196 | "User-Agent": "nuxflare-auth", 197 | }, 198 | }); 199 | if (!data.email) { 200 | const emails = (await ofetch( 201 | "https://api.github.com/user/emails", 202 | { 203 | headers: { 204 | Authorization: `Bearer ${accessToken}`, 205 | "User-Agent": "nuxflare-auth", 206 | }, 207 | }, 208 | )) as any[]; 209 | data.email = ( 210 | emails.find((email) => email.primary) || emails[0] 211 | )?.email; 212 | } 213 | return { 214 | email: data.email as string, 215 | name: data.name as string, 216 | image: data.avatar_url as string, 217 | data, 218 | }; 219 | } 220 | return { email: value.email }; 221 | } 222 | 223 | try { 224 | const userObject = await getUserObject(); 225 | const now = new Date().toISOString(); 226 | const existing = (await storage.get([ 227 | "users", 228 | userObject.email, 229 | ])) as FullUser; 230 | let user; 231 | if (existing) { 232 | user = existing; 233 | if (!user.email && userObject.email) user.email = userObject.email; 234 | if (!user.name && userObject.name) user.name = userObject.name; 235 | if (!user.image && userObject.image) user.image = userObject.image; 236 | user.updatedAt = now; 237 | const existingAccount = user.accounts.find( 238 | (acc) => acc.type === value.provider, 239 | ); 240 | if (existingAccount) { 241 | existingAccount.data = userObject.data; 242 | } else { 243 | user.accounts.push({ 244 | linkedAt: now, 245 | type: value.provider, 246 | data: userObject.data, 247 | }); 248 | } 249 | } else { 250 | user = { 251 | id: createId(), 252 | email: userObject.email, 253 | name: userObject.name || "", 254 | image: userObject.image || "", 255 | createdAt: now, 256 | updatedAt: now, 257 | accounts: [ 258 | { 259 | linkedAt: now, 260 | type: value.provider, 261 | data: userObject.data, 262 | }, 263 | ], 264 | }; 265 | await sendEmail(fromEmail, userObject.email, "Welcome", { 266 | username: userObject.name || "User", 267 | }); 268 | } 269 | await storage.set(["users", user.email], user); 270 | return ctx.subject("user", { 271 | id: user.id, 272 | name: user.name, 273 | email: user.email, 274 | image: user.image, 275 | }); 276 | } catch (err) { 277 | console.error("err in success", err); 278 | throw new Error("Something went wrong"); 279 | } 280 | }, 281 | }); 282 | 283 | const authMiddleware = createMiddleware(async (c, next) => { 284 | const client = getClient(new URL(c.req.url).origin, hono); 285 | const token = 286 | c.req.header("Authorization")?.match(/^Bearer\s+(.+)$/)?.[1] || ""; 287 | const verified = await client.verify(subjects, token); 288 | if (!verified.err) { 289 | c.set("subject", verified.subject.properties); 290 | return await next(); 291 | } 292 | return c.json({ message: "Unauthorized" }, 401); 293 | }); 294 | 295 | hono.get("/user", authMiddleware as any, async (c) => { 296 | const user = await storage.get([ 297 | "users", 298 | (c.get("subject" as any) as SubjectUser).email, 299 | ]); 300 | return c.json(user); 301 | }); 302 | 303 | hono.get("/*", async (c) => { 304 | const res: Response = await ( 305 | (Resource as any).StaticRouter as Service 306 | ).fetch(c.req.raw); 307 | return res; 308 | }); 309 | 310 | return hono.fetch(request, env, ctx); 311 | }, 312 | }; 313 | --------------------------------------------------------------------------------