nuxt-auth-utils 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +7 -0
- package/dist/module.d.ts +7 -0
- package/dist/module.json +5 -0
- package/dist/module.mjs +52 -0
- package/dist/runtime/composables/session.d.ts +10 -0
- package/dist/runtime/composables/session.mjs +19 -0
- package/dist/runtime/plugins/session.server.d.ts +2 -0
- package/dist/runtime/plugins/session.server.mjs +9 -0
- package/dist/runtime/server/api/session.delete.d.ts +4 -0
- package/dist/runtime/server/api/session.delete.mjs +6 -0
- package/dist/runtime/server/api/session.get.d.ts +2 -0
- package/dist/runtime/server/api/session.get.mjs +5 -0
- package/dist/runtime/server/lib/oauth/github.d.ts +45 -0
- package/dist/runtime/server/lib/oauth/github.mjs +82 -0
- package/dist/runtime/server/lib/oauth/spotify.d.ts +45 -0
- package/dist/runtime/server/lib/oauth/spotify.mjs +76 -0
- package/dist/runtime/server/utils/oauth.d.ts +6 -0
- package/dist/runtime/server/utils/oauth.mjs +6 -0
- package/dist/runtime/server/utils/session.d.ts +14 -0
- package/dist/runtime/server/utils/session.mjs +33 -0
- package/dist/types.d.mts +15 -0
- package/dist/types.d.ts +15 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Sébastien Chopin
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Nuxt Auth Utils
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
|
+
[![License][license-src]][license-href]
|
|
6
|
+
[![Nuxt][nuxt-src]][nuxt-href]
|
|
7
|
+
|
|
8
|
+
Minimalist Authentication module for Nuxt exposing Vue composables and server utils.
|
|
9
|
+
|
|
10
|
+
- [✨ Release Notes](/CHANGELOG.md)
|
|
11
|
+
<!-- - [🏀 Online playground](https://stackblitz.com/github/your-org/nuxt-auth-utils?file=playground%2Fapp.vue) -->
|
|
12
|
+
<!-- - [📖 Documentation](https://example.com) -->
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Secured & sealed cookies sessions
|
|
17
|
+
- OAuth Providers
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
This module only works with SSR (server-side rendering) enabled as it uses server API routes. You cannot use this module with `nuxt generate`.
|
|
22
|
+
|
|
23
|
+
## Quick Setup
|
|
24
|
+
|
|
25
|
+
1. Add `nuxt-auth-utils` dependency to your project
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Using pnpm
|
|
29
|
+
pnpm add -D nuxt-auth-utils
|
|
30
|
+
|
|
31
|
+
# Using yarn
|
|
32
|
+
yarn add --dev nuxt-auth-utils
|
|
33
|
+
|
|
34
|
+
# Using npm
|
|
35
|
+
npm install --save-dev nuxt-auth-utils
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. Add `nuxt-auth-utils` to the `modules` section of `nuxt.config.ts`
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
export default defineNuxtConfig({
|
|
42
|
+
modules: [
|
|
43
|
+
'nuxt-auth-utils'
|
|
44
|
+
]
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
3. Add a `NUXT_SESSION_PASSWORD` env variable with at least 32 characters in the `.env`.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# .env
|
|
52
|
+
NUXT_SESSION_PASSWORD=password-with-at-least-32-characters
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Nuxt Auth Core can generate one for you when running Nuxt in development the first time when no `NUXT_SESSION_PASSWORD` is set.
|
|
56
|
+
|
|
57
|
+
4. That's it! You can now add authentication to your Nuxt app ✨
|
|
58
|
+
|
|
59
|
+
## Vue Composables
|
|
60
|
+
|
|
61
|
+
Nuxt Neo automatically add some plugins to fetch the current user session to let you access it from your Vue components.
|
|
62
|
+
|
|
63
|
+
### User Session
|
|
64
|
+
|
|
65
|
+
```vue
|
|
66
|
+
<script setup>
|
|
67
|
+
const { loggedIn, user, session, clear } = useUserSession()
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<div v-if="loggedIn">
|
|
72
|
+
<h1>Welcome {{ user.login }}!</h1>
|
|
73
|
+
<p>Logged in since {{ session.loggedInAt }}</p>
|
|
74
|
+
<button @click="clear">Logout</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div v-else>
|
|
77
|
+
<h1>Not logged in</h1>
|
|
78
|
+
<a href="/api/auth/github">Login with GitHub</a>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Server Utils
|
|
84
|
+
|
|
85
|
+
The following helpers are auto-imported in your `server/` directory.
|
|
86
|
+
|
|
87
|
+
### Session Management
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// Set a user session, note that this data is encrypted in the cookie but can be decrypted with an API call
|
|
91
|
+
// Only store the data that allow you to recognize an user, but do not store sensitive data
|
|
92
|
+
await setUserSession(event, {
|
|
93
|
+
user: {
|
|
94
|
+
// ... user data
|
|
95
|
+
},
|
|
96
|
+
loggedInAt: new Date()
|
|
97
|
+
// Any extra fields
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Get the current user session
|
|
101
|
+
const session = await getUserSession(event)
|
|
102
|
+
|
|
103
|
+
// Clear the current user session
|
|
104
|
+
await clearUserSession(event)
|
|
105
|
+
|
|
106
|
+
// Require a user session (send back 401 if no `user` key in session)
|
|
107
|
+
const session = await requireUserSession(event)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### OAuth Event Handlers
|
|
111
|
+
|
|
112
|
+
All helpers are exposed from the `oauth` global variable and can be used in your server routes or API routes.
|
|
113
|
+
|
|
114
|
+
The pattern is `oauth.<provider>EventHandler({ onSuccess, config?, onError? })`, example: `oauth.githubEventHandler`.
|
|
115
|
+
|
|
116
|
+
The helper returns an event handler that automatically redirects to the provider authorization page and then call `onSuccess` or `onError` depending on the result.
|
|
117
|
+
|
|
118
|
+
The `config` can be defined directly from the `runtimeConfig` in your `nuxt.config.ts`:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
export default defineNuxtConfig({
|
|
122
|
+
runtimeConfig: {
|
|
123
|
+
oauth: {
|
|
124
|
+
<provider>: {
|
|
125
|
+
clientId: '...',
|
|
126
|
+
clientSecret: '...'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
It can also be set using environment variables:
|
|
134
|
+
- `NUXT_OAUTH_<PROVIDER>_CLIENT_ID`
|
|
135
|
+
- `NUXT_OAUTH_<PROVIDER>_CLIENT_SECRET`
|
|
136
|
+
|
|
137
|
+
Supported providers:
|
|
138
|
+
- GitHub
|
|
139
|
+
- Spotify
|
|
140
|
+
|
|
141
|
+
### Example
|
|
142
|
+
|
|
143
|
+
Example: `~/server/routes/auth/github.get.ts`
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
export default oauth.githubEventHandler({
|
|
147
|
+
async onSuccess(event, { user, tokens }) {
|
|
148
|
+
await setUserSession(event, {
|
|
149
|
+
user: {
|
|
150
|
+
githubId: user.id
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
return sendRedirect(event, '/')
|
|
154
|
+
},
|
|
155
|
+
// Optional, will return a json error and 401 status code by default
|
|
156
|
+
onError(event, error) {
|
|
157
|
+
console.error('GitHub OAuth error:', error)
|
|
158
|
+
return sendRedirect(event, '/')
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Make sure to set the callback URL in your OAuth app settings as `<your-domain>/auth/github`.
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Install dependencies
|
|
170
|
+
npm install
|
|
171
|
+
|
|
172
|
+
# Generate type stubs
|
|
173
|
+
npm run dev:prepare
|
|
174
|
+
|
|
175
|
+
# Develop with the playground
|
|
176
|
+
npm run dev
|
|
177
|
+
|
|
178
|
+
# Build the playground
|
|
179
|
+
npm run dev:build
|
|
180
|
+
|
|
181
|
+
# Run ESLint
|
|
182
|
+
npm run lint
|
|
183
|
+
|
|
184
|
+
# Run Vitest
|
|
185
|
+
npm run test
|
|
186
|
+
npm run test:watch
|
|
187
|
+
|
|
188
|
+
# Release new version
|
|
189
|
+
npm run release
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
<!-- Badges -->
|
|
193
|
+
[npm-version-src]: https://img.shields.io/npm/v/nuxt-auth-utils/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
194
|
+
[npm-version-href]: https://npmjs.com/package/nuxt-auth-utils
|
|
195
|
+
|
|
196
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-auth-utils.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
197
|
+
[npm-downloads-href]: https://npmjs.com/package/nuxt-auth-utils
|
|
198
|
+
|
|
199
|
+
[license-src]: https://img.shields.io/npm/l/nuxt-auth-utils.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
200
|
+
[license-href]: https://npmjs.com/package/nuxt-auth-utils
|
|
201
|
+
|
|
202
|
+
[nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
|
|
203
|
+
[nuxt-href]: https://nuxt.com
|
package/dist/module.cjs
ADDED
package/dist/module.d.ts
ADDED
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addImportsDir, addPlugin, addServerHandler } from '@nuxt/kit';
|
|
2
|
+
import { sha256 } from 'ohash';
|
|
3
|
+
import { defu } from 'defu';
|
|
4
|
+
|
|
5
|
+
const module = defineNuxtModule({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "auth-core",
|
|
8
|
+
configKey: "auth"
|
|
9
|
+
},
|
|
10
|
+
// Default configuration options of the Nuxt module
|
|
11
|
+
defaults: {},
|
|
12
|
+
setup(options, nuxt) {
|
|
13
|
+
const resolver = createResolver(import.meta.url);
|
|
14
|
+
if (!process.env.NUXT_SESSION_PASSWORD) {
|
|
15
|
+
console.warn("No session password set, using a random password, please set NUXT_SESSION_PASSWORD in your .env file with at least 32 chars");
|
|
16
|
+
const randomPassword = sha256(`${Date.now()}${Math.random()}`).slice(0, 32);
|
|
17
|
+
console.log(`NUXT_SESSION_PASSWORD=${randomPassword}`);
|
|
18
|
+
process.env.NUXT_SESSION_PASSWORD = randomPassword;
|
|
19
|
+
}
|
|
20
|
+
addImportsDir(resolver.resolve("./runtime/composables"));
|
|
21
|
+
addPlugin(resolver.resolve("./runtime/plugins/session.server"));
|
|
22
|
+
nuxt.options.nitro.imports = nuxt.options.nitro.imports || {};
|
|
23
|
+
nuxt.options.nitro.imports.dirs = nuxt.options.nitro.imports.dirs || [];
|
|
24
|
+
nuxt.options.nitro.imports.dirs.push(resolver.resolve("./runtime/server/utils"));
|
|
25
|
+
addServerHandler({
|
|
26
|
+
handler: resolver.resolve("./runtime/server/api/session.delete"),
|
|
27
|
+
route: "/api/_auth/session",
|
|
28
|
+
method: "delete"
|
|
29
|
+
});
|
|
30
|
+
addServerHandler({
|
|
31
|
+
handler: resolver.resolve("./runtime/server/api/session.get"),
|
|
32
|
+
route: "/api/_auth/session",
|
|
33
|
+
method: "get"
|
|
34
|
+
});
|
|
35
|
+
const runtimeConfig = nuxt.options.runtimeConfig;
|
|
36
|
+
runtimeConfig.session = defu(runtimeConfig.session, {
|
|
37
|
+
name: "nuxt-session",
|
|
38
|
+
password: ""
|
|
39
|
+
});
|
|
40
|
+
runtimeConfig.oauth = defu(runtimeConfig.oauth, {});
|
|
41
|
+
runtimeConfig.oauth.github = defu(runtimeConfig.oauth.github, {
|
|
42
|
+
clientId: "",
|
|
43
|
+
clientSecret: ""
|
|
44
|
+
});
|
|
45
|
+
runtimeConfig.oauth.spotify = defu(runtimeConfig.oauth.spotify, {
|
|
46
|
+
clientId: "",
|
|
47
|
+
clientSecret: ""
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export { module as default };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useState, computed, useRequestFetch } from "#imports";
|
|
2
|
+
const useSessionState = () => useState("nuxt-session", () => ({}));
|
|
3
|
+
export const useUserSession = () => {
|
|
4
|
+
const sessionState = useSessionState();
|
|
5
|
+
return {
|
|
6
|
+
loggedIn: computed(() => Boolean(sessionState.value.user)),
|
|
7
|
+
user: computed(() => sessionState.value.user || null),
|
|
8
|
+
session: sessionState,
|
|
9
|
+
fetch,
|
|
10
|
+
clear
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
async function fetch() {
|
|
14
|
+
useSessionState().value = await useRequestFetch()("/api/_auth/session").catch(() => ({}));
|
|
15
|
+
}
|
|
16
|
+
async function clear() {
|
|
17
|
+
await $fetch("/api/_auth/session", { method: "DELETE" });
|
|
18
|
+
useSessionState().value = {};
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { H3Event, H3Error } from 'h3';
|
|
2
|
+
export interface OAuthGitHubConfig {
|
|
3
|
+
/**
|
|
4
|
+
* GitHub OAuth Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_GITHUB_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* GitHub OAuth Client Secret
|
|
10
|
+
* @default process.env.NUXT_OAUTH_GITHUB_CLIENT_SECRET
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* GitHub OAuth Scope
|
|
15
|
+
* @default []
|
|
16
|
+
* @see https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps
|
|
17
|
+
* @example ['user:email']
|
|
18
|
+
*/
|
|
19
|
+
scope?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Require email from user, adds the ['user:email'] scope if not present
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
emailRequired?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* GitHub OAuth Authorization URL
|
|
27
|
+
* @default 'https://github.com/login/oauth/authorize'
|
|
28
|
+
*/
|
|
29
|
+
authorizationURL?: string;
|
|
30
|
+
/**
|
|
31
|
+
* GitHub OAuth Token URL
|
|
32
|
+
* @default 'https://github.com/login/oauth/access_token'
|
|
33
|
+
*/
|
|
34
|
+
tokenURL?: string;
|
|
35
|
+
}
|
|
36
|
+
interface OAuthConfig {
|
|
37
|
+
config?: OAuthGitHubConfig;
|
|
38
|
+
onSuccess: (event: H3Event, result: {
|
|
39
|
+
user: any;
|
|
40
|
+
tokens: any;
|
|
41
|
+
}) => Promise<void> | void;
|
|
42
|
+
onError?: (event: H3Event, error: H3Error) => Promise<void> | void;
|
|
43
|
+
}
|
|
44
|
+
export declare function githubEventHandler({ config, onSuccess, onError }: OAuthConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
|
|
2
|
+
import { ofetch } from "ofetch";
|
|
3
|
+
import { withQuery } from "ufo";
|
|
4
|
+
import { defu } from "defu";
|
|
5
|
+
export function githubEventHandler({ config, onSuccess, onError }) {
|
|
6
|
+
return eventHandler(async (event) => {
|
|
7
|
+
config = defu(config, useRuntimeConfig(event).oauth?.github, {
|
|
8
|
+
authorizationURL: "https://github.com/login/oauth/authorize",
|
|
9
|
+
tokenURL: "https://github.com/login/oauth/access_token"
|
|
10
|
+
});
|
|
11
|
+
const { code } = getQuery(event);
|
|
12
|
+
if (!config.clientId || !config.clientSecret) {
|
|
13
|
+
const error = createError({
|
|
14
|
+
statusCode: 500,
|
|
15
|
+
message: "Missing NUXT_OAUTH_GITHUB_CLIENT_ID or NUXT_OAUTH_GITHUB_CLIENT_SECRET env variables."
|
|
16
|
+
});
|
|
17
|
+
if (!onError)
|
|
18
|
+
throw error;
|
|
19
|
+
return onError(event, error);
|
|
20
|
+
}
|
|
21
|
+
if (!code) {
|
|
22
|
+
config.scope = config.scope || [];
|
|
23
|
+
if (config.emailRequired && !config.scope.includes("user:email")) {
|
|
24
|
+
config.scope.push("user:email");
|
|
25
|
+
}
|
|
26
|
+
const redirectUrl = getRequestURL(event).href;
|
|
27
|
+
return sendRedirect(
|
|
28
|
+
event,
|
|
29
|
+
withQuery(config.authorizationURL, {
|
|
30
|
+
client_id: config.clientId,
|
|
31
|
+
redirect_uri: redirectUrl,
|
|
32
|
+
scope: config.scope.join("%20")
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const tokens = await $fetch(
|
|
37
|
+
config.tokenURL,
|
|
38
|
+
{
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: {
|
|
41
|
+
client_id: config.clientId,
|
|
42
|
+
client_secret: config.clientSecret,
|
|
43
|
+
code
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
if (tokens.error) {
|
|
48
|
+
const error = createError({
|
|
49
|
+
statusCode: 401,
|
|
50
|
+
message: `GitHub login failed: ${tokens.error || "Unknown error"}`,
|
|
51
|
+
data: tokens
|
|
52
|
+
});
|
|
53
|
+
if (!onError)
|
|
54
|
+
throw error;
|
|
55
|
+
return onError(event, error);
|
|
56
|
+
}
|
|
57
|
+
const accessToken = tokens.access_token;
|
|
58
|
+
const user = await ofetch("https://api.github.com/user", {
|
|
59
|
+
headers: {
|
|
60
|
+
"User-Agent": `Github-OAuth-${config.clientId}`,
|
|
61
|
+
Authorization: `token ${accessToken}`
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
if (!user.email && config.emailRequired) {
|
|
65
|
+
const emails = await ofetch("https://api.github.com/user/emails", {
|
|
66
|
+
headers: {
|
|
67
|
+
"User-Agent": `Github-OAuth-${config.clientId}`,
|
|
68
|
+
Authorization: `token ${accessToken}`
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const primaryEmail = emails.find((email) => email.primary);
|
|
72
|
+
if (!primaryEmail) {
|
|
73
|
+
throw new Error("GitHub login failed: no user email found");
|
|
74
|
+
}
|
|
75
|
+
user.email = primaryEmail.email;
|
|
76
|
+
}
|
|
77
|
+
return onSuccess(event, {
|
|
78
|
+
user,
|
|
79
|
+
tokens
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { H3Event, H3Error } from 'h3';
|
|
2
|
+
export interface OAuthSpotifyConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Spotify OAuth Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_SPOTIFY_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Spotify OAuth Client Secret
|
|
10
|
+
* @default process.env.NUXT_OAUTH_SPOTIFY_CLIENT_SECRET
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Spotify OAuth Scope
|
|
15
|
+
* @default []
|
|
16
|
+
* @see https://developer.spotify.com/documentation/web-api/concepts/scopes
|
|
17
|
+
* @example ['user-read-email']
|
|
18
|
+
*/
|
|
19
|
+
scope?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Require email from user, adds the ['user-read-email'] scope if not present
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
emailRequired?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Spotify OAuth Authorization URL
|
|
27
|
+
* @default 'https://accounts.spotify.com/authorize'
|
|
28
|
+
*/
|
|
29
|
+
authorizationURL?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Spotify OAuth Token URL
|
|
32
|
+
* @default 'https://accounts.spotify.com/api/token'
|
|
33
|
+
*/
|
|
34
|
+
tokenURL?: string;
|
|
35
|
+
}
|
|
36
|
+
interface OAuthConfig {
|
|
37
|
+
config?: OAuthSpotifyConfig;
|
|
38
|
+
onSuccess: (event: H3Event, result: {
|
|
39
|
+
user: any;
|
|
40
|
+
tokens: any;
|
|
41
|
+
}) => Promise<void> | void;
|
|
42
|
+
onError?: (event: H3Event, error: H3Error) => Promise<void> | void;
|
|
43
|
+
}
|
|
44
|
+
export declare function spotifyEventHandler({ config, onSuccess, onError }: OAuthConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
|
|
2
|
+
import { withQuery, parsePath } from "ufo";
|
|
3
|
+
import { ofetch } from "ofetch";
|
|
4
|
+
import { defu } from "defu";
|
|
5
|
+
export function spotifyEventHandler({ config, onSuccess, onError }) {
|
|
6
|
+
return eventHandler(async (event) => {
|
|
7
|
+
config = defu(config, useRuntimeConfig(event).oauth?.spotify, {
|
|
8
|
+
authorizationURL: "https://accounts.spotify.com/authorize",
|
|
9
|
+
tokenURL: "https://accounts.spotify.com/api/token"
|
|
10
|
+
});
|
|
11
|
+
const { code } = getQuery(event);
|
|
12
|
+
if (!config.clientId || !config.clientSecret) {
|
|
13
|
+
const error = createError({
|
|
14
|
+
statusCode: 500,
|
|
15
|
+
message: "Missing NUXT_OAUTH_SPOTIFY_CLIENT_ID or NUXT_OAUTH_SPOTIFY_CLIENT_SECRET env variables."
|
|
16
|
+
});
|
|
17
|
+
if (!onError)
|
|
18
|
+
throw error;
|
|
19
|
+
return onError(event, error);
|
|
20
|
+
}
|
|
21
|
+
const redirectUrl = getRequestURL(event).href;
|
|
22
|
+
if (!code) {
|
|
23
|
+
config.scope = config.scope || [];
|
|
24
|
+
if (config.emailRequired && !config.scope.includes("user-read-email")) {
|
|
25
|
+
config.scope.push("user-read-email");
|
|
26
|
+
}
|
|
27
|
+
return sendRedirect(
|
|
28
|
+
event,
|
|
29
|
+
withQuery(config.authorizationURL, {
|
|
30
|
+
response_type: "code",
|
|
31
|
+
client_id: config.clientId,
|
|
32
|
+
redirect_uri: redirectUrl,
|
|
33
|
+
scope: config.scope.join("%20")
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const authCode = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
|
|
38
|
+
const tokens = await ofetch(
|
|
39
|
+
config.tokenURL,
|
|
40
|
+
{
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Basic ${authCode}`,
|
|
44
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
45
|
+
},
|
|
46
|
+
params: {
|
|
47
|
+
grant_type: "authorization_code",
|
|
48
|
+
redirect_uri: parsePath(redirectUrl).pathname,
|
|
49
|
+
code
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
).catch((error) => {
|
|
53
|
+
return { error };
|
|
54
|
+
});
|
|
55
|
+
if (tokens.error) {
|
|
56
|
+
const error = createError({
|
|
57
|
+
statusCode: 401,
|
|
58
|
+
message: `Spotify login failed: ${tokens.error?.data?.error_description || "Unknown error"}`,
|
|
59
|
+
data: tokens
|
|
60
|
+
});
|
|
61
|
+
if (!onError)
|
|
62
|
+
throw error;
|
|
63
|
+
return onError(event, error);
|
|
64
|
+
}
|
|
65
|
+
const accessToken = tokens.access_token;
|
|
66
|
+
const user = await ofetch("https://api.spotify.com/v1/me", {
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${accessToken}`
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return onSuccess(event, {
|
|
72
|
+
tokens,
|
|
73
|
+
user
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
export interface UserSession {
|
|
3
|
+
user?: any;
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
}
|
|
6
|
+
export declare function getUserSession(event: H3Event): Promise<UserSession>;
|
|
7
|
+
/**
|
|
8
|
+
* Set a user session
|
|
9
|
+
* @param event
|
|
10
|
+
* @param data User session data, please only store public information since it can be decoded with API calls
|
|
11
|
+
*/
|
|
12
|
+
export declare function setUserSession(event: H3Event, data: UserSession): Promise<UserSession>;
|
|
13
|
+
export declare function clearUserSession(event: H3Event): Promise<boolean>;
|
|
14
|
+
export declare function requireUserSession(event: H3Event): Promise<UserSession>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useSession, createError } from "h3";
|
|
2
|
+
import { defu } from "defu";
|
|
3
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
|
+
export async function getUserSession(event) {
|
|
5
|
+
return (await _useSession(event)).data;
|
|
6
|
+
}
|
|
7
|
+
export async function setUserSession(event, data) {
|
|
8
|
+
const session = await _useSession(event);
|
|
9
|
+
await session.update(defu(data, session.data));
|
|
10
|
+
return session.data;
|
|
11
|
+
}
|
|
12
|
+
export async function clearUserSession(event) {
|
|
13
|
+
const session = await _useSession(event);
|
|
14
|
+
await session.clear();
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
export async function requireUserSession(event) {
|
|
18
|
+
const userSession = await getUserSession(event);
|
|
19
|
+
if (!userSession.user) {
|
|
20
|
+
throw createError({
|
|
21
|
+
statusCode: 401,
|
|
22
|
+
message: "Unauthorized"
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return userSession;
|
|
26
|
+
}
|
|
27
|
+
let sessionConfig;
|
|
28
|
+
function _useSession(event) {
|
|
29
|
+
if (!sessionConfig) {
|
|
30
|
+
sessionConfig = defu({ password: process.env.NUXT_SESSION_PASSWORD }, useRuntimeConfig(event).session);
|
|
31
|
+
}
|
|
32
|
+
return useSession(event, sessionConfig);
|
|
33
|
+
}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import { ModuleOptions } from './module'
|
|
3
|
+
|
|
4
|
+
declare module '@nuxt/schema' {
|
|
5
|
+
interface NuxtConfig { ['auth']?: Partial<ModuleOptions> }
|
|
6
|
+
interface NuxtOptions { ['auth']?: ModuleOptions }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare module 'nuxt/schema' {
|
|
10
|
+
interface NuxtConfig { ['auth']?: Partial<ModuleOptions> }
|
|
11
|
+
interface NuxtOptions { ['auth']?: ModuleOptions }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export { ModuleOptions, default } from './module'
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
import { ModuleOptions } from './module'
|
|
3
|
+
|
|
4
|
+
declare module '@nuxt/schema' {
|
|
5
|
+
interface NuxtConfig { ['auth']?: Partial<ModuleOptions> }
|
|
6
|
+
interface NuxtOptions { ['auth']?: ModuleOptions }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare module 'nuxt/schema' {
|
|
10
|
+
interface NuxtConfig { ['auth']?: Partial<ModuleOptions> }
|
|
11
|
+
interface NuxtOptions { ['auth']?: ModuleOptions }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export { ModuleOptions, default } from './module'
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuxt-auth-utils",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Minimalist Auth module for Nuxt with SSR",
|
|
5
|
+
"repository": "Atinux/nuxt-auth-utils",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/types.d.ts",
|
|
11
|
+
"import": "./dist/module.mjs",
|
|
12
|
+
"require": "./dist/module.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/module.cjs",
|
|
16
|
+
"types": "./dist/types.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prepack": "nuxt-module-build build",
|
|
22
|
+
"dev": "nuxi dev playground",
|
|
23
|
+
"dev:build": "nuxi build playground",
|
|
24
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
25
|
+
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
|
|
26
|
+
"lint": "eslint .",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest watch"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@nuxt/kit": "^3.8.0",
|
|
32
|
+
"defu": "^6.1.3",
|
|
33
|
+
"ohash": "^1.1.3"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@iconify-json/simple-icons": "^1.1.76",
|
|
37
|
+
"@nuxt/devtools": "latest",
|
|
38
|
+
"@nuxt/eslint-config": "^0.2.0",
|
|
39
|
+
"@nuxt/module-builder": "^0.5.2",
|
|
40
|
+
"@nuxt/schema": "^3.8.0",
|
|
41
|
+
"@nuxt/test-utils": "^3.8.0",
|
|
42
|
+
"@nuxt/ui": "^2.10.0",
|
|
43
|
+
"@nuxt/ui-pro": "^0.4.2",
|
|
44
|
+
"@types/node": "^20.8.9",
|
|
45
|
+
"changelogen": "^0.5.5",
|
|
46
|
+
"eslint": "^8.52.0",
|
|
47
|
+
"nuxt": "^3.8.0",
|
|
48
|
+
"vitest": "^0.33.0"
|
|
49
|
+
}
|
|
50
|
+
}
|