feat: spotify provider (#60)

* feat: add spotify oauth2 provider

* feat: add spotify oauth2 provider

* feat: add spotify oauth2 provider

* feat: add spotify oauth2 provider

* feat: add spotify oauth2 provider

* feat: add spotify oauth2 provider

* feat: add spotify oauth2 provider

* feat: add spotify oauth2 provider

* feat: add spotify oauth2 provider

* 🔧 Add `README.md` to `.prettierignore`

* 🎨 Update Prettier and ESLint + format codebase

* 💚 Use `--frozen-lockfile` flag in `yarn install` GitHub CI

* 🔧 Ignore app dir in ESLint script

* 🔧 Move ESLint ignore instructions to `.eslintignore` file

* 🔧 Fix various glob patterns to properly ignore dirs

* 🎨 Run Prettier

Co-authored-by: Alexander Staroselsky <Alexander.Staroselsky1@T-Mobile.com>
Co-authored-by: Dan6erbond <moravrav@gmail.com>
This commit is contained in:
Alexander Staroselsky 2022-01-12 11:00:35 -07:00 committed by GitHub
parent 9a5dbe2386
commit 962064eb48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 218 additions and 52 deletions

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
README.md
app/
*.cjs

21
.eslintrc-shared.cjs Normal file
View 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",
},
};

View File

@ -1,22 +1,9 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
plugins: ["@typescript-eslint"],
ignorePatterns: ["*.cjs", "app/**/*"],
parserOptions: {
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,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"./.eslintrc-shared.cjs",
],
};

View File

@ -15,9 +15,6 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: 14
- run: yarn install
- run: yarn lint
- run: yarn install
working-directory: app
- run: yarn lint
- run: yarn install --frozen-lockfile && yarn lint
- run: yarn install --frozen-lockfile && yarn lint
working-directory: app

View File

@ -12,7 +12,7 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: 14
- run: yarn install
- run: yarn install --frozen-lockfile
- run: yarn build
- uses: JS-DevTools/npm-publish@v1
with:

View File

@ -1,2 +1,4 @@
dist/**
node_modules/**
dist/
node_modules/
README.md
app/

View File

@ -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:
_**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
export const appAuth = new SvelteKitAuth({

2
app/.eslintignore Normal file
View File

@ -0,0 +1,2 @@
README.md
*.cjs

View File

@ -1,20 +1,15 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
extends: ["../.eslintrc.cjs"],
plugins: ["svelte3", "@typescript-eslint"],
ignorePatterns: ["*.cjs"],
overrides: [{ files: ["*.svelte"], processor: "svelte3/svelte3" }],
extends: ["../.eslintrc-shared.cjs"],
plugins: ["svelte3"],
overrides: [
{
files: ["*.svelte"],
processor: "svelte3/svelte3",
},
],
settings: {
"svelte3/typescript": () => require("typescript"),
},
parserOptions: {
sourceType: "module",
ecmaVersion: 2019,
},
env: {
browser: true,
es2017: true,
node: true,
},
};

View File

@ -1,4 +1,4 @@
.svelte-kit/**
static/**
build/**
node_modules/**
.svelte-kit/
static/
build/
node_modules/

View File

@ -5,7 +5,7 @@
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"preview": "svelte-kit preview",
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore --config .eslintrc.cjs .",
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
"format": "prettier --write --plugin-search-dir=. ."
},
"devDependencies": {
@ -33,6 +33,7 @@
"dependencies": {
"@fontsource/fira-mono": "^4.3.0",
"@fontsource/inter": "^4.3.0",
"clipboard": "^2.0.8",
"clsx": "^1.1.1",
"prismjs": "^1.23.0",
"sk-auth": "file:../"

2
app/src/global.d.ts vendored
View File

@ -11,5 +11,7 @@ interface ImportMetaEnv {
VITE_TWITTER_API_SECRET: string;
VITE_REDDIT_API_KEY: string;
VITE_REDDIT_API_SECRET: string;
VITE_SPOTIFY_CLIENT_ID: string;
VITE_SPOTIFY_CLIENT_SECRET: string;
VITE_JWT_SECRET_KEY: string;
}

View File

@ -5,6 +5,7 @@ import {
GoogleOAuth2Provider,
RedditOAuth2Provider,
TwitterAuthProvider,
SpotifyOAuth2Provider,
} from "sk-auth/providers";
export const appAuth = new SvelteKitAuth({
@ -45,6 +46,13 @@ export const appAuth = new SvelteKitAuth({
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: {
jwt(token, profile) {

View File

@ -180,6 +180,38 @@
<span>Sign in with Reddit</span>
</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")}>
Coming soon.
</p>

View File

@ -201,6 +201,47 @@
{/if}
</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>
<p class={clsx("text-lg", "mb-2")}>Session</p>

View File

@ -1467,7 +1467,7 @@ chokidar@^3.5.1:
optionalDependencies:
fsevents "~2.3.1"
clipboard@^2.0.0:
clipboard@^2.0.0, clipboard@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.8.tgz#ffc6c103dd2967a83005f3f61976aa4655a4cdba"
integrity sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==

View File

@ -21,7 +21,7 @@
"build": "rollup --config",
"dev": "rollup --config --watch",
"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=. ."
},
"keywords": [

View File

@ -1,9 +1,8 @@
export { Provider } from "./base";
export { TwitchOAuth2Provider } from "./twitch";
export type { TwitchProfile, TwitchTokens } from "./twitch";
export { GitHubOAuth2Provider } from "./github";
export type { GitHubProfile, GitHubTokens } from "./github";
export { GoogleOAuth2Provider } from "./google";
export type { GoogleProfile, GoogleTokens } from "./google";
export { TwitterAuthProvider } from "./twitter";
export { FacebookOAuth2Provider } from "./facebook";
export type { FacebookProfile, FacebookTokens } from "./facebook";
export { OAuth2BaseProvider } from "./oauth2.base";
@ -11,5 +10,8 @@ export type { ProfileCallback } from "./oauth2.base";
export { OAuth2Provider } from "./oauth2";
export { RedditOAuth2Provider } from "./reddit";
export type { RedditProfile, RedditTokens } from "./reddit";
export { GitHubOAuth2Provider } from "./github";
export type { GitHubProfile, GitHubTokens } from "./github";
export { SpotifyOAuth2Provider } from "./spotify";
export type { SpotifyProfile, SpotifyTokens } from "./spotify";
export { TwitchOAuth2Provider } from "./twitch";
export type { TwitchProfile, TwitchTokens } from "./twitch";
export { TwitterAuthProvider } from "./twitter";

73
src/providers/spotify.ts Normal file
View 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,
});
}
}