mirror of
https://github.com/Dan6erbond/sk-auth.git
synced 2024-11-20 19:07:20 +01:00
Merge branch 'main' into CrispyBacon1999/main
This commit is contained in:
commit
5c984a3653
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
README.md
|
||||||
|
app/
|
||||||
|
*.cjs
|
||||||
|
dist/
|
21
.eslintrc-shared.cjs
Normal file
21
.eslintrc-shared.cjs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
extends: ["prettier"],
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaVersion: 2019,
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["off", { argsIgnorePattern: "^_" }],
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
},
|
||||||
|
};
|
@ -1,22 +1,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
extends: [
|
||||||
plugins: ["@typescript-eslint"],
|
"eslint:recommended",
|
||||||
ignorePatterns: ["*.cjs", "app/**/*"],
|
"plugin:@typescript-eslint/recommended",
|
||||||
parserOptions: {
|
"./.eslintrc-shared.cjs",
|
||||||
sourceType: "module",
|
],
|
||||||
ecmaVersion: 2019,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": ["off", { argsIgnorePattern: "^_" }],
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2017: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
7
.github/workflows/lint-on-pr.yml
vendored
7
.github/workflows/lint-on-pr.yml
vendored
@ -15,9 +15,6 @@ jobs:
|
|||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
- run: yarn install
|
- run: yarn install --frozen-lockfile && yarn lint
|
||||||
- run: yarn lint
|
- run: yarn install --frozen-lockfile && yarn lint
|
||||||
- run: yarn install
|
|
||||||
working-directory: app
|
|
||||||
- run: yarn lint
|
|
||||||
working-directory: app
|
working-directory: app
|
||||||
|
2
.github/workflows/npm-publish.yml
vendored
2
.github/workflows/npm-publish.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
- run: yarn install
|
- run: yarn install --frozen-lockfile
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- uses: JS-DevTools/npm-publish@v1
|
- uses: JS-DevTools/npm-publish@v1
|
||||||
with:
|
with:
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -76,4 +76,4 @@ typings/
|
|||||||
.fusebox/
|
.fusebox/
|
||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
dist/*
|
dist/
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
dist/**
|
dist/
|
||||||
node_modules/**
|
node_modules/
|
||||||
|
README.md
|
||||||
|
app/
|
||||||
|
@ -31,7 +31,7 @@ SvelteKitAuth also comes with first-class support for Typescript out of the box,
|
|||||||
|
|
||||||
SvelteKitAuth is very easy to setup! All you need to do is instantiate the `SvelteKitAuth` class, and configure it with some default providers, as well as a JWT secret key used to verify the cookies:
|
SvelteKitAuth is very easy to setup! All you need to do is instantiate the `SvelteKitAuth` class, and configure it with some default providers, as well as a JWT secret key used to verify the cookies:
|
||||||
|
|
||||||
_**Warning**: env variables prefixed with `VITE_` can be exposed and leaked into client-side bundles if they are referenced in any client-side code. Make sure this is not the case, or consider using an alternative method such as loading them via dotenv directly instead._
|
***Warning**: env variables prefixed with `VITE_` can be exposed and leaked into client-side bundles if they are referenced in any client-side code. Make sure this is not the case, or consider using an alternative method such as loading them via dotenv directly instead.*
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export const appAuth = new SvelteKitAuth({
|
export const appAuth = new SvelteKitAuth({
|
||||||
@ -144,6 +144,10 @@ SvelteKitAuth is inspired by the [NextAuth.js](https://next-auth.js.org/) packag
|
|||||||
|
|
||||||
As it leverages classes and Typescript, the implementation of such providers is very straightforward, and in the future it will even be possible to register multiple SvelteKitAuth handlers in the same project, should the need arise, by leveraging a class-based client and server setup.
|
As it leverages classes and Typescript, the implementation of such providers is very straightforward, and in the future it will even be possible to register multiple SvelteKitAuth handlers in the same project, should the need arise, by leveraging a class-based client and server setup.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Looking for help? Check out the [example app](./app/) in the repository source. Make something cool you want to show off? Share it with others [in the discussion section](https://github.com/Dan6erbond/sk-auth/discussions/72).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
🚧 Work in Progress!
|
🚧 Work in Progress!
|
||||||
|
6
app/.eslintignore
Normal file
6
app/.eslintignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
README.md
|
||||||
|
*.cjs
|
||||||
|
.svelte-kit/
|
||||||
|
static/
|
||||||
|
build/
|
@ -1,20 +1,15 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
extends: ["../.eslintrc.cjs"],
|
extends: ["../.eslintrc-shared.cjs"],
|
||||||
plugins: ["svelte3", "@typescript-eslint"],
|
plugins: ["svelte3"],
|
||||||
ignorePatterns: ["*.cjs"],
|
overrides: [
|
||||||
overrides: [{ files: ["*.svelte"], processor: "svelte3/svelte3" }],
|
{
|
||||||
|
files: ["*.svelte"],
|
||||||
|
processor: "svelte3/svelte3",
|
||||||
|
},
|
||||||
|
],
|
||||||
settings: {
|
settings: {
|
||||||
"svelte3/typescript": () => require("typescript"),
|
"svelte3/typescript": () => require("typescript"),
|
||||||
},
|
},
|
||||||
parserOptions: {
|
|
||||||
sourceType: "module",
|
|
||||||
ecmaVersion: 2019,
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2017: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.svelte-kit/**
|
.svelte-kit/
|
||||||
static/**
|
static/
|
||||||
build/**
|
build/
|
||||||
node_modules/**
|
node_modules/
|
||||||
|
@ -5,11 +5,11 @@
|
|||||||
"dev": "svelte-kit dev",
|
"dev": "svelte-kit dev",
|
||||||
"build": "svelte-kit build",
|
"build": "svelte-kit build",
|
||||||
"preview": "svelte-kit preview",
|
"preview": "svelte-kit preview",
|
||||||
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
||||||
"format": "prettier --write --plugin-search-dir=. ."
|
"format": "prettier --write --plugin-search-dir=. ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/kit": "next",
|
"@sveltejs/kit": "^1.0.0-next.259",
|
||||||
"@types/prismjs": "^1.16.5",
|
"@types/prismjs": "^1.16.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
||||||
"@typescript-eslint/parser": "^4.19.0",
|
"@typescript-eslint/parser": "^4.19.0",
|
||||||
@ -33,6 +33,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/fira-mono": "^4.3.0",
|
"@fontsource/fira-mono": "^4.3.0",
|
||||||
"@fontsource/inter": "^4.3.0",
|
"@fontsource/inter": "^4.3.0",
|
||||||
|
"clipboard": "^2.0.8",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"prismjs": "^1.23.0",
|
"prismjs": "^1.23.0",
|
||||||
"sk-auth": "file:../"
|
"sk-auth": "file:../"
|
||||||
|
2
app/src/global.d.ts
vendored
2
app/src/global.d.ts
vendored
@ -11,5 +11,7 @@ interface ImportMetaEnv {
|
|||||||
VITE_TWITTER_API_SECRET: string;
|
VITE_TWITTER_API_SECRET: string;
|
||||||
VITE_REDDIT_API_KEY: string;
|
VITE_REDDIT_API_KEY: string;
|
||||||
VITE_REDDIT_API_SECRET: string;
|
VITE_REDDIT_API_SECRET: string;
|
||||||
|
VITE_SPOTIFY_CLIENT_ID: string;
|
||||||
|
VITE_SPOTIFY_CLIENT_SECRET: string;
|
||||||
VITE_JWT_SECRET_KEY: string;
|
VITE_JWT_SECRET_KEY: string;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import type { Handle } from "@sveltejs/kit";
|
import type { Handle } from "@sveltejs/kit";
|
||||||
import { appAuth } from "$lib/appAuth";
|
import { appAuth } from "$lib/appAuth";
|
||||||
|
|
||||||
export const handle: Handle = async ({ request, render }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
// TODO https://github.com/sveltejs/kit/issues/1046
|
// TODO https://github.com/sveltejs/kit/issues/1046
|
||||||
if (request.query.has("_method")) {
|
|
||||||
request.method = request.query.get("_method").toUpperCase();
|
|
||||||
|
if (event.request.query.has("_method")) {
|
||||||
|
event.request.method = event.request.query.get("_method").toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await render(request);
|
const response = await resolve(event);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
GoogleOAuth2Provider,
|
GoogleOAuth2Provider,
|
||||||
RedditOAuth2Provider,
|
RedditOAuth2Provider,
|
||||||
TwitterAuthProvider,
|
TwitterAuthProvider,
|
||||||
|
SpotifyOAuth2Provider,
|
||||||
} from "sk-auth/providers";
|
} from "sk-auth/providers";
|
||||||
|
|
||||||
export const appAuth = new SvelteKitAuth({
|
export const appAuth = new SvelteKitAuth({
|
||||||
@ -45,6 +46,13 @@ export const appAuth = new SvelteKitAuth({
|
|||||||
return { ...slim, provider: "reddit" };
|
return { ...slim, provider: "reddit" };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
new SpotifyOAuth2Provider({
|
||||||
|
clientId: import.meta.env.VITE_SPOTIFY_CLIENT_ID,
|
||||||
|
clientSecret: import.meta.env.VITE_SPOTIFY_CLIENT_SECRET,
|
||||||
|
profile(profile) {
|
||||||
|
return { ...profile, provider: "spotify" };
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
jwt(token, profile) {
|
jwt(token, profile) {
|
||||||
|
@ -180,6 +180,38 @@
|
|||||||
<span>Sign in with Reddit</span>
|
<span>Sign in with Reddit</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/api/auth/signin/spotify?redirect=/profile"
|
||||||
|
class={clsx(
|
||||||
|
"text-sm",
|
||||||
|
"md:text-base",
|
||||||
|
"inline-flex",
|
||||||
|
"space-x-4",
|
||||||
|
"py-2",
|
||||||
|
"px-4",
|
||||||
|
"border-gray-400",
|
||||||
|
"rounded",
|
||||||
|
"hover:no-underline",
|
||||||
|
"border",
|
||||||
|
"hover:bg-gray-100",
|
||||||
|
"transition-colors",
|
||||||
|
"items-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 168 168"
|
||||||
|
class={clsx("h-4", "w-4", "md:h-6", "md:w-6")}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#1ED760"
|
||||||
|
d="m83.996 0.277c-46.249 0-83.743 37.493-83.743 83.742 0 46.251 37.494 83.741 83.743 83.741 46.254 0 83.744-37.49 83.744-83.741 0-46.246-37.49-83.738-83.745-83.738l0.001-0.004zm38.404 120.78c-1.5 2.46-4.72 3.24-7.18 1.73-19.662-12.01-44.414-14.73-73.564-8.07-2.809 0.64-5.609-1.12-6.249-3.93-0.643-2.81 1.11-5.61 3.926-6.25 31.9-7.291 59.263-4.15 81.337 9.34 2.46 1.51 3.24 4.72 1.73 7.18zm10.25-22.805c-1.89 3.075-5.91 4.045-8.98 2.155-22.51-13.839-56.823-17.846-83.448-9.764-3.453 1.043-7.1-0.903-8.148-4.35-1.04-3.453 0.907-7.093 4.354-8.143 30.413-9.228 68.222-4.758 94.072 11.127 3.07 1.89 4.04 5.91 2.15 8.976v-0.001zm0.88-23.744c-26.99-16.031-71.52-17.505-97.289-9.684-4.138 1.255-8.514-1.081-9.768-5.219-1.254-4.14 1.08-8.513 5.221-9.771 29.581-8.98 78.756-7.245 109.83 11.202 3.73 2.209 4.95 7.016 2.74 10.733-2.2 3.722-7.02 4.949-10.73 2.739z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Sign in with Spotify</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<p class={clsx("text-gray-600", "text-center", "border-gray-400", "border-b", "pb-2")}>
|
<p class={clsx("text-gray-600", "text-center", "border-gray-400", "border-b", "pb-2")}>
|
||||||
Coming soon.
|
Coming soon.
|
||||||
</p>
|
</p>
|
||||||
|
@ -201,6 +201,47 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class={clsx("flex", "items-center", "space-x-4")}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 168 168"
|
||||||
|
class={clsx("h-4", "w-4", "md:h-6", "md:w-6")}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#1ED760"
|
||||||
|
d="m83.996 0.277c-46.249 0-83.743 37.493-83.743 83.742 0 46.251 37.494 83.741 83.743 83.741 46.254 0 83.744-37.49 83.744-83.741 0-46.246-37.49-83.738-83.745-83.738l0.001-0.004zm38.404 120.78c-1.5 2.46-4.72 3.24-7.18 1.73-19.662-12.01-44.414-14.73-73.564-8.07-2.809 0.64-5.609-1.12-6.249-3.93-0.643-2.81 1.11-5.61 3.926-6.25 31.9-7.291 59.263-4.15 81.337 9.34 2.46 1.51 3.24 4.72 1.73 7.18zm10.25-22.805c-1.89 3.075-5.91 4.045-8.98 2.155-22.51-13.839-56.823-17.846-83.448-9.764-3.453 1.043-7.1-0.903-8.148-4.35-1.04-3.453 0.907-7.093 4.354-8.143 30.413-9.228 68.222-4.758 94.072 11.127 3.07 1.89 4.04 5.91 2.15 8.976v-0.001zm0.88-23.744c-26.99-16.031-71.52-17.505-97.289-9.684-4.138 1.255-8.514-1.081-9.768-5.219-1.254-4.14 1.08-8.513 5.221-9.771 29.581-8.98 78.756-7.245 109.83 11.202 3.73 2.209 4.95 7.016 2.74 10.733-2.2 3.722-7.02 4.949-10.73 2.739z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div class={clsx("flex", "flex-col", "items-start", "space-y-1")}>
|
||||||
|
{#if $session.user.connections.spotify}
|
||||||
|
<p class={clsx("font-bold")}>Signed in as:</p>
|
||||||
|
|
||||||
|
{$session.user.connections.spotify.display_name}
|
||||||
|
{:else}
|
||||||
|
<p class={clsx("font-bold")}>Not signed in</p>
|
||||||
|
<div
|
||||||
|
class={clsx(
|
||||||
|
"text-xs",
|
||||||
|
"md:text-sm",
|
||||||
|
"py-1",
|
||||||
|
"px-2",
|
||||||
|
"border-gray-400",
|
||||||
|
"rounded",
|
||||||
|
"hover:no-underline",
|
||||||
|
"border",
|
||||||
|
"hover:bg-gray-100",
|
||||||
|
"transition-colors",
|
||||||
|
"items-center",
|
||||||
|
"cursor-not-allowed",
|
||||||
|
"inline-block",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class={clsx("text-lg", "mb-2")}>Session</p>
|
<p class={clsx("text-lg", "mb-2")}>Session</p>
|
||||||
|
3308
app/yarn.lock
3308
app/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sk-auth",
|
"name": "sk-auth",
|
||||||
"version": "0.3.7",
|
"version": "0.4.1",
|
||||||
"description": "Authentication library for use with SvelteKit featuring built-in OAuth providers and zero restriction customization!",
|
"description": "Authentication library for use with SvelteKit featuring built-in OAuth providers and zero restriction customization!",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.esm.js",
|
"module": "dist/index.esm.js",
|
||||||
@ -18,10 +18,11 @@
|
|||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"prepare": "npm run build",
|
||||||
"build": "rollup --config",
|
"build": "rollup --config",
|
||||||
"dev": "rollup --config --watch",
|
"dev": "rollup --config --watch",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
||||||
"format": "prettier --write --plugin-search-dir=. ."
|
"format": "prettier --write --plugin-search-dir=. ."
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@ -47,7 +48,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-typescript": "^8.2.1",
|
"@rollup/plugin-typescript": "^8.2.1",
|
||||||
"@sveltejs/kit": "^1.0.0-next.107",
|
"@sveltejs/kit": "^1.0.0-next.259",
|
||||||
"@types/jsonwebtoken": "^8.5.1",
|
"@types/jsonwebtoken": "^8.5.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||||
"@typescript-eslint/parser": "^4.23.0",
|
"@typescript-eslint/parser": "^4.23.0",
|
||||||
|
67
src/auth.ts
67
src/auth.ts
@ -1,6 +1,6 @@
|
|||||||
import type { GetSession, RequestHandler } from "@sveltejs/kit";
|
import type { GetSession, RequestHandler } from "@sveltejs/kit";
|
||||||
import type { EndpointOutput, ServerRequest } from "@sveltejs/kit/types/endpoint";
|
import type { EndpointOutput } from "@sveltejs/kit/types/endpoint";
|
||||||
import type { Headers } from "@sveltejs/kit/types/helper";
|
import { RequestEvent } from "@sveltejs/kit/types/hooks";
|
||||||
import cookie from "cookie";
|
import cookie from "cookie";
|
||||||
import * as jsonwebtoken from "jsonwebtoken";
|
import * as jsonwebtoken from "jsonwebtoken";
|
||||||
import type { JWT, Session } from "./interfaces";
|
import type { JWT, Session } from "./interfaces";
|
||||||
@ -13,6 +13,7 @@ interface AuthConfig {
|
|||||||
jwtSecret?: string;
|
jwtSecret?: string;
|
||||||
jwtExpiresIn?: string | number;
|
jwtExpiresIn?: string | number;
|
||||||
host?: string;
|
host?: string;
|
||||||
|
protocol?: string;
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,12 +44,12 @@ export class Auth {
|
|||||||
return "svelte_auth_secret";
|
return "svelte_auth_secret";
|
||||||
}
|
}
|
||||||
|
|
||||||
async getToken(headers: Headers) {
|
async getToken(headers: any) {
|
||||||
if (!headers.cookie) {
|
if (!headers.get("cookie")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookies = cookie.parse(headers.cookie);
|
const cookies = cookie.parse(headers.get("cookie"));
|
||||||
|
|
||||||
if (!cookies.svelteauthjwt) {
|
if (!cookies.svelteauthjwt) {
|
||||||
return null;
|
return null;
|
||||||
@ -69,7 +70,9 @@ export class Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getBaseUrl(host?: string) {
|
getBaseUrl(host?: string) {
|
||||||
return this.config?.host ?? `http://${host}`;
|
const protocol = this.config?.protocol ?? "https";
|
||||||
|
host = this.config?.host ?? host;
|
||||||
|
return `${protocol}://${host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPath(path: string) {
|
getPath(path: string) {
|
||||||
@ -82,7 +85,7 @@ export class Auth {
|
|||||||
return new URL(pathname, this.getBaseUrl(host)).href;
|
return new URL(pathname, this.getBaseUrl(host)).href;
|
||||||
}
|
}
|
||||||
|
|
||||||
setToken(headers: Headers, newToken: JWT | any) {
|
setToken(headers: any, newToken: JWT | any) {
|
||||||
const originalToken = this.getToken(headers);
|
const originalToken = this.getToken(headers);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -109,12 +112,10 @@ export class Auth {
|
|||||||
return redirect;
|
return redirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleProviderCallback(
|
async handleProviderCallback(event: RequestEvent, provider: Provider): Promise<EndpointOutput> {
|
||||||
request: ServerRequest,
|
const { headers } = event.request;
|
||||||
provider: Provider,
|
const { url } = event;
|
||||||
): Promise<EndpointOutput> {
|
const [profile, redirectUrl] = await provider.callback(event, this);
|
||||||
const { headers, host } = request;
|
|
||||||
const [profile, redirectUrl] = await provider.callback(request, this);
|
|
||||||
|
|
||||||
let token = (await this.getToken(headers)) ?? { user: {} };
|
let token = (await this.getToken(headers)) ?? { user: {} };
|
||||||
if (this.config?.callbacks?.jwt) {
|
if (this.config?.callbacks?.jwt) {
|
||||||
@ -124,7 +125,7 @@ export class Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const jwt = this.signToken(token);
|
const jwt = this.signToken(token);
|
||||||
const redirect = await this.getRedirectUrl(host, redirectUrl ?? undefined);
|
const redirect = await this.getRedirectUrl(url.host, redirectUrl ?? undefined);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
@ -135,11 +136,12 @@ export class Auth {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleEndpoint(request: ServerRequest): Promise<EndpointOutput> {
|
async handleEndpoint(event: RequestEvent): Promise<EndpointOutput> {
|
||||||
const { path, headers, method, host } = request;
|
const { headers, method } = event.request;
|
||||||
|
const { url } = event;
|
||||||
|
|
||||||
if (path === this.getPath("signout")) {
|
if (url.pathname === this.getPath("signout")) {
|
||||||
const token = this.setToken(headers, {});
|
const token = this.setToken(event.request.headers, {});
|
||||||
const jwt = this.signToken(token);
|
const jwt = this.signToken(token);
|
||||||
|
|
||||||
if (method === "POST") {
|
if (method === "POST") {
|
||||||
@ -153,7 +155,7 @@ export class Auth {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirect = await this.getRedirectUrl(host);
|
const redirect = await this.getRedirectUrl(url.host);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
@ -165,7 +167,7 @@ export class Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const regex = new RegExp(join([this.basePath, `(?<method>signin|callback)/(?<provider>\\w+)`]));
|
const regex = new RegExp(join([this.basePath, `(?<method>signin|callback)/(?<provider>\\w+)`]));
|
||||||
const match = path.match(regex);
|
const match = url.pathname.match(regex);
|
||||||
|
|
||||||
if (match && match.groups) {
|
if (match && match.groups) {
|
||||||
const provider = this.config?.providers?.find(
|
const provider = this.config?.providers?.find(
|
||||||
@ -173,9 +175,9 @@ export class Auth {
|
|||||||
);
|
);
|
||||||
if (provider) {
|
if (provider) {
|
||||||
if (match.groups.method === "signin") {
|
if (match.groups.method === "signin") {
|
||||||
return await provider.signin(request, this);
|
return await provider.signin(event, this);
|
||||||
} else {
|
} else {
|
||||||
return await this.handleProviderCallback(request, provider);
|
return await this.handleProviderCallback(event, provider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,13 +188,13 @@ export class Auth {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
get: RequestHandler = async (request) => {
|
get: RequestHandler = async (event: RequestEvent): Promise<any> => {
|
||||||
const { path } = request;
|
const { url } = event;
|
||||||
|
|
||||||
if (path === this.getPath("csrf")) {
|
if (url.pathname === this.getPath("csrf")) {
|
||||||
return { body: "1234" }; // TODO: Generate real token
|
return { body: "1234" }; // TODO: Generate real token
|
||||||
} else if (path === this.getPath("session")) {
|
} else if (url.pathname === this.getPath("session")) {
|
||||||
const session = await this.getSession(request);
|
const session = await this.getSession(event);
|
||||||
return {
|
return {
|
||||||
body: {
|
body: {
|
||||||
session,
|
session,
|
||||||
@ -200,15 +202,16 @@ export class Auth {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.handleEndpoint(request);
|
return await this.handleEndpoint(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
post: RequestHandler = async (request) => {
|
post: RequestHandler = async (event: RequestEvent) => {
|
||||||
return await this.handleEndpoint(request);
|
return await this.handleEndpoint(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
getSession: GetSession = async ({ headers }) => {
|
getSession: GetSession = async (event: RequestEvent) => {
|
||||||
const token = await this.getToken(headers);
|
const { request } = event;
|
||||||
|
const token = await this.getToken(request.headers);
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
if (this.config?.callbacks?.session) {
|
if (this.config?.callbacks?.session) {
|
||||||
|
19
src/client/helpers.ts
Normal file
19
src/client/helpers.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
function mergePath(basePaths: (string | null)[], path: string) {
|
||||||
|
if (path.startsWith("/")) {
|
||||||
|
path = path.slice(1);
|
||||||
|
}
|
||||||
|
let retPath;
|
||||||
|
for (let basePath of basePaths) {
|
||||||
|
if (basePath !== null) {
|
||||||
|
if (!basePath.startsWith("/")) {
|
||||||
|
basePath = "/" + basePath;
|
||||||
|
}
|
||||||
|
if (!basePath.endsWith("/")) {
|
||||||
|
basePath = basePath + "/";
|
||||||
|
}
|
||||||
|
retPath = basePath + path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retPath;
|
||||||
|
}
|
@ -1,14 +1,15 @@
|
|||||||
/* import { goto } from "@sveltejs/kit/assets/runtime/app/navigation";
|
/* import { goto } from "@sveltejs/kit/assets/runtime/app/navigation";
|
||||||
import { page } from "@sveltejs/kit/assets/runtime/app/stores"; */
|
import { page } from "@sveltejs/kit/assets/runtime/app/stores"; */
|
||||||
import type { Page } from "@sveltejs/kit";
|
import type { LoadInput } from "@sveltejs/kit";
|
||||||
|
import type { ClientRequestConfig } from "./types";
|
||||||
|
|
||||||
interface SignInConfig {
|
interface SignInConfig extends ClientRequestConfig {
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function signIn(provider: string, data?: any, config?: SignInConfig) {
|
export async function signIn(provider: string, data?: any, config?: SignInConfig) {
|
||||||
if (data) {
|
if (data) {
|
||||||
const path = `/api/auth/callback/${provider}`;
|
const path = mergePath(["/api/auth", config?.basePath ?? null], `/callback/${provider}`);
|
||||||
const res = await fetch(path, {
|
const res = await fetch(path, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -23,10 +24,10 @@ export async function signIn(provider: string, data?: any, config?: SignInConfig
|
|||||||
if (config?.redirectUrl) {
|
if (config?.redirectUrl) {
|
||||||
redirectUrl = config.redirectUrl;
|
redirectUrl = config.redirectUrl;
|
||||||
} else {
|
} else {
|
||||||
let $val: Page | undefined;
|
let $val: LoadInput | undefined;
|
||||||
/* page.subscribe(($) => ($val = $))(); */
|
/* page.subscribe(($) => ($val = $))(); */
|
||||||
if ($val) {
|
if ($val) {
|
||||||
redirectUrl = `${$val.host}${$val.path}?${$val.query}`;
|
redirectUrl = `${$val.url.host}${$val.url.pathname}?${$val.url.searchParams}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ export async function signIn(provider: string, data?: any, config?: SignInConfig
|
|||||||
redirect: redirectUrl ?? "/",
|
redirect: redirectUrl ?? "/",
|
||||||
};
|
};
|
||||||
const query = new URLSearchParams(queryData);
|
const query = new URLSearchParams(queryData);
|
||||||
const path = `/api/auth/login/${provider}?${query}`;
|
const path = mergePath(["/api/auth", config?.basePath ?? null], `/signin/${provider}?${query}`);
|
||||||
|
|
||||||
return path; // await goto(path);
|
return path; // await goto(path);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
/* import { session as session$ } from "$app/stores"; */
|
/* import { session as session$ } from "$app/stores"; */
|
||||||
|
|
||||||
export async function signOut() {
|
import type { ClientRequestConfig } from "./types";
|
||||||
let res = await fetch("/api/auth/signout", { method: "POST" });
|
|
||||||
|
export async function signOut(config?: ClientRequestConfig) {
|
||||||
|
let res = await fetch(mergePath(["/api/auth", config?.basePath ?? null], "signout"), {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
const { signout } = await res.json();
|
const { signout } = await res.json();
|
||||||
|
|
||||||
if (!signout) {
|
if (!signout) {
|
||||||
throw new Error("Sign out not successful!");
|
throw new Error("Sign out not successful!");
|
||||||
}
|
}
|
||||||
|
|
||||||
res = await fetch("/api/auth/session");
|
res = await fetch(mergePath(["/api/auth", config?.basePath ?? null], "session"));
|
||||||
const session = await res.json();
|
const session = await res.json();
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
|
3
src/client/types.ts
Normal file
3
src/client/types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface ClientRequestConfig {
|
||||||
|
basePath?: string;
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import type { EndpointOutput } from "@sveltejs/kit";
|
import type { EndpointOutput } from "@sveltejs/kit";
|
||||||
import type { ServerRequest } from "@sveltejs/kit/types/endpoint";
|
import { RequestEvent } from "@sveltejs/kit/types/hooks";
|
||||||
import type { Auth } from "../auth";
|
import type { Auth } from "../auth";
|
||||||
import type { CallbackResult } from "../types";
|
import type { CallbackResult } from "../types";
|
||||||
|
|
||||||
@ -28,12 +28,12 @@ export abstract class Provider<T extends ProviderConfig = ProviderConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract signin<Locals extends Record<string, any> = Record<string, any>, Body = unknown>(
|
abstract signin<Locals extends Record<string, any> = Record<string, any>, Body = unknown>(
|
||||||
request: ServerRequest<Locals, Body>,
|
event: RequestEvent,
|
||||||
svelteKitAuth: Auth,
|
svelteKitAuth: Auth,
|
||||||
): EndpointOutput | Promise<EndpointOutput>;
|
): EndpointOutput | Promise<EndpointOutput>;
|
||||||
|
|
||||||
abstract callback<Locals extends Record<string, any> = Record<string, any>, Body = unknown>(
|
abstract callback<Locals extends Record<string, any> = Record<string, any>, Body = unknown>(
|
||||||
request: ServerRequest<Locals, Body>,
|
event: RequestEvent,
|
||||||
svelteKitAuth: Auth,
|
svelteKitAuth: Auth,
|
||||||
): CallbackResult | Promise<CallbackResult>;
|
): CallbackResult | Promise<CallbackResult>;
|
||||||
}
|
}
|
||||||
|
50
src/providers/github.ts
Normal file
50
src/providers/github.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { OAuth2Provider, OAuth2ProviderConfig } from "./oauth2";
|
||||||
|
|
||||||
|
export interface GitHubProfile {
|
||||||
|
id: number;
|
||||||
|
login: string;
|
||||||
|
avatar_url: string;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
// complete with more info
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitHubTokens {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GitHubOAuth2ProviderConfig = OAuth2ProviderConfig<GitHubProfile, GitHubTokens>;
|
||||||
|
|
||||||
|
const defaultConfig: Partial<GitHubOAuth2ProviderConfig> = {
|
||||||
|
id: "github",
|
||||||
|
scope: "user",
|
||||||
|
accessTokenUrl: "https://github.com/login/oauth/access_token",
|
||||||
|
authorizationUrl: "https://github.com/login/oauth/authorize",
|
||||||
|
profileUrl: "https://api.github.com/user",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GitHubOAuth2Provider extends OAuth2Provider<
|
||||||
|
GitHubProfile,
|
||||||
|
GitHubTokens,
|
||||||
|
GitHubOAuth2ProviderConfig
|
||||||
|
> {
|
||||||
|
constructor(config: GitHubOAuth2ProviderConfig) {
|
||||||
|
super({
|
||||||
|
...defaultConfig,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserProfile(tokens: GitHubTokens): Promise<GitHubProfile> {
|
||||||
|
const tokenType = "token"; // 🤷♂️ token type returned is "bearer" but GitHub uses "token" keyword
|
||||||
|
const res = await fetch(this.config.profileUrl!, {
|
||||||
|
headers: { Authorization: `${tokenType} ${tokens.access_token}` },
|
||||||
|
});
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
export { Provider } from "./base";
|
export { Provider } from "./base";
|
||||||
export { TwitchOAuth2Provider } from "./twitch";
|
export { GitHubOAuth2Provider } from "./github";
|
||||||
export type { TwitchProfile, TwitchTokens } from "./twitch";
|
export type { GitHubProfile, GitHubTokens } from "./github";
|
||||||
export { GoogleOAuth2Provider } from "./google";
|
export { GoogleOAuth2Provider } from "./google";
|
||||||
export type { GoogleProfile, GoogleTokens } from "./google";
|
export type { GoogleProfile, GoogleTokens } from "./google";
|
||||||
export { TwitterAuthProvider } from "./twitter";
|
|
||||||
export { FacebookOAuth2Provider } from "./facebook";
|
export { FacebookOAuth2Provider } from "./facebook";
|
||||||
export type { FacebookProfile, FacebookTokens } from "./facebook";
|
export type { FacebookProfile, FacebookTokens } from "./facebook";
|
||||||
export { OAuth2BaseProvider } from "./oauth2.base";
|
export { OAuth2BaseProvider } from "./oauth2.base";
|
||||||
@ -13,3 +12,8 @@ export { OktaOAuth2Provider } from "./okta";
|
|||||||
export type { OktaProfile, OktaTokens, OktaAddress } from "./okta";
|
export type { OktaProfile, OktaTokens, OktaAddress } from "./okta";
|
||||||
export { RedditOAuth2Provider } from "./reddit";
|
export { RedditOAuth2Provider } from "./reddit";
|
||||||
export type { RedditProfile, RedditTokens } from "./reddit";
|
export type { RedditProfile, RedditTokens } from "./reddit";
|
||||||
|
export { SpotifyOAuth2Provider } from "./spotify";
|
||||||
|
export type { SpotifyProfile, SpotifyTokens } from "./spotify";
|
||||||
|
export { TwitchOAuth2Provider } from "./twitch";
|
||||||
|
export type { TwitchProfile, TwitchTokens } from "./twitch";
|
||||||
|
export { TwitterAuthProvider } from "./twitter";
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { EndpointOutput, ServerRequest } from "@sveltejs/kit/types/endpoint";
|
import type { EndpointOutput } from "@sveltejs/kit/types/endpoint";
|
||||||
|
import { RequestEvent } from "@sveltejs/kit/types/hooks";
|
||||||
import type { Auth } from "../auth";
|
import type { Auth } from "../auth";
|
||||||
import type { CallbackResult } from "../types";
|
import type { CallbackResult } from "../types";
|
||||||
import { Provider, ProviderConfig } from "./base";
|
import { Provider, ProviderConfig } from "./base";
|
||||||
@ -24,7 +25,7 @@ export abstract class OAuth2BaseProvider<
|
|||||||
T extends OAuth2BaseProviderConfig,
|
T extends OAuth2BaseProviderConfig,
|
||||||
> extends Provider<T> {
|
> extends Provider<T> {
|
||||||
abstract getAuthorizationUrl(
|
abstract getAuthorizationUrl(
|
||||||
request: ServerRequest,
|
event: RequestEvent,
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
state: string,
|
state: string,
|
||||||
nonce: string,
|
nonce: string,
|
||||||
@ -32,17 +33,20 @@ export abstract class OAuth2BaseProvider<
|
|||||||
abstract getTokens(code: string, redirectUri: string): TokensType | Promise<TokensType>;
|
abstract getTokens(code: string, redirectUri: string): TokensType | Promise<TokensType>;
|
||||||
abstract getUserProfile(tokens: any): ProfileType | Promise<ProfileType>;
|
abstract getUserProfile(tokens: any): ProfileType | Promise<ProfileType>;
|
||||||
|
|
||||||
async signin(request: ServerRequest, auth: Auth): Promise<EndpointOutput> {
|
async signin(event: RequestEvent, auth: Auth): Promise<EndpointOutput> {
|
||||||
const { method, host, query } = request;
|
const { method } = event.request;
|
||||||
const state = [`redirect=${query.get("redirect") ?? this.getUri(auth, "/", host)}`].join(",");
|
const { url } = event;
|
||||||
|
const state = [
|
||||||
|
`redirect=${url.searchParams.get("redirect") ?? this.getUri(auth, "/", url.host)}`,
|
||||||
|
].join(",");
|
||||||
const base64State = Buffer.from(state).toString("base64");
|
const base64State = Buffer.from(state).toString("base64");
|
||||||
const nonce = Math.round(Math.random() * 1000).toString(); // TODO: Generate random based on user values
|
const nonce = Math.round(Math.random() * 1000).toString(); // TODO: Generate random based on user values
|
||||||
const url = await this.getAuthorizationUrl(request, auth, base64State, nonce);
|
const authUrl = await this.getAuthorizationUrl(event, auth, base64State, nonce);
|
||||||
|
|
||||||
if (method === "POST") {
|
if (method === "POST") {
|
||||||
return {
|
return {
|
||||||
body: {
|
body: {
|
||||||
redirect: url,
|
redirect: authUrl,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -50,7 +54,7 @@ export abstract class OAuth2BaseProvider<
|
|||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: {
|
headers: {
|
||||||
Location: url,
|
Location: authUrl,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -65,17 +69,18 @@ export abstract class OAuth2BaseProvider<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async callback({ query, host }: ServerRequest, auth: Auth): Promise<CallbackResult> {
|
async callback(event: RequestEvent, auth: Auth): Promise<any> {
|
||||||
const code = query.get("code");
|
const { request, url } = event;
|
||||||
const redirect = this.getStateValue(query, "redirect");
|
const code = url.searchParams.get("code");
|
||||||
|
const redirect = this.getStateValue(url.searchParams, "redirect");
|
||||||
|
|
||||||
const tokens = await this.getTokens(code!, this.getCallbackUri(auth, host));
|
const tokens = await this.getTokens(code!, this.getCallbackUri(auth, url.host));
|
||||||
let user = await this.getUserProfile(tokens);
|
let user = await this.getUserProfile(tokens);
|
||||||
|
|
||||||
if (this.config.profile) {
|
if (this.config.profile) {
|
||||||
user = await this.config.profile(user, tokens);
|
user = await this.config.profile(user, tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [user, redirect ?? this.getUri(auth, "/", host)];
|
return [user, redirect ?? this.getUri(auth, "/", url.host)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { ServerRequest } from "@sveltejs/kit/types/endpoint";
|
import { RequestEvent } from "@sveltejs/kit/types/hooks";
|
||||||
import type { Auth } from "../auth";
|
import type { Auth } from "../auth";
|
||||||
import { ucFirst } from "../helpers";
|
import { ucFirst } from "../helpers";
|
||||||
import { OAuth2BaseProvider, OAuth2BaseProviderConfig, OAuth2Tokens } from "./oauth2.base";
|
import { OAuth2BaseProvider, OAuth2BaseProviderConfig, OAuth2Tokens } from "./oauth2.base";
|
||||||
@ -37,19 +37,19 @@ export class OAuth2Provider<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthorizationUrl({ host }: ServerRequest, auth: Auth, state: string, nonce: string) {
|
getAuthorizationUrl({ url }: RequestEvent, auth: Auth, state: string, nonce: string) {
|
||||||
const data = {
|
const data = {
|
||||||
state,
|
state,
|
||||||
nonce,
|
nonce,
|
||||||
response_type: this.config.responseType,
|
response_type: this.config.responseType,
|
||||||
client_id: this.config.clientId,
|
client_id: this.config.clientId,
|
||||||
scope: Array.isArray(this.config.scope) ? this.config.scope.join(" ") : this.config.scope!,
|
scope: Array.isArray(this.config.scope) ? this.config.scope.join(" ") : this.config.scope!,
|
||||||
redirect_uri: this.getCallbackUri(auth, host),
|
redirect_uri: this.getCallbackUri(auth, url.host),
|
||||||
...(this.config.authorizationParams ?? {}),
|
...(this.config.authorizationParams ?? {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = `${this.config.authorizationUrl}?${new URLSearchParams(data)}`;
|
const authUrl = `${this.config.authorizationUrl}?${new URLSearchParams(data)}`;
|
||||||
return url;
|
return authUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTokens(code: string, redirectUri: string): Promise<TokensType> {
|
async getTokens(code: string, redirectUri: string): Promise<TokensType> {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { OAuth2Provider, OAuth2ProviderConfig } from "./oauth2";
|
import { OAuth2Provider, OAuth2ProviderConfig } from "./oauth2";
|
||||||
import type { ProfileCallback } from "./oauth2.base";
|
|
||||||
|
|
||||||
export interface RedditProfile {
|
export interface RedditProfile {
|
||||||
is_employee: boolean;
|
is_employee: boolean;
|
||||||
|
73
src/providers/spotify.ts
Normal file
73
src/providers/spotify.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { OAuth2Provider, OAuth2ProviderConfig } from "./oauth2";
|
||||||
|
|
||||||
|
export interface SpotifyProfile {
|
||||||
|
display_name: string;
|
||||||
|
email: string;
|
||||||
|
external_urls: SpotifyProfileExternalUrls;
|
||||||
|
followers: SpotifyProfileFollowers;
|
||||||
|
href: string;
|
||||||
|
id: string;
|
||||||
|
images: SpotifyProfileImage[];
|
||||||
|
type: string;
|
||||||
|
uri: string;
|
||||||
|
// This field is only available when the current user has granted access to the user-read-private scope.
|
||||||
|
explicit_content?: SpotifyExplicitContent;
|
||||||
|
// This field is only available when the current user has granted access to the user-read-private scope.
|
||||||
|
product?: string;
|
||||||
|
// This field is only available when the current user has granted access to the user-read-private scope.
|
||||||
|
country?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpotifyExplicitContent {
|
||||||
|
filter_enabled: boolean;
|
||||||
|
filter_locked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpotifyProfileImage {
|
||||||
|
height: number;
|
||||||
|
url: string;
|
||||||
|
width: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpotifyProfileFollowers {
|
||||||
|
href: string;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpotifyProfileExternalUrls {
|
||||||
|
spotify: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpotifyTokens {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
scope: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpotifyOAuth2ProviderConfig extends OAuth2ProviderConfig<SpotifyProfile, SpotifyTokens> {
|
||||||
|
show_dialog: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultConfig: Partial<SpotifyOAuth2ProviderConfig> = {
|
||||||
|
id: "spotify",
|
||||||
|
scope: "user-read-email",
|
||||||
|
accessTokenUrl: "https://accounts.spotify.com/api/token",
|
||||||
|
authorizationUrl: "https://accounts.spotify.com/authorize",
|
||||||
|
profileUrl: "https://api.spotify.com/v1/me",
|
||||||
|
contentType: "application/x-www-form-urlencoded",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SpotifyOAuth2Provider extends OAuth2Provider<
|
||||||
|
SpotifyProfile,
|
||||||
|
SpotifyTokens,
|
||||||
|
SpotifyOAuth2ProviderConfig
|
||||||
|
> {
|
||||||
|
constructor(config: SpotifyOAuth2ProviderConfig) {
|
||||||
|
super({
|
||||||
|
...defaultConfig,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import type { ServerRequest } from "@sveltejs/kit/types/endpoint";
|
import { RequestEvent } from "@sveltejs/kit/types/hooks";
|
||||||
import type { Auth } from "../auth";
|
import type { Auth } from "../auth";
|
||||||
import type { CallbackResult } from "../types";
|
|
||||||
import { OAuth2BaseProvider, OAuth2BaseProviderConfig } from "./oauth2.base";
|
import { OAuth2BaseProvider, OAuth2BaseProviderConfig } from "./oauth2.base";
|
||||||
|
|
||||||
interface TwitterAuthProviderConfig extends OAuth2BaseProviderConfig {
|
interface TwitterAuthProviderConfig extends OAuth2BaseProviderConfig {
|
||||||
@ -38,17 +37,17 @@ export class TwitterAuthProvider extends OAuth2BaseProvider<any, any, TwitterAut
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAuthorizationUrl({ host }: ServerRequest, auth: Auth, state: string, nonce: string) {
|
async getAuthorizationUrl({ url }: RequestEvent, auth: Auth, state: string, nonce: string) {
|
||||||
const endpoint = "https://api.twitter.com/oauth/authorize";
|
const endpoint = "https://api.twitter.com/oauth/authorize";
|
||||||
|
|
||||||
const { oauthToken } = await this.getRequestToken(auth, host);
|
const { oauthToken } = await this.getRequestToken(auth, url.host);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
oauth_token: oauthToken,
|
oauth_token: oauthToken,
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = `${endpoint}?${new URLSearchParams(data)}`;
|
const authUrl = `${endpoint}?${new URLSearchParams(data)}`;
|
||||||
return url;
|
return authUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTokens(oauthToken: string, oauthVerifier: string) {
|
async getTokens(oauthToken: string, oauthVerifier: string) {
|
||||||
@ -71,10 +70,11 @@ export class TwitterAuthProvider extends OAuth2BaseProvider<any, any, TwitterAut
|
|||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async callback({ query, host }: ServerRequest, auth: Auth): Promise<CallbackResult> {
|
async callback(event: RequestEvent, auth: Auth): Promise<any> {
|
||||||
const oauthToken = query.get("oauth_token");
|
const { url } = event;
|
||||||
const oauthVerifier = query.get("oauth_verifier");
|
const oauthToken = url.searchParams.get("oauth_token");
|
||||||
const redirect = this.getStateValue(query, "redirect");
|
const oauthVerifier = url.searchParams.get("oauth_verifier");
|
||||||
|
const redirect = this.getStateValue(url.searchParams, "redirect");
|
||||||
|
|
||||||
const tokens = await this.getTokens(oauthToken!, oauthVerifier!);
|
const tokens = await this.getTokens(oauthToken!, oauthVerifier!);
|
||||||
let user = await this.getUserProfile(tokens);
|
let user = await this.getUserProfile(tokens);
|
||||||
@ -83,6 +83,6 @@ export class TwitterAuthProvider extends OAuth2BaseProvider<any, any, TwitterAut
|
|||||||
user = await this.config.profile(user, tokens);
|
user = await this.config.profile(user, tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [user, redirect ?? this.getUri(auth, "/", host)];
|
return [user, redirect ?? this.getUri(auth, "/", url.host)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
export type Profile = any;
|
export type Profile = any;
|
||||||
export type CallbackResult = [Profile, string | null];
|
export type CallbackResult = [Profile, string | null, { error: string } | null];
|
||||||
|
Loading…
Reference in New Issue
Block a user