diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..103ccfb --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +README.md +app/ +*.cjs diff --git a/.eslintrc-shared.cjs b/.eslintrc-shared.cjs new file mode 100644 index 0000000..3973896 --- /dev/null +++ b/.eslintrc-shared.cjs @@ -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", + }, +}; diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 5fc0ca5..cbe7bf6 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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", + ], }; diff --git a/.github/workflows/lint-on-pr.yml b/.github/workflows/lint-on-pr.yml index 28538b3..3398bbc 100644 --- a/.github/workflows/lint-on-pr.yml +++ b/.github/workflows/lint-on-pr.yml @@ -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 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 142720a..64fc5e9 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -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: diff --git a/.prettierignore b/.prettierignore index 94375f7..31cb9fd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,4 @@ -dist/** -node_modules/** +dist/ +node_modules/ +README.md +app/ diff --git a/README.md b/README.md index 74acad7..1d79b23 100644 --- a/README.md +++ b/README.md @@ -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({ diff --git a/app/.eslintignore b/app/.eslintignore new file mode 100644 index 0000000..250f8ab --- /dev/null +++ b/app/.eslintignore @@ -0,0 +1,2 @@ +README.md +*.cjs diff --git a/app/.eslintrc.cjs b/app/.eslintrc.cjs index be59143..2698697 100644 --- a/app/.eslintrc.cjs +++ b/app/.eslintrc.cjs @@ -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, - }, }; diff --git a/app/.prettierignore b/app/.prettierignore index d26fa1c..03cf74d 100644 --- a/app/.prettierignore +++ b/app/.prettierignore @@ -1,4 +1,4 @@ -.svelte-kit/** -static/** -build/** -node_modules/** +.svelte-kit/ +static/ +build/ +node_modules/ diff --git a/app/package.json b/app/package.json index f5c792f..5d99a85 100644 --- a/app/package.json +++ b/app/package.json @@ -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:../" diff --git a/app/src/global.d.ts b/app/src/global.d.ts index f4d31c5..6381d5e 100644 --- a/app/src/global.d.ts +++ b/app/src/global.d.ts @@ -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; } diff --git a/app/src/lib/appAuth.ts b/app/src/lib/appAuth.ts index 31e3203..74d1664 100644 --- a/app/src/lib/appAuth.ts +++ b/app/src/lib/appAuth.ts @@ -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) { diff --git a/app/src/routes/login.svelte b/app/src/routes/login.svelte index df5cf77..0299a7e 100644 --- a/app/src/routes/login.svelte +++ b/app/src/routes/login.svelte @@ -180,6 +180,38 @@ Sign in with Reddit + + + + + Sign in with Spotify + +

Coming soon.

diff --git a/app/src/routes/profile.svelte b/app/src/routes/profile.svelte index 9b61cbc..b7bc3fa 100644 --- a/app/src/routes/profile.svelte +++ b/app/src/routes/profile.svelte @@ -201,6 +201,47 @@ {/if} +
+ + + +
+ {#if $session.user.connections.spotify} +

Signed in as:

+ + {$session.user.connections.spotify.display_name} + {:else} +

Not signed in

+
+ Connect +
+ {/if} +
+

Session

diff --git a/app/yarn.lock b/app/yarn.lock index 6665d82..a942399 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -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== diff --git a/package.json b/package.json index 4d981d9..f6776ba 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/src/providers/index.ts b/src/providers/index.ts index 8e422f6..b33a5fc 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -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"; \ No newline at end of file +export { SpotifyOAuth2Provider } from "./spotify"; +export type { SpotifyProfile, SpotifyTokens } from "./spotify"; +export { TwitchOAuth2Provider } from "./twitch"; +export type { TwitchProfile, TwitchTokens } from "./twitch"; +export { TwitterAuthProvider } from "./twitter"; diff --git a/src/providers/spotify.ts b/src/providers/spotify.ts new file mode 100644 index 0000000..66b91e6 --- /dev/null +++ b/src/providers/spotify.ts @@ -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 { + show_dialog: boolean; +} + +const defaultConfig: Partial = { + 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, + }); + } +}