├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── .nuxtrc ├── README.md ├── middleware.d.ts ├── package.json ├── playground ├── app.vue ├── components │ ├── Form.vue │ └── Header.vue ├── composables │ └── useAuth.ts ├── middleware │ ├── protected.ts │ └── public.ts ├── nuxt.config.ts ├── package.json ├── pages │ ├── index.vue │ ├── login.vue │ └── profile.vue ├── public │ └── GitHub-Mark-32px.png └── server │ └── api │ ├── events.get.ts │ ├── login.post.ts │ └── logout.get.ts ├── pnpm-lock.yaml ├── src ├── module.ts └── runtime │ ├── handler.ts │ └── middleware.ts ├── tsconfig.json └── vercel.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | package.json 4 | tsconfig.json 5 | vercel.json 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs/eslint-config-typescript" 4 | ], 5 | "rules": { 6 | "@typescript-eslint/no-unused-vars": [ 7 | "off" 8 | ], 9 | "vue/multi-word-component-names": "off", 10 | "vue/no-multiple-template-root": "off" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 16.x 19 | 20 | - run: npx changelogithub 21 | env: 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode 38 | 39 | # Intellij idea 40 | *.iml 41 | .idea 42 | 43 | # OSX 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | 53 | .vercel 54 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | imports.autoImport=true 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nuxt-iron-session 2 | 3 | [![Version](https://img.shields.io/npm/v/nuxt-iron-session?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/nuxt-iron-session) 4 | 5 | 🛠 Add stateless session support for Nuxt apps using signed and encrypted cookies. Powered by [iron-session](https://github.com/vvo/iron-session). 6 | 7 | The session data is stored in encrypted cookies ("seals"). And only your server can decode the session data. There are no session ids, making iron sessions "stateless" from the server point of view. 8 | 9 | Demo https://nuxt-iron-session.vercel.app 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install nuxt-iron-session 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```ts 20 | // nuxt.config.ts 21 | export default defineNuxtConfig({ 22 | modules: ['nuxt-iron-session'], 23 | session: { 24 | cookieName: 'yourapp_cookiename', 25 | password: 'complex_password_at_least_32_characters_long', 26 | cookieOptions: { 27 | secure: process.env.NODE_ENV === 'production' 28 | } 29 | } 30 | }) 31 | ``` 32 | 33 | ### API Routes 34 | 35 | ```ts 36 | // ~/server/api/login.ts 37 | export default defineEventHandler((event) => { 38 | // get user from database then: 39 | event.context.session.user = { 40 | id: 69, 41 | admin: true, 42 | } 43 | await event.context.session.save() 44 | return { ok: true } 45 | }) 46 | ``` 47 | 48 | ```ts 49 | // ~/server/api/user.ts 50 | export default defineEventHandler((event) => { 51 | return { user: event.context.session.user } 52 | }) 53 | ``` 54 | 55 | ```ts 56 | // ~/server/api/logout.ts 57 | export default defineEventHandler((event) => { 58 | await event.context.session.destroy() 59 | return { ok: true } 60 | }) 61 | ``` 62 | 63 | ### Components 64 | 65 | ```html 66 | 72 | ``` 73 | 74 | ## Typing session data with TypeScript 75 | 76 | ```ts 77 | declare module 'iron-session' { 78 | interface IronSessionData { 79 | user?: { 80 | id: number 81 | admin?: boolean 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | ## Usage with h3 88 | 89 | ```ts 90 | import { createIronSessionMiddleware } from 'nuxt-iron-session/middleware' 91 | 92 | const app = createApp() 93 | 94 | app.use(createIronSessionMiddleware({})) 95 | app.use('/api/user', eventHandler((event) => ({ user: event.context.session.user }))) 96 | ``` 97 | 98 | Visit the [iron-session docs](https://github.com/vvo/iron-session) to see the complete configuration. 99 | 100 | ## Development 101 | 102 | - Run `npm run dev:prepare` to generate type stubs. 103 | - Use `npm run dev` to start [playground](./playground) in development mode. 104 | 105 | ## License 106 | 107 | MIT 108 | -------------------------------------------------------------------------------- /middleware.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/runtime/middleware' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-iron-session", 3 | "version": "0.2.0", 4 | "type": "module", 5 | "author": "Robert Soriano ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/wobsoriano/nuxt-iron-session#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/wobsoriano/nuxt-iron-session.git" 11 | }, 12 | "bugs": "https://github.com/wobsoriano/nuxt-iron-session/issues", 13 | "keywords": [ 14 | "session", 15 | "nuxt", 16 | "h3", 17 | "middleware" 18 | ], 19 | "exports": { 20 | ".": { 21 | "import": "./dist/module.mjs", 22 | "require": "./dist/module.cjs" 23 | }, 24 | "./middleware": { 25 | "import": "./dist/runtime/middleware.mjs", 26 | "types": "./dist/runtime/middleware.d.ts" 27 | } 28 | }, 29 | "main": "./dist/module.cjs", 30 | "types": "./dist/types.d.ts", 31 | "files": [ 32 | "dist", 33 | "middleware.d.ts" 34 | ], 35 | "scripts": { 36 | "prepublishOnly": "pnpm prepack", 37 | "lint": "eslint .", 38 | "prepack": "nuxt-module-build", 39 | "dev": "nuxi dev playground", 40 | "dev:build": "nuxi build playground", 41 | "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground", 42 | "dev:vercel:deploy": "NITRO_PRESET=vercel rm -rf .vercel && pnpm dev:prepare && pnpm prepack && pnpm dev:build && mv ./playground/.vercel .", 43 | "release": "bumpp && npm publish" 44 | }, 45 | "dependencies": { 46 | "@nuxt/kit": "^3.5.0", 47 | "defu": "^6.1.2", 48 | "iron-session": "^6.3.1" 49 | }, 50 | "devDependencies": { 51 | "@nuxt/module-builder": "^0.3.1", 52 | "@nuxt/schema": "^3.5.0", 53 | "@nuxtjs/eslint-config-typescript": "^12.0.0", 54 | "bumpp": "^9.1.0", 55 | "eslint": "^8.41.0", 56 | "nuxt": "^3.5.0", 57 | "octokit": "^2.0.16" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 37 | -------------------------------------------------------------------------------- /playground/components/Form.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | 32 | 55 | -------------------------------------------------------------------------------- /playground/components/Header.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 50 | 51 | 85 | -------------------------------------------------------------------------------- /playground/composables/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useNuxtApp, useState } from '#imports' 2 | 3 | interface User { 4 | isLoggedIn: boolean; 5 | login: string; 6 | avatarUrl: string; 7 | } 8 | 9 | export default function useAuth () { 10 | const { ssrContext } = useNuxtApp() 11 | const user = useState('user', () => ssrContext?.event?.context?.session?.user) 12 | 13 | async function login (username: string) { 14 | const result = await $fetch('/api/login', { 15 | method: 'POST', 16 | body: { 17 | username 18 | } 19 | }) 20 | 21 | user.value = result 22 | } 23 | 24 | async function logout () { 25 | const resp = await $fetch('/api/logout') 26 | user.value = resp 27 | } 28 | 29 | return { 30 | user, 31 | login, 32 | logout 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /playground/middleware/protected.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtRouteMiddleware, navigateTo, useAuth } from '#imports' 2 | 3 | export default defineNuxtRouteMiddleware(() => { 4 | const { user } = useAuth() 5 | 6 | if (!user.value?.isLoggedIn) { 7 | return navigateTo('/login') 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /playground/middleware/public.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtRouteMiddleware, navigateTo, useAuth } from '#imports' 2 | 3 | export default defineNuxtRouteMiddleware(() => { 4 | const { user } = useAuth() 5 | 6 | if (user.value?.isLoggedIn) { 7 | return navigateTo('/profile') 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | import MyModule from '..' 3 | 4 | export default defineNuxtConfig({ 5 | modules: [ 6 | MyModule 7 | ], 8 | myModule: { 9 | addPlugin: true 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "my-module-playground" 4 | } 5 | -------------------------------------------------------------------------------- /playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 40 | 41 | 46 | -------------------------------------------------------------------------------- /playground/pages/login.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 28 | 29 | 38 | -------------------------------------------------------------------------------- /playground/pages/profile.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | -------------------------------------------------------------------------------- /playground/public/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-iron-session/84855607eb3116bac36e50cac534a1f03edfd204/playground/public/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /playground/server/api/events.get.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from 'octokit' 2 | 3 | const octokit = new Octokit() 4 | 5 | export default defineEventHandler(async (event) => { 6 | const user = event.context.session.user 7 | 8 | if (!user || user.isLoggedIn === false) { 9 | throw createError({ 10 | status: 401, 11 | statusMessage: 'Not authenticated' 12 | }) 13 | } 14 | 15 | try { 16 | const { data: events } = 17 | await octokit.rest.activity.listPublicEventsForUser({ 18 | username: user.login 19 | }) 20 | 21 | return events 22 | } catch { 23 | return [] 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /playground/server/api/login.post.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from 'octokit' 2 | 3 | const octokit = new Octokit() 4 | 5 | export default defineEventHandler(async (event) => { 6 | const { username } = await readBody<{ username: string }>(event) 7 | 8 | try { 9 | const { 10 | data: { login, avatar_url: avatarUrl } 11 | } = await octokit.rest.users.getByUsername({ username }) 12 | 13 | const user = { isLoggedIn: true, login, avatarUrl } 14 | 15 | event.context.session.user = user 16 | await event.context.session.save() 17 | 18 | return user 19 | } catch (error) { 20 | if (error.status === 404) { 21 | throw createError({ 22 | status: 404, 23 | message: error.response.data.message 24 | }) 25 | } 26 | 27 | throw createError({ 28 | status: 500, 29 | message: 'Server error' 30 | }) 31 | } 32 | }) 33 | 34 | declare module 'iron-session' { 35 | interface IronSessionData { 36 | user?: { 37 | isLoggedIn: boolean; 38 | login: string; 39 | avatarUrl: string; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /playground/server/api/logout.get.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | await event.req.session.destroy() 3 | return { 4 | isLoggedIn: false, 5 | login: '', 6 | avatarUrl: '' 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url' 2 | import { defineNuxtModule, addServerHandler, addTemplate, createResolver } from '@nuxt/kit' 3 | import { defu } from 'defu' 4 | import { IronSessionOptions } from 'iron-session' 5 | 6 | export default defineNuxtModule({ 7 | meta: { 8 | name: 'nuxt-iron-session', 9 | configKey: 'session', 10 | compatibility: { 11 | nuxt: '^3.5.0' 12 | } 13 | }, 14 | defaults: { 15 | cookieName: 'nuxtapp_cookiename', 16 | password: 'complex_password_at_least_32_characters_long', 17 | // secure: true should be used in production (HTTPS) but can't be used in development (HTTP) 18 | cookieOptions: { 19 | secure: process.env.NODE_ENV === 'production' 20 | } 21 | }, 22 | setup (moduleOptions, nuxt) { 23 | const { resolve } = createResolver(import.meta.url) 24 | 25 | // Private runtimeConfig 26 | nuxt.options.runtimeConfig.session = defu(nuxt.options.runtimeConfig.session, moduleOptions) 27 | 28 | // Transpile runtime 29 | const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url)) 30 | nuxt.options.build.transpile.push(runtimeDir) 31 | 32 | addServerHandler({ 33 | handler: resolve(runtimeDir, 'handler'), 34 | middleware: true 35 | }) 36 | 37 | addTemplate({ 38 | filename: 'types/iron-session.d.ts', 39 | getContents: () => ` 40 | declare module 'h3' { 41 | import type { IronSession } from 'iron-session' 42 | interface H3EventContext { 43 | session: IronSession 44 | } 45 | } 46 | 47 | export {} 48 | ` 49 | }) 50 | 51 | nuxt.hook('nitro:config', (nitroConfig) => { 52 | // Inline module runtime in Nitro bundle 53 | nitroConfig.externals = defu(typeof nitroConfig.externals === 'object' ? nitroConfig.externals : {}, { 54 | inline: [resolve('./runtime')] 55 | }) 56 | }) 57 | 58 | nuxt.hook('prepare:types', (options) => { 59 | options.references.push({ path: resolve(nuxt.options.buildDir, 'types/iron-session.d.ts') }) 60 | }) 61 | } 62 | }) 63 | -------------------------------------------------------------------------------- /src/runtime/handler.ts: -------------------------------------------------------------------------------- 1 | import { createIronSessionMiddleware } from './middleware' 2 | // @ts-expect-error: Nuxt generated 3 | import { useRuntimeConfig } from '#imports' 4 | 5 | const config = useRuntimeConfig() 6 | 7 | export default createIronSessionMiddleware(config.session) 8 | -------------------------------------------------------------------------------- /src/runtime/middleware.ts: -------------------------------------------------------------------------------- 1 | import { eventHandler } from 'h3' 2 | import type { IronSession, IronSessionOptions } from 'iron-session' 3 | import { getIronSession } from 'iron-session' 4 | 5 | // https://github.com/vvo/iron-session/blob/main/src/getPropertyDescriptorForReqSession.ts 6 | function getPropertyDescriptorForReqSession ( 7 | session: IronSession 8 | ): PropertyDescriptor { 9 | return { 10 | enumerable: true, 11 | get () { 12 | return session 13 | }, 14 | set (value) { 15 | const keys = Object.keys(value) 16 | const currentKeys = Object.keys(session) 17 | 18 | currentKeys.forEach((key) => { 19 | if (!keys.includes(key)) { 20 | delete session[key] 21 | } 22 | }) 23 | 24 | keys.forEach((key) => { 25 | session[key] = value[key] 26 | }) 27 | } 28 | } 29 | } 30 | 31 | export function createIronSessionMiddleware (options: IronSessionOptions) { 32 | return eventHandler(async (event) => { 33 | const session = await getIronSession(event.node.req, event.node.res, options) 34 | 35 | Object.defineProperty( 36 | event.node.req, 37 | 'session', 38 | getPropertyDescriptorForReqSession(session) 39 | ) 40 | 41 | Object.defineProperty( 42 | event.context, 43 | 'session', 44 | getPropertyDescriptorForReqSession(session) 45 | ) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./playground/.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | --------------------------------------------------------------------------------