diff --git a/.github/workflows/release.yml b/.github/workflows/main.yml similarity index 58% rename from .github/workflows/release.yml rename to .github/workflows/main.yml index 62c3ae0..4e0b016 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/main.yml @@ -1,20 +1,33 @@ -# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created -# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages +name: Main -name: Release on: push: - branches: - - release + branches: [main] + pull_request: + branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 24 + cache: npm + - run: npm ci + - run: npx nuxt-module-build prepare + + test: + if: github.event_name == 'push' + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm - run: npm ci - run: npm run lint - run: npx nuxt-module-build prepare @@ -22,7 +35,8 @@ jobs: - run: npm test release: - needs: build + if: github.event_name == 'push' + needs: test runs-on: ubuntu-latest permissions: contents: write @@ -32,9 +46,10 @@ jobs: id-token: write steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: 21 + node-version: 24 + cache: npm - run: npm ci - run: npx nuxt-module-build prepare - run: npm run prepack diff --git a/.github/workflows/versioned.yml b/.github/workflows/versioned.yml new file mode 100644 index 0000000..a1093fb --- /dev/null +++ b/.github/workflows/versioned.yml @@ -0,0 +1,59 @@ +name: Versioned + +on: + push: + branches: ['v*'] + pull_request: + branches: ['v*'] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npx nuxt-module-build prepare + + test: + if: github.event_name == 'push' + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm run lint + - run: npx nuxt-module-build prepare + - run: npm run prepack + - run: npm test + + release: + if: github.event_name == 'push' && github.ref_name != vars.LATEST_VERSION_BRANCH + needs: test + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: write + packages: write + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npx nuxt-module-build prepare + - run: npm run prepack + - run: npx semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9e9ee17 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,61 @@ +# AGENTS.md + +Nuxt module package (`@workmate/nuxt-auth`). Published artifact is `dist/module.mjs` built by `@nuxt/module-builder`. There is no host app — `playground/` is the dev harness and `test/fixtures/basic/` is the test harness. + +## Layout that matters + +- `src/module.ts` — module entry. Registers plugins, server handlers (`/api/auth/*`), route middlewares, and the schema-generation templates. Adding a new server route or middleware requires editing here, not just dropping a file. +- `src/schema-utils.ts` — build-time only. Collects per-provider Zod schemas from user config, converts each to JSON Schema, strips schemas from the options written to `runtimeConfig`, and renders `auth-schemas.gen.mjs` + `auth-schemas.gen.d.ts` via `addTemplate`. Both Nuxt and Nitro aliases for `#auth-schemas` point at the generated runtime file (see the alias setup in `src/module.ts`). +- `src/runtime/` — code shipped to the host app. Composables, plugins, middleware, providers, server handlers wired by `module.ts`. +- `src/runtime/providers/` — `AuthProvider` is the base class; `Local`, `Github`, `Google` extend it. There are no Facebook/LinkedIn provider files. +- Github/Google server callback handlers (`/api/auth/callback/{provider}`) are only registered when that provider is present in user config — check the `providers.github` / `providers.google` conditionals in `src/module.ts`. +- `playground/nuxt.config.ts` and `test/fixtures/basic/nuxt.config.ts` import the module from `../src/module` (not the published package). +- `test/basic.test.ts` is the only test today. It boots Nuxt via `@nuxt/test-utils/e2e` — full SSR, not unit tests. `PLAN.md` describes a larger intended test suite but it is not built yet. + +## Schema pipeline (non-obvious, easy to break) + +- Providers accept `schemas: { login?, user? }` (Zod). These cannot survive `runtimeConfig` — Nuxt 4 runs Nitro in a separate worker and Zod instances aren't JSON-serializable. +- `collectSchemas` converts them to JSON Schema at build time; `stripSchemasFromProviders` removes them before defu-merge into `runtimeConfig.auth`. At runtime the generated `#auth-schemas` module rebuilds Zod via `z.fromJSONSchema()`. +- Providers and `src/runtime/composables/useAuthLogin.ts` read `loginSchemas[name]` / `userSchemas[name]` from `#auth-schemas` for validation. Never read schemas from `runtimeConfig` — they won't be there. +- Generated type file exports `LoginData` and `UserDataByProvider` (keyed by `local | github | google`). `useAuthLogin().local()` is typed off `LoginData["local"]`, which is derived from the user's Zod schema in `nuxt.config.ts` — so changing that schema changes public types. +- This depends on **Zod 4** (`zod@4.x`). `z.toJSONSchema` / `z.fromJSONSchema` do not exist on Zod 3. Do not downgrade. +- After changing schemas in `playground/nuxt.config.ts`, the generated files in `.nuxt/` need a regen — re-run `dev` / `dev:prepare`. + +## Commands + +First-time / after `node_modules` reset: + +```bash +npm run dev:prepare # nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground +``` + +Daily: + +```bash +npm run dev # runs playground at :3000 +npm run lint # eslint . +npm test # vitest run (boots Nuxt; slow, not unit-level) +npm run test:watch +npm run prepack # nuxt-module-build build → dist/ +``` + +No `typecheck` script. Types are validated indirectly by `nuxt-module-build prepare` (which writes `.nuxt/tsconfig.json` that the root `tsconfig.json` extends) and by `npm test`. + +Do **not** run `npm run release` locally — it publishes to npm and pushes tags. Releases happen on CI when commits land on the `release` branch (`.github/workflows/release.yml` runs `lint → nuxt-module-build prepare → prepack → test → semantic-release`). + +## Conventions that bite + +- **Conventional Commits required.** `semantic-release` derives the version from commit messages on the `release` branch. A non-conforming commit gets ignored or breaks the release. +- ESLint config (`eslint.config.js`) intentionally disables `no-explicit-any`, `no-empty-object-type`, `no-invalid-void-type`, `vue/multi-word-component-names`, `no-console`, stylistic rules, and more. Don't reintroduce these as "fixes" — the relaxations are deliberate. +- `ban-ts-comment` is configured to **allow** `@ts-expect-error`. Prefer it over `@ts-ignore`. +- `.editorconfig` enforces 2-space, LF, final newline. Source files use **double quotes** and trailing semicolons; match the existing style (ESLint stylistic is off so it won't catch you). +- Module options are merged into `runtimeConfig.auth` (private, full options minus Zod schemas) and a subset into `runtimeConfig.public.auth` (`redirects`, `global`, `apiClient` only). When adding options, decide which side they belong on — see the defu merges near the bottom of `src/module.ts`'s `setup`. +- `nuxt-module-build prepare` writes types into `.nuxt/` that `tsconfig.json` extends. Type errors after pulling new code usually mean re-running `npm run dev:prepare`. + +## Things not to assume + +- `README.md` and `CHANGELOG.md` are partly stale — `PLAN.md` (Stage 3) explicitly tracks doc updates for the schema-pipeline API still owed. Treat `PLAN.md` + source as truth; treat README config examples (`principal`/`password`, `defineAuthEndpointSchemas`) as outdated. +- There is no separate unit-test layer. Every test currently spins up Nuxt — adding cheap unit tests is fine but pick a non-`@nuxt/test-utils` import. +- No formatter (no Prettier). ESLint with stylistic off is the only lint pass. +- No husky / lint-staged / pre-commit hooks. +- Node 24 in the CI build step, Node 21 in the release step (`.github/workflows/release.yml`). Prefer Node ≥21 locally. diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ef9ec..2782b62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,5 +46,26 @@ # Changelog +## v2.0.0 + +### Breaking changes + +- `body: { principal, password }` removed from `local` provider `signIn` config — use `schemas.login` instead +- Zod 4 required (`zod@^4.0.0`) — `z.toJSONSchema` / `z.fromJSONSchema` are Zod 4 only + +### Features + +- Open login body for local provider — any object is forwarded to the `signIn` endpoint; field names are no longer constrained to `principal`/`password` +- Per-provider Zod schema pipeline: define `schemas.login` and `schemas.user` in `nuxt.config.ts` per provider +- Build-time JSON Schema conversion via `src/schema-utils.ts`; runtime `#auth-schemas` virtual module reconstructs Zod schemas for both Nuxt and Nitro +- `useAuthLogin` composable — `localLogin`, `googleLogin`, `githubLogin` typed from your schemas +- `useAuthUser` composable — `{ user, isLoggedIn, refreshUser }` +- `useAuthToken` composable — `{ token, refreshToken, tokenType, provider, tokenNames, refreshTokens }` +- Server-side login body validation via `schemas.login` — invalid requests return HTTP 400 with field errors +- Server-side user response validation via `schemas.user` — mismatches throw with field-level detail + +--- ## v1.0.0 + +Initial release. See [migration guide](README.md#migration-v1--v2) for upgrading to v2. diff --git a/README.md b/README.md index 1a5a870..c0996a1 100644 --- a/README.md +++ b/README.md @@ -1,131 +1,124 @@ - - -# Auth module for Nuxt 3 & 4 server apps +# @workmate/nuxt-auth ![NPM](https://img.shields.io/npm/l/@workmate/nuxt-auth) ![npm](https://img.shields.io/npm/v/@workmate/nuxt-auth) ![GitHub last commit](https://img.shields.io/github/last-commit/work-mate/nuxt-auth-module) -
-Auth module for Nuxt 3 & 4 apps. - -## Featured Auth Providers - -| Provider | Provider Key | Status | -| -------- | ------------ | ------------------ | -| Local | local | :white_check_mark: | -| Google | google | :white_check_mark: | -| Github | github | :white_check_mark: | -| Facebook | facebook | :construction: | -| LinkedIn | linkedin | :construction: | +Auth module for Nuxt 3 & 4 apps. Supports local credentials, GitHub OAuth, and Google OAuth with Zod schema validation. -## Local Auth Features +## Providers -| Feature | Status | -| ------------- | ------------------ | -| Login | :white_check_mark: | -| Logout | :white_check_mark: | -| User | :white_check_mark: | -| Refresh Token | :white_check_mark: | - - +| Provider | Key | Status | +| -------- | -------- | ------ | +| Local | local | ✅ | +| GitHub | github | ✅ | +| Google | google | ✅ | +| Facebook | facebook | 🚧 | +| LinkedIn | linkedin | 🚧 | ## Installation -#### Package Manager - ```bash -# using npm -npm install --save @workmate/nuxt-auth - -# using yarn -yarn add @workmate/nuxt-auth +npm install @workmate/nuxt-auth zod +# or +yarn add @workmate/nuxt-auth zod ``` -## Setup - -### Add to modules +Add to `nuxt.config.ts`: ```ts -// nuxt.config.ts export default defineNuxtConfig({ - modules: [ - "@workmate/nuxt-auth" - ], - ... + modules: ["@workmate/nuxt-auth"], }); ``` -### Configure Auth +--- + +## Configuration ```ts // nuxt.config.ts +import { z } from "zod"; + +const userSchema = z.object({ + id: z.string(), + email: z.email(), + name: z.string().optional(), +}); + export default defineNuxtConfig({ modules: ["@workmate/nuxt-auth"], auth: { + global: false, + defaultProvider: "local", + redirects: { + redirectIfNotLoggedIn: "/login", + redirectIfLoggedIn: "/", + }, + apiClient: { + baseURL: "http://localhost:8080", + }, + token: { + type: "Bearer", + maxAge: 1000 * 60 * 60 * 24 * 30, + cookiesNames: { + accessToken: "auth:token", + refreshToken: "auth:refreshToken", + authProvider: "auth:provider", + tokenType: "auth:tokenType", + }, + }, providers: { local: { endpoints: { signIn: { - path: "/signin", + path: "/api/auth/login/password", + method: "POST", + tokenKey: "token", + refreshTokenKey: "refresh_token", + }, + signOut: { path: "/api/auth/logout", method: "POST" }, + user: { path: "/api/auth/user", userKey: "user" }, + refreshToken: { + path: "/api/auth/refresh", method: "POST", tokenKey: "token", - body: { - principal: "username", - password: "password", - }, + refreshTokenKey: "refresh_token", + body: { token: "token", refreshToken: "refresh_token" }, }, }, + schemas: { + login: z.object({ + email_address: z.email(), + password: z.string().min(8), + }), + user: userSchema, + }, }, github: { CLIENT_ID: process.env.GITHUB_CLIENT_ID || "", CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET || "", - HASHING_SECRET: process.env.HASHING_SECRET || "secret", + HASHING_SECRET: process.env.HASHING_SECRET || "", SCOPES: "user repo", + schemas: { user: userSchema }, }, google: { CLIENT_ID: process.env.GOOGLE_CLIENT_ID || "", CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET || "", - HASHING_SECRET: process.env.HASHING_SECRET || "secret", + HASHING_SECRET: process.env.HASHING_SECRET || "", SCOPES: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", - }, - }, - global: false, - redirects: { - redirectIfNotLoggedIn: "/login", - redirectIfLoggedIn: "/", - }, - apiClient: { - baseURL: "", - }, - defaultProvider: "local", - token: { - type: "Bearer", - maxAge: 1000 * 60 * 60 * 24 * 30, - cookiesNames: { - accessToken: "auth:token", - refreshToken: "auth:refreshToken", - authProvider: "auth:provider", - tokenType: "auth:tokenType", + schemas: { user: userSchema }, }, }, }, }); ``` -### Full List of Module Options +### Module options reference ```ts interface ModuleOptions { - providers: ModuleProvidersOptions; global: boolean; defaultProvider?: string; redirects: { @@ -142,16 +135,16 @@ interface ModuleOptions { accessToken: string; refreshToken: string; authProvider: string; + tokenType: string; }; }; + providers: { + local?: LocalAuthInitializerOptions; + github?: GithubAuthInitializerOptions; + google?: GoogleAuthInitializerOptions; + }; } -type ModuleProvidersOptions = { - local?: LocalAuthInitializerOptions; - github?: GithubAuthInitializerOptions; - google?: GoogleAuthInitializerOptions; -}; - type HttpMethod = | "GET" | "HEAD" @@ -170,10 +163,6 @@ type LocalAuthInitializerOptions = { method?: HttpMethod; tokenKey?: string; refreshTokenKey?: string; - body?: { - principal?: string; - password?: string; - }; }; signOut?: { path: string; method: HttpMethod } | false; signUp?: { path?: string; method?: HttpMethod } | false; @@ -184,13 +173,14 @@ type LocalAuthInitializerOptions = { method: HttpMethod; tokenKey: string; refreshTokenKey: string; - body: { - token: string; - refreshToken: string; - }; + body: { token: string; refreshToken: string }; } | false; }; + schemas?: { + login?: ZodType; + user?: ZodType; + }; }; type GithubAuthInitializerOptions = { @@ -198,6 +188,7 @@ type GithubAuthInitializerOptions = { CLIENT_SECRET: string; HASHING_SECRET: string; SCOPES?: string; + schemas?: { user?: ZodType }; }; type GoogleAuthInitializerOptions = { @@ -205,206 +196,369 @@ type GoogleAuthInitializerOptions = { CLIENT_SECRET: string; HASHING_SECRET: string; SCOPES?: string; + schemas?: { user?: ZodType }; }; ``` -## Usage +--- + +## Server routes + +The module registers these routes on your Nuxt server: + +| Method | Path | Description | +| ------ | ------------------------- | ------------------------------------- | +| POST | /api/auth/login | Login with any provider | +| POST | /api/auth/logout | Clear session and cookies | +| GET | /api/auth/user | Get the current authenticated user | +| POST | /api/auth/refresh | Refresh access token | +| GET | /api/auth/callback/github | GitHub OAuth callback (if configured) | +| GET | /api/auth/callback/google | Google OAuth callback (if configured) | + +### POST /api/auth/login + +```ts +// Request body +{ + provider: string; // "local" | "github" | "google" + [key: string]: any; // provider-specific fields (e.g. email_address, password) +} + +// Response — local provider +{ + tokens: { + accessToken: string; + refreshToken?: string; + tokenType: string; + provider: string; + }; +} + +// Response — OAuth providers +{ + url: string; // redirect to this URL to begin OAuth flow +} +``` + +### POST /api/auth/logout + +No request body. Clears all auth cookies and calls the provider's logout endpoint if configured. + +### GET /api/auth/user -#### While using the social auth (google, github) +No request body. Reads token from cookies. -Add the callback URL to the auth provider configuration. For example: `/api/auth/callback/` for google it would be `/api/auth/callback/google`. +```ts +// Response +{ + user: AuthUser | null; +} +``` + +### POST /api/auth/refresh -#### composables +No request body. Reads tokens from cookies, calls the provider's refresh endpoint. + +```ts +// Response +{ + tokens: { + accessToken: string; + refreshToken?: string; + tokenType: string; + provider: string; + }; +} +``` + +### OAuth callbacks + +Add `/api/auth/callback/github` and `/api/auth/callback/google` as the callback URLs in your OAuth app settings. + +--- + +## Composables + +All composables are auto-imported. + +### `useAuth()` + +Low-level access to the full auth plugin. Prefer the focused composables below for most use cases. ```ts const { - loggedIn, + isLoggedIn, user, token, refreshToken, + tokenType, + provider, + tokenNames, login, logout, refreshUser, refreshTokens, } = useAuth(); -type AuthUser = any; - -// state is of type AuthState -type AuthPlugin = { - loggedIn: ComputedRef; - user: ComputedRef; - token: ComputedRef; - refreshToken: ComputedRef; - tokenType: ComputedRef; - provider: ComputedRef; - login: ( - provider: string | SupportedAuthProvider, - data?: Record, - redirectTo?: string, - ) => Promise< - | { - tokens: AccessTokens; - } - | { - message: string; - } - >; - logout: (redirectTo?: string) => Promise; - refreshUser: () => Promise; - refreshTokens: () => Promise; -}; +// login(provider, data?, redirectTo?) +await login("local", { email_address: "me@example.com", password: "secret" }); +await login("github"); + +// logout(redirectTo?) +await logout("/login"); ``` -To use useFetch with the authorization header, use `useAuthFetch`, and instead of using $fetch use $authFetch +### `useAuthLogin()` + +Typed login functions. The local provider accepts any object — whatever you pass is sent directly to your `signIn` endpoint. If `schemas.login` is configured, the object is validated against it first; if not, it is forwarded as-is. You control the field names. ```ts -useAuthFetch("/api/auth/melting", options); +const { localLogin, googleLogin, githubLogin } = useAuthLogin(); + +// localLogin(opts, redirectTo?) +// opts shape comes entirely from your schemas.login Zod schema (or Record if no schema) +await localLogin({ email_address: "me@example.com", password: "secret123" }); +await localLogin({ username: "alice", pin: "1234" }); // different backend, different fields +await localLogin( + { email_address: "me@example.com", password: "secret123" }, + "/dashboard", +); + +// googleLogin(opts?, redirectTo?) +await googleLogin(); +await googleLogin({ redirectUrl: "/dashboard" }); + +// githubLogin(opts?, redirectTo?) +await githubLogin(); +``` -// instead of $fetch, -$fetch("/api/auth/melting", options); +### `useAuthUser()` -// use, -const { $authFetch } = useNuxtApp(); -$authFetch("/api/auth/melting", options); +```ts +const { user, isLoggedIn, refreshUser } = useAuthUser(); + +// user: ComputedRef +// isLoggedIn: ComputedRef + +if (isLoggedIn.value) { + console.log(user.value); +} + +await refreshUser(); // re-fetches user from the server ``` -to change the base URI of the authFetch client, update the nuxt.config.ts +### `useAuthToken()` ```ts -// nuxt.config.ts -export default defineNuxtConfig({ - auth: { - ... - apiClient: { - baseURL: "http://localhost:8080/v1", - }, - ... - }, -}) +const { token, refreshToken, tokenType, provider, tokenNames, refreshTokens } = + useAuthToken(); + +// All values are computed refs +console.log(token.value); // current access token +console.log(provider.value); // "local" | "github" | "google" +await refreshTokens(); // exchange refresh token for new tokens ``` -#### Logging in +### `useAuthFetch()` + +Wrapper around Nuxt's `useFetch` that automatically injects the Authorization header. Retries once on 401 after refreshing tokens. ```ts -const { login } = useAuth(); +const { data, error } = await useAuthFetch("/api/profile"); -// to login -login("local", { - principal, - password, +// Supports all useFetch options +const { data } = await useAuthFetch("/api/profile", { + method: "POST", + body: { name: "Alice" }, }); +``` + +--- + +## Plugins + +### `$auth` + +The full auth plugin, same as `useAuth()`. + +```ts +const { $auth } = useNuxtApp(); + +if ($auth.isLoggedIn.value) { + await $auth.logout(); +} +``` + +### `$authFetch` + +An `ofetch` instance pre-configured with your `apiClient.baseURL`, the current Authorization header, and automatic token refresh on 401. Use this instead of `$fetch` for authenticated requests. + +```ts +const { $authFetch } = useNuxtApp(); + +const data = await $authFetch("/api/protected"); +``` -// using github -login("github"); +To set the base URL: + +```ts +auth: { + apiClient: { + baseURL: "http://localhost:8080/v1", + }, +} ``` -## middlewares +--- + +## Middlewares ### Route middleware -To protect a page +Protect a page (requires login): ```ts -// pages/index.vue +// pages/dashboard.vue definePageMeta({ middleware: "auth", }); - -// or if adding it to a list of middlewares -definePageMeta({ - middleware: [..., "auth", ...], -}); ``` -To make sure a page is only accessible if you are not logged in. Eg. a login page +Guest-only page (redirect away if logged in): ```ts // pages/login.vue definePageMeta({ middleware: "auth-guest", }); - -// or if adding it to a list of middlewares -definePageMeta({ - middleware: [..., "auth-guest", ...], -}); ``` ### Global middleware -For a global middleware, set the the `auth.global` to true in the `nuxt.config.ts` file +Apply auth to all routes: ```ts -export default defineNuxtConfig({ - ... - auth: { - global: true, - ... - }, -}) +// nuxt.config.ts +auth: { + global: true, +} ``` -To prevent a page from being protected from auth, set the auth meta to false +Opt a page out when global is enabled: ```ts -// pages/index.vue definePageMeta({ auth: false, }); ``` -Example of nuxt.config.ts +Redirects are configured under `auth.redirects`: + +- `redirectIfNotLoggedIn` — where `auth` middleware sends unauthenticated users (default: `"/login"`) +- `redirectIfLoggedIn` — where `auth-guest` middleware sends authenticated users (default: `"/"`) + +--- + +## Schema validation + +Schemas are defined in `nuxt.config.ts` per provider using Zod 4. They are converted to JSON Schema at build time and reconstructed at runtime — no serialization issues. ```ts -const BACKEND_URL = "http://localhost:8080/api/v1"; -export default defineNuxtConfig({ - modules: ["@workmate/nuxt-auth"], - auth: { - global: true, - redirects: { - redirectIfLoggedIn: "/protected", - }, - apiClient: { - baseURL: BACKEND_URL, - }, - providers: { - local: { - endpoints: { - user: { - path: BACKEND_URL + "/api/auth/user", - userKey: "user", - }, - signIn: { - path: BACKEND_URL + "/api/auth/login/password", - body: { - principal: "email_address", - password: "password", - }, - tokenKey: "token", - refreshTokenKey: "refresh_token", - }, - refreshToken: { - path: BACKEND_URL + "/api/auth/refresh", - method: "POST", - tokenKey: "token", - refreshTokenKey: "refresh_token", - body: { - token: "token", - refreshToken: "refresh_token", - }, - }, - }, - }, +// nuxt.config.ts +import { z } from "zod"; - github: { - CLIENT_ID: process.env.GITHUB_CLIENT_ID || "", - CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET || "", - HASHING_SECRET: process.env.HASHING_SECRET || "secret", - SCOPES: "user repo", +local: { + schemas: { + login: z.object({ email_address: z.email(), password: z.string().min(8) }), + user: z.object({ id: z.string(), email: z.email(), name: z.string().optional() }), + }, +} +``` + +**`schemas.login`** — the local provider sends whatever object you pass directly to your `signIn` endpoint. There are no fixed field names — use whatever your backend expects (`email_address`, `username`, `phone`, etc.). If `schemas.login` is configured, the body is validated server-side before being forwarded. Invalid bodies return HTTP 400 with field errors: + +```json +{ + "message": "Validation error", + "data": { "email_address": ["Invalid email"] } +} +``` + +If `schemas.login` is not configured, the body is forwarded as-is with no validation. + +**`schemas.user`** — validated server-side when your backend returns user data. Mismatches throw a server error with field-level detail. Optional — omit to skip validation. + +TypeScript infers the login payload type from your schema — `localLogin(opts)` is typed as the Zod output of `schemas.login`, or `Record` if no schema is set. + +--- + +## Migration: v1 → v2 (breaking changes in v2.0.0) + +### Configuration + +In v1, `signIn.body` mapped fixed field names (`principal`, `password`) onto the request. In v2 the body is open — you pass any object and it is sent as-is to your endpoint. Remove `signIn.body` entirely and add `schemas.login` to describe the shape your backend expects. + +**Before:** + +```ts +local: { + endpoints: { + signIn: { + path: "/api/auth/login", + tokenKey: "token", + body: { + principal: "email_address", + password: "password", }, }, }, -}); +} +``` + +**After:** + +```ts +import { z } from "zod"; + +local: { + endpoints: { + signIn: { + path: "/api/auth/login", + tokenKey: "token", + }, + }, + schemas: { + login: z.object({ + email_address: z.email(), + password: z.string().min(8), + }), + }, +} ``` + +### Login call + +**Before:** + +```ts +const { login } = useAuth(); +await login("local", { principal: values.email, password: values.password }); +``` + +**After:** + +```ts +const { localLogin } = useAuthLogin(); +await localLogin({ email_address: values.email, password: values.password }); +``` + +### Peer dependency + +Zod 4 is now required (`zod@^4.0.0`). If you were on Zod 3, upgrade: + +```bash +npm install zod@^4.0.0 +``` + +`z.toJSONSchema` and `z.fromJSONSchema` do not exist on Zod 3. diff --git a/eslint.config.js b/eslint.config.js index 16f3b39..d300a2b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -26,7 +26,7 @@ export default createConfigForNuxt({ // Allow any component names in playground 'vue/multi-word-component-names': 'off', // Allow unused variables with underscore prefix - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], // Allow any types in certain contexts (auth module complexity) '@typescript-eslint/no-explicit-any': 'off', // Allow empty interfaces for extensibility diff --git a/package-lock.json b/package-lock.json index 5c01b94..672d1ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "defu": "^6.1.4", "h3": "^1.11.1", "jsonwebtoken": "^9.0.2", - "ofetch": "^1.4.1" + "ofetch": "^1.4.1", + "zod": "4.4.3" }, "devDependencies": { "@nuxt/devtools": "latest", @@ -6160,13 +6161,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz", - "integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==", + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/before-after-hook": { @@ -6412,9 +6416,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001746", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001746.tgz", - "integrity": "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==", + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", "dev": true, "funding": [ { @@ -19187,6 +19191,15 @@ "engines": { "node": ">= 14" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index fb87893..58cafd6 100644 --- a/package.json +++ b/package.json @@ -1,66 +1,38 @@ { "name": "@workmate/nuxt-auth", - "version": "1.5.1", - "description": "Nuxt Auth Module: Auth module for Nuxt 3 apps", + "version": "2.0.0", + "description": "Authentication module for Nuxt 3 and Nuxt 4 with local credentials, GitHub, and Google OAuth — Zod schema validation, refresh tokens, cookie management, route middleware, and typed composables.", "repository": "https://github.com/work-mate/nuxt-auth-module", "license": "MIT", "type": "module", "keywords": [ - "@workmate/nuxt-auth", - "Nuxt 3", + "nuxt", + "nuxt3", + "nuxt4", + "nuxt-module", "authentication", "authorization", - "local authentication", - "Github authentication", - "refresh tokens", - "access tokens", + "oauth", + "local-auth", + "github-auth", + "google-auth", + "refresh-token", + "access-token", + "jwt", "cookies", + "zod", + "schema-validation", "middleware", + "route-middleware", "composables", - "fetch", - "authFetch", - "login", - "logout", - "refreshUser", - "refreshTokens", - "loggedIn", - "user", - "token", - "refreshToken", - "provider", - "tokenType", - "auth middleware", - "auth-guest middleware", - "global middleware", + "useAuth", + "useAuthUser", + "useAuthFetch", + "auth-middleware", + "auth-guest-middleware", "redirects", - "apiClient", - "token", - "cookiesNames", - "defaultProvider", - "GitHub", - "Google", - "Facebook", - "local", - "login", - "logout", - "user", - "refresh token", - "construction", - "package manager", - "npm", - "yarn", - "installation", - "setup", - "add to modules", - "configure auth", - "full list of module options", - "usage", - "composables", - "logging in", - "middlewares", - "route middleware", - "global middleware", - "example of nuxt.config.ts" + "multi-provider", + "typescript" ], "exports": { ".": { @@ -72,6 +44,11 @@ "files": [ "dist" ], + "vitest": { + "sequence": { + "concurrent": false + } + }, "scripts": { "prepack": "nuxt-module-build build", "dev": "nuxi dev playground", @@ -79,6 +56,7 @@ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground", "release": "npm run lint && npm run test && npm run prepack && npm publish --access public && git push --follow-tags", "lint": "eslint .", + "lint:fix": "eslint . --fix", "test": "vitest run", "test:watch": "vitest watch" }, @@ -87,7 +65,8 @@ "defu": "^6.1.4", "h3": "^1.11.1", "jsonwebtoken": "^9.0.2", - "ofetch": "^1.4.1" + "ofetch": "^1.4.1", + "zod": "4.4.3" }, "devDependencies": { "@nuxt/devtools": "latest", diff --git a/playground/components/TestNavigations.vue b/playground/components/TestNavigations.vue index be782bc..8f91af7 100644 --- a/playground/components/TestNavigations.vue +++ b/playground/components/TestNavigations.vue @@ -1,40 +1,20 @@