first commit

This commit is contained in:
René Preuß
2023-02-18 14:27:55 +01:00
commit 47b0bf4ec9
23 changed files with 10379 additions and 0 deletions

12
.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
root = true
[*]
indent_size = 2
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
dist
node_modules

4
.eslintrc Normal file
View File

@@ -0,0 +1,4 @@
{
"root": true,
"extends": ["@nuxt/eslint-config"]
}

56
.gitignore vendored Normal file
View File

@@ -0,0 +1,56 @@
# Dependencies
node_modules
# Logs
*.log*
# Temp directories
.temp
.tmp
.cache
# Yarn
**/.yarn/cache
**/.yarn/*state*
# Generated dirs
dist
# Nuxt
.nuxt
.output
.vercel_build_output
.build-*
.env
.netlify
# Env
.env
# Testing
reports
coverage
*.lcov
.nyc_output
# VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Intellij idea
*.iml
.idea
# OSX
.DS_Store
.AppleDouble
.LSOverride
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false

2
.nuxtrc Normal file
View File

@@ -0,0 +1,2 @@
imports.autoImport=false
typescript.includeWorkspace=true

80
README.md Normal file
View File

@@ -0,0 +1,80 @@
# @bitinflow/nuxt-oauth
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![License][license-src]][license-href]
> My new Nuxt module
- [✨  Release Notes](/CHANGELOG.md)
<!-- - [📖 &nbsp;Documentation](https://example.com) -->
## Features
<!-- Highlight some of the features your module provide here -->
- ⛰ &nbsp;Foo
- 🚠 &nbsp;Bar
- 🌲 &nbsp;Baz
## Quick Setup
1. Add `my-module` dependency to your project
```bash
# Using pnpm
pnpm add -D my-module
# Using yarn
yarn add --dev my-module
# Using npm
npm install --save-dev my-module
```
2. Add `my-module` to the `modules` section of `nuxt.config.ts`
```js
export default defineNuxtConfig({
modules: [
'my-module'
]
})
```
That's it! You can now use My Module in your Nuxt app ✨
## Development
```bash
# Install dependencies
npm install
# Generate type stubs
npm run dev:prepare
# Develop with the playground
npm run dev
# Build the playground
npm run dev:build
# Run ESLint
npm run lint
# Run Vitest
npm run test
npm run test:watch
# Release new version
npm run release
```
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/my-module/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-href]: https://npmjs.com/package/my-module
[npm-downloads-src]: https://img.shields.io/npm/dm/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-downloads-href]: https://npmjs.com/package/my-module
[license-src]: https://img.shields.io/npm/l/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D
[license-href]: https://npmjs.com/package/my-module

9892
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "@bitinflow/nuxt-oauth",
"version": "1.0.0",
"description": "Nuxt 3 OAuth Module",
"license": "MIT",
"type": "module",
"exports": {
".": {
"types": "./dist/types.d.ts",
"import": "./dist/module.mjs",
"require": "./dist/module.cjs"
}
},
"main": "./dist/module.cjs",
"types": "./dist/types.d.ts",
"files": [
"dist"
],
"scripts": {
"prepack": "nuxt-module-build",
"dev": "nuxi dev playground",
"dev:build": "nuxi build playground",
"dev:prepare": "nuxt-module-build --stub && nuxi prepare playground",
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
"lint": "eslint .",
"test": "vitest run",
"test:watch": "vitest watch"
},
"dependencies": {
"@nuxt/kit": "^3.2.2",
"defu": "^6.1.2"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.1.1",
"@nuxt/module-builder": "^0.2.1",
"@nuxt/schema": "^3.2.2",
"@nuxt/test-utils": "^3.2.2",
"changelogen": "^0.4.1",
"eslint": "^8.34.0",
"nuxt": "^3.2.2",
"vitest": "^0.28.5"
}
}

18
playground/nuxt.config.ts Normal file
View File

@@ -0,0 +1,18 @@
export default defineNuxtConfig({
modules: ['../src/module'],
oauth: {
redirect: {
login: '/login/', // sandbox appends / at the end of url
logout: '/',
callback: '/login/', // sandbox appends / at the end of url
home: '/home'
},
endpoints: {
authorization: 'https://api.sandbox.own3d.pro/v1/oauth/authorization',
userInfo: `https://id.stream.tv/api/users/@me`,
logout: 'https://id.stream.tv/oauth/token'
},
clientId: '90a951d1-ea50-4fda-8c4d-275b81f7d219',
scope: ['user:read', 'connections']
},
})

4
playground/package.json Normal file
View File

@@ -0,0 +1,4 @@
{
"private": true,
"name": "my-module-playground"
}

View File

@@ -0,0 +1,11 @@
<template>
<div>
Guest only page
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: ["guest"]
})
</script>

20
playground/pages/home.vue Normal file
View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import {useAuth} from "#imports";
const {user, signOut} = await useAuth();
definePageMeta({
middleware: ["auth"]
})
</script>
<template>
<div v-if="user">
Hello {{ user.name }}
<button @click="signOut">
Sign Out
</button>
</div>
</template>

View File

@@ -0,0 +1,11 @@
<template>
<div>
Index page
<nuxt-link to="/login">
Login
</nuxt-link>
<nuxt-link to="/guest">
Guest
</nuxt-link>
</div>
</template>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import {definePageMeta, useAuth} from "#imports";
const {signIn} = await useAuth();
definePageMeta({
middleware: ["auth"]
})
</script>
<template>
<div>
<h1>Login</h1>
<button @click="signIn">
Sign In
</button>
</div>
</template>

65
src/module.ts Normal file
View File

@@ -0,0 +1,65 @@
import {addImportsDir, addPlugin, createResolver, defineNuxtModule} from '@nuxt/kit'
import defu from "defu";
// Module options TypeScript interface definition
export interface ModuleOptions {
redirect: {
login: string,
logout: string,
callback: string,
home: string
},
endpoints: {
authorization: string,
userInfo: string,
logout: string
},
clientId: string,
scope: string[]
}
const defaults: ModuleOptions = {
redirect: {
login: '/login',
logout: '/',
callback: '/login',
home: '/'
},
endpoints: {
authorization: 'https://accounts.bitinflow.com/oauth/authorize',
userInfo: `https://accounts.bitinflow.com/api/v3/user`,
logout: 'https://accounts.bitinflow.com/logout'
},
clientId: 'please-set-client-id',
scope: ['user:read']
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: '@bitinflow/nuxt-oauth',
configKey: 'oauth'
},
defaults,
setup (moduleOptions, nuxt) {
const resolver = createResolver(import.meta.url)
const options = defu(moduleOptions, {
...defaults
})
// Set up runtime configuration
nuxt.options.runtimeConfig = nuxt.options.runtimeConfig || { public: {} }
nuxt.options.runtimeConfig.oauth = defu(nuxt.options.runtimeConfig.oauth, {
...options
})
nuxt.options.runtimeConfig.public.oauth = defu(nuxt.options.runtimeConfig.public.oauth, {
...options
})
// Do not add the extension since the `.ts` will be transpiled to `.mjs` after `npm run prepack`
addPlugin(resolver.resolve('./runtime/plugin'))
const composables = resolver.resolve('./runtime/composables')
addImportsDir(composables)
}
})

View File

@@ -0,0 +1,68 @@
import {CookieRef, navigateTo, useCookie, useRuntimeConfig} from "#app";
import {ModuleOptions} from "../../module";
declare interface ComposableOptions {
fetchUserOnInitialization: boolean
}
export default async (options: ComposableOptions = {
fetchUserOnInitialization: false
}) => {
const user: CookieRef<any> = useCookie('oauth_user')
const accessToken: CookieRef<any> = useCookie('oauth_access_token')
const authConfig = useRuntimeConfig().public.oauth as ModuleOptions;
const fetchUser = async () => {
try {
const response = await fetch(authConfig.endpoints.userInfo, {
headers: {
Accept: 'application/json',
Authorization: `${accessToken.value.tokenType} ${accessToken.value.token}`
}
});
user.value = response.ok
? await response.json()
: null;
} catch (e) {
user.value = null;
}
}
const signIn = async () => {
// create oauth authorization url
const params = new URLSearchParams({
client_id: authConfig.clientId,
redirect_uri: window.location.origin + authConfig.redirect.callback,
response_type: 'token',
scope: authConfig.scope.join(' ')
})
window.location.href = `${authConfig.endpoints.authorization}?${params.toString()}`
};
const signOut = async () => {
accessToken.value = null;
user.value = null;
return navigateTo('/')
}
const setBearer = async (token: string, tokenType: string, expires: number) => {
accessToken.value = {token, tokenType, expiresAt: Date.now() + expires * 1000};
await fetchUser()
}
// Initialize the user if the option is set to true
if (options.fetchUserOnInitialization) {
await fetchUser()
}
return {
user,
signIn,
signOut,
setBearer,
authConfig
}
}

35
src/runtime/plugin.ts Normal file
View File

@@ -0,0 +1,35 @@
import {addRouteMiddleware, defineNuxtPlugin, navigateTo} from '#app'
import useAuth from "./composables/useAuth"
export default defineNuxtPlugin(() => {
addRouteMiddleware('auth', async (to) => {
const {user, authConfig, setBearer} = await useAuth()
if (to.path === authConfig.redirect.callback) {
const params = new URLSearchParams(to.hash.substring(1))
if (params.has('access_token')) {
const token = params.get('access_token') as string;
const tokenType = params.get('token_type') as string;
const expires = params.get('expires_in') as string;
await setBearer(token, tokenType, parseInt(expires));
return navigateTo(authConfig.redirect.home)
}
return
}
if (user.value === undefined) {
return navigateTo(authConfig.redirect.login)
}
})
addRouteMiddleware('guest', async () => {
const {user, authConfig} = await useAuth()
if (user.value !== undefined) {
return navigateTo(authConfig.redirect.home)
}
})
})

15
test/basic.test.ts Normal file
View File

@@ -0,0 +1,15 @@
import { describe, it, expect } from 'vitest'
import { fileURLToPath } from 'node:url'
import { setup, $fetch } from '@nuxt/test-utils'
describe('ssr', async () => {
await setup({
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
})
it('renders the index page', async () => {
// Get response to a server-rendered page with `$fetch`.
const html = await $fetch('/')
expect(html).toContain('<div>basic</div>')
})
})

6
test/fixtures/basic/app.vue vendored Normal file
View File

@@ -0,0 +1,6 @@
<template>
<div>basic</div>
</template>
<script setup>
</script>

7
test/fixtures/basic/nuxt.config.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
import MyModule from '../../../src/module'
export default defineNuxtConfig({
modules: [
MyModule
]
})

5
test/fixtures/basic/package.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"private": true,
"name": "basic",
"type": "module"
}

3
tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "./playground/.nuxt/tsconfig.json"
}