Skip to content

Commit 8c5242f

Browse files
authored
feat(traktv-service): implements authentication & adds unit testing (#117)
* fix: client signature and _call method * chore: fix stylelint * feat: initial login POC * chore: refactor * feat: unit test base-client * fix: pnpm.lock file * fix: imports in components
1 parent f09cc40 commit 8c5242f

File tree

79 files changed

+2219
-893
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+2219
-893
lines changed

.eslintrc.cjs

+2
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@ module.exports = {
1212
ignorePatterns: ['^v-.*'],
1313
},
1414
],
15+
'vitest/no-hooks': 'off',
16+
'vitest/max-expects': ['warn', { max: 10 }],
1517
},
1618
};

.stylelintrc.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
{
2-
"extends": ["@dvcol/stylelint-plugin-presets/config/vue"]
2+
"extends": ["@dvcol/stylelint-plugin-presets/config/vue"],
3+
"rules": {
4+
"@dvcol/progress": null
5+
}
36
}

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"lint": "eslint .",
4747
"lint:fix": "pnpm run lint --fix",
4848
"style": "stylelint **/*.{vue,css,scss,less,html} --go '{\"gitignore\":true}'",
49-
"style:fix": "yarn style --fix",
49+
"style:fix": "pnpm run style --fix",
5050
"release": "standard-version",
5151
"release:changelog": "extract-changelog-release > RELEASE.md"
5252
},
@@ -61,7 +61,7 @@
6161
"@commitlint/cli": "^18.0.0",
6262
"@commitlint/config-conventional": "^18.0.0",
6363
"@dvcol/eslint-plugin-presets": "^1.3.10",
64-
"@dvcol/stylelint-plugin-presets": "^2.1.0",
64+
"@dvcol/stylelint-plugin-presets": "^2.1.2",
6565
"@tsconfig/node20": "^20.1.2",
6666
"@types/chrome": "^0.0.259",
6767
"@types/fs-extra": "^11.0.1",
@@ -106,7 +106,7 @@
106106
"rollup": "^4.8.0",
107107
"sass": "^1.66.1",
108108
"standard-version": "^9.5.0",
109-
"stylelint": "^16.0.0",
109+
"stylelint": "^16.2.0",
110110
"typescript": "^5.3.3",
111111
"unplugin-vue-ce": "^1.0.0-beta.19",
112112
"vite": "^5.0.10",

pnpm-lock.yaml

+146-401
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/manifest.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import { getDirName, isDev, port, resolveParent } from './utils';
66

77
import type { Manifest } from 'webextension-polyfill';
88

9+
export const Endpoints = {
10+
Production: 'https://api.trakt.tv',
11+
Staging: 'https://api-staging.trakt.tv',
12+
} as const;
13+
914
export const manifest: Manifest.WebExtensionManifest = {
1015
manifest_version: 3,
1116
name: pkg.title || pkg.name,
@@ -29,7 +34,14 @@ export const manifest: Manifest.WebExtensionManifest = {
2934
background: {
3035
service_worker: 'scripts/background.js',
3136
},
32-
permissions: ['storage'],
37+
permissions: ['storage', 'tabs'],
38+
web_accessible_resources: [
39+
{
40+
resources: ['/views/options/index.html'],
41+
matches: [`${Endpoints.Production}/*`, `${Endpoints.Staging}/*`],
42+
},
43+
],
44+
host_permissions: [`${Endpoints.Production}/*`, `${Endpoints.Staging}/*`],
3345
content_security_policy: {
3446
// Adds localhost for vite hot reload
3547
extension_pages: isDev

src/components/AppComponent.vue

+12-13
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
11
<script setup lang="ts">
2-
import { ref } from 'vue';
32
import { RouterView } from 'vue-router';
43
54
import { NavbarComponent } from '~/components/common';
6-
import { LoginComponent } from '~/components/views/login';
7-
5+
import { useSettingsStore } from '~/stores/settings.store';
86
import { useI18n } from '~/utils';
97
108
const i18n = useI18n('global');
11-
const isAuthenticated = ref(true);
9+
const { isAuthenticated } = useSettingsStore();
1210
</script>
1311

1412
<template>
15-
<LoginComponent v-if="!isAuthenticated" />
16-
<template v-else>
17-
<header>
18-
<NavbarComponent />
19-
</header>
20-
<main>
21-
<RouterView />
22-
</main>
23-
</template>
13+
<header v-if="isAuthenticated">
14+
<NavbarComponent />
15+
</header>
16+
<main>
17+
<RouterView />
18+
</main>
2419
</template>
2520

2621
<style lang="scss" scoped>
@@ -31,6 +26,10 @@ header {
3126
}
3227
3328
main {
29+
display: flex;
30+
align-items: center;
31+
justify-content: center;
32+
height: 100%;
3433
padding: 0 2rem;
3534
}
3635
</style>

src/components/common/navbar/NavbarComponent.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const routes = [Route.Progress, Route.Calendar, Route.History, Route.List, Route
2828

2929
<style lang="scss" scoped>
3030
nav {
31-
margin-top: 2rem;
31+
margin: 1rem 0;
3232
font-size: 12px;
3333
text-align: center;
3434
}

src/components/views/login/LoginComponent.vue

+64-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,74 @@
11
<script lang="ts" setup>
2+
import { NButton, NFlex } from 'naive-ui';
3+
4+
import { onMounted, ref } from 'vue';
5+
6+
import { useRoute, useRouter } from 'vue-router';
7+
8+
import type { LoginAuthResponse } from '~/models/login/login-auth-response';
9+
10+
import type { TraktDeviceAuthentication } from '~/models/trakt/trakt-authentication.model';
11+
12+
import { isLoginAuthResponseSuccess } from '~/models/login/login-auth-response';
13+
14+
import { traktService } from '~/services/trakt-client/trakt-client.service';
215
import { useI18n } from '~/utils';
316
17+
type LoginQueryParams = {
18+
redirect: string;
19+
} & LoginAuthResponse;
20+
421
const i18n = useI18n('global');
22+
23+
const authResponse = ref<LoginQueryParams>();
24+
const route = useRoute();
25+
const router = useRouter();
26+
27+
onMounted(async () => {
28+
authResponse.value = route.query as LoginQueryParams;
29+
30+
if (isLoginAuthResponseSuccess(authResponse.value)) {
31+
const response = await traktService.exchangeCodeForToken(authResponse.value.code);
32+
if (authResponse.value.redirect) await router.push(authResponse.value.redirect);
33+
}
34+
});
35+
36+
const auth = ref<TraktDeviceAuthentication>();
37+
38+
const redirect = async () => {
39+
try {
40+
const response = await traktService.redirectToAuthentication();
41+
await chrome.tabs.create({ url: response.url });
42+
} catch (error) {
43+
console.error('Error:', error);
44+
}
45+
};
46+
47+
const login = async () => {
48+
const device = await traktService.getDeviceCode();
49+
auth.value = device;
50+
51+
const response = await traktService.pollWithDeviceCode(device);
52+
console.info('response', response);
53+
};
54+
55+
const writeToClipboard = (text: string) => {
56+
navigator.clipboard.writeText(text);
57+
};
558
</script>
659

760
<template>
8-
<img alt="Vue logo" class="logo" src="/assets/logo.svg" width="125" height="125" />
9-
<span>This is a login component</span>
61+
<NFlex vertical justify="space-around" align="center">
62+
<img alt="Vue logo" class="logo" src="/assets/logo.svg" width="125" height="125" />
63+
<span>This is a login component</span>
64+
<NButton @click="redirect">Redirect Url</NButton>
65+
<NButton @click="login">Generate Codes</NButton>
66+
<template v-if="auth">
67+
{{ JSON.stringify(auth, undefined, 2) }}
68+
<NButton @click="writeToClipboard(auth.user_code)">{{ auth.user_code }}</NButton>
69+
<NButton tag="a" :href="auth.verification_url" target="_blank">{{ auth.verification_url }}</NButton>
70+
</template>
71+
</NFlex>
1072
</template>
1173

1274
<style lang="scss" scoped>

src/components/web/ContainerComponent.ce.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const theme = computed(() => (isDark.value ? darkTheme : lightTheme));
1414

1515
<template>
1616
<div id="trakt-extension-root">
17-
<NConfigProvider :theme="theme">
17+
<NConfigProvider :theme="theme" abstract>
1818
<AppComponent />
1919
</NConfigProvider>
2020
</div>
@@ -51,4 +51,8 @@ const theme = computed(() => (isDark.value ? darkTheme : lightTheme));
5151
-webkit-font-smoothing: antialiased;
5252
-moz-osx-font-smoothing: grayscale;
5353
}
54+
55+
#trakt-extension-root {
56+
height: 100%;
57+
}
5458
</style>
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export type LoginAuthResponseFailure = {
2+
error: string;
3+
error_description: string;
4+
state: string;
5+
};
6+
7+
export type LoginAuthRequestSuccess = {
8+
code: string;
9+
};
10+
11+
export type LoginAuthResponse<T extends 'success' | 'failure' | 'any' = 'any'> = T extends 'success'
12+
? LoginAuthRequestSuccess
13+
: T extends 'failure'
14+
? LoginAuthResponseFailure
15+
: LoginAuthRequestSuccess | LoginAuthResponseFailure;
16+
17+
export const isLoginAuthResponseSuccess = (response: LoginAuthResponse): response is LoginAuthRequestSuccess => 'code' in response;

src/models/trakt-authentication.model.ts src/models/trakt/trakt-authentication.model.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
export type TraktAuthentication = {
22
access_token: string;
3-
token_type: string;
4-
expires_in: number;
53
refresh_token: string;
6-
scope: string;
74
created_at: number;
5+
expires_in: number;
6+
token_type: string;
7+
scope: string;
88
};
99

1010
export type TraktDeviceAuthentication = {

src/models/trakt-calendar.model.ts src/models/trakt/trakt-calendar.model.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { TraktEpisode } from '~/models/trakt-episode.model';
2-
import type { TraktMovie } from '~/models/trakt-movie.model';
3-
import type { TraktShow } from '~/models/trakt-show.model';
1+
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
2+
import type { TraktMovie } from '~/models/trakt/trakt-movie.model';
3+
import type { TraktShow } from '~/models/trakt/trakt-show.model';
44

55
export type TraktCalendarShow = {
66
/** Timestamp in ISO 8601 GMT format (YYYY-MM-DD'T'hh:mm:ss.sssZ) */

src/models/trakt-checkin.model.ts src/models/trakt/trakt-checkin.model.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { TraktSharing } from '~/models/trakt-entity.model';
2-
import type { TraktEpisode } from '~/models/trakt-episode.model';
3-
import type { TraktMovie } from '~/models/trakt-movie.model';
4-
import type { TraktShow } from '~/models/trakt-show.model';
1+
import type { TraktSharing } from '~/models/trakt/trakt-entity.model';
2+
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
3+
import type { TraktMovie } from '~/models/trakt/trakt-movie.model';
4+
import type { TraktShow } from '~/models/trakt/trakt-show.model';
55

66
type BaseTraktCheckin = {
77
movie: TraktMovie;

0 commit comments

Comments
 (0)