diff --git a/package.json b/package.json index 7ac885d..e010a9b 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@vueuse/core": "^4.4.0", "bulma": "^0.9.2", "core-js": "^3.9.0", + "date-fns": "^2.21.1", "markdown-it": "^12.0.4", "markdown-it-block-embed": "^0.0.3", "nanoid": "^3.1.22", diff --git a/src/components/Authorize.vue b/src/components/Authorize.vue new file mode 100644 index 0000000..6583b0a --- /dev/null +++ b/src/components/Authorize.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/SignInGithub.vue b/src/components/SignInGithub.vue new file mode 100644 index 0000000..994163e --- /dev/null +++ b/src/components/SignInGithub.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/components/WelcomeWorld.vue b/src/components/WelcomeWorld.vue index d87c1e4..c3a7f63 100644 --- a/src/components/WelcomeWorld.vue +++ b/src/components/WelcomeWorld.vue @@ -3,14 +3,20 @@

Lite Note

- Get started +
+ Get started + about +
+

@@ -90,8 +96,6 @@ rel="noopener noreferrer" >Julien - | - about

@@ -102,8 +106,10 @@ import { defineComponent } from 'vue' import { useForm } from '@/hooks/useForm.hook' import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook' import { useFavoriteRepos } from '@/modules/repo/hooks/useFavoriteRepos.hook' +import SignInGithub from '@/components/SignInGithub.vue' export default defineComponent({ + components: { SignInGithub }, name: 'WelcomeWord', setup() { const { isLogged, username } = useGitHubLogin() @@ -119,7 +125,9 @@ export default defineComponent({ padding: 1rem; margin: auto; display: flex; + flex: 1; flex-direction: column; + justify-content: space-between; .get-started { margin: center; @@ -130,6 +138,10 @@ export default defineComponent({ h4 { text-align: center; } + + .github-login { + margin-top: 1rem; + } } footer { diff --git a/src/data/models/GithubAccessToken.ts b/src/data/models/GithubAccessToken.ts index d58931b..4e0ebe8 100644 --- a/src/data/models/GithubAccessToken.ts +++ b/src/data/models/GithubAccessToken.ts @@ -3,5 +3,10 @@ import { Model } from '@/data/models/Model' export interface GithubAccessToken extends Model { username: string - personalAccessToken: string + token: string + expirationDate: string + expiresIn: number + refreshToken: string + refreshTokenExpiresIn: number + refreshTokenExpirationDate: string } diff --git a/src/hooks/useGitHubLogin.hook.ts b/src/hooks/useGitHubLogin.hook.ts index b74a4ac..7f76d13 100644 --- a/src/hooks/useGitHubLogin.hook.ts +++ b/src/hooks/useGitHubLogin.hook.ts @@ -1,51 +1,35 @@ import { computed, ref } from 'vue' -import { DataType } from '@/data/DataType.enum' -import { GithubAccessToken } from '@/data/models/GithubAccessToken' -import { data } from '@/data/data' import { confirmMessage } from '@/utils/notif' +import { GithubToken } from '@/modules/user/interfaces/GithubToken' +import { getAccessToken, saveAccessToken } from '@/modules/user/service/signIn' -const personalAccessTokenId = 'PAT' const username = ref(null) const accessToken = ref(null) let init = true export const useGitHubLogin = () => { - const getAccessToken = async () => { - const response = await data.get< - DataType.GithubAccessToken, - GithubAccessToken - >(data.generateId(DataType.GithubAccessToken, personalAccessTokenId)) + const saveAccessTokenToLocal = async () => { + const response = await getAccessToken() username.value = response?.username || '' - accessToken.value = response?.personalAccessToken || '' - - return response + accessToken.value = response?.token || '' } if (init) { init = false - getAccessToken() + saveAccessTokenToLocal() } - const saveCredentials = async (username: string, token: string) => { - const actualPAT = await getAccessToken() + const saveCredentials = async (githubToken: GithubToken) => { + const accessToken = await saveAccessToken(githubToken) - const personalAccessToken: GithubAccessToken = { - ...actualPAT, - _id: data.generateId(DataType.GithubAccessToken, personalAccessTokenId), - $type: DataType.GithubAccessToken, - username, - personalAccessToken: token - } - - await data.add(personalAccessToken) - getAccessToken() - confirmMessage('token saved!') + await saveAccessTokenToLocal() + confirmMessage(`${accessToken.username} is logged in!`) } return { - isLogged: !!username.value && !!accessToken.value, + isLogged: !!accessToken.value, isReady: computed(() => accessToken.value !== null), username, accessToken, diff --git a/src/hooks/useRepos.hook.ts b/src/hooks/useRepos.hook.ts index cbbc643..36fbf91 100644 --- a/src/hooks/useRepos.hook.ts +++ b/src/hooks/useRepos.hook.ts @@ -1,7 +1,7 @@ -import { Octokit } from '@octokit/rest' import { RepoBase } from '@/modules/repo/interfaces/RepoBase' import { useAsyncState } from '@vueuse/core' import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook' +import { getOctokit } from '@/modules/repo/services/octo' export const useRepos = () => { const { username, accessToken } = useGitHubLogin() @@ -10,9 +10,7 @@ export const useRepos = () => { return [] } - const octokit = new Octokit({ - auth: accessToken.value - }) + const octokit = await getOctokit() const repoList = await octokit.request('GET /search/repositories', { q: `user:${username.value}`, diff --git a/src/modules/repo/services/octo.ts b/src/modules/repo/services/octo.ts new file mode 100644 index 0000000..c0df59f --- /dev/null +++ b/src/modules/repo/services/octo.ts @@ -0,0 +1,37 @@ +import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook' +import { needToRefreshToken, refreshToken } from '@/modules/user/service/signIn' +import { Octokit } from '@octokit/rest' + +let refreshingToken = false +let octokit = new Octokit() + +const sleep = async (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +export const getOctokit = async (): Promise => { + const { accessToken } = useGitHubLogin() + octokit = new Octokit({ + auth: accessToken.value + }) + + if (refreshingToken) { + await sleep(100) + return getOctokit() + } + + if (!refreshingToken) { + refreshingToken = true + if (await needToRefreshToken()) { + const accessToken = await refreshToken() + if (accessToken) { + octokit = new Octokit({ + auth: accessToken?.token + }) + } + } + refreshingToken = false + } + + return octokit +} diff --git a/src/modules/repo/services/repo.ts b/src/modules/repo/services/repo.ts index bb8605c..676ac35 100644 --- a/src/modules/repo/services/repo.ts +++ b/src/modules/repo/services/repo.ts @@ -1,9 +1,8 @@ -import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook' import { useMarkdown } from '@/hooks/useMarkdown.hook' import { useNoteCache } from '@/modules/note/hooks/useNoteCache' import { RepoFile } from '@/modules/repo/interfaces/RepoFile' import { UserSettings } from '@/modules/repo/interfaces/UserSettings' -import { Octokit } from '@octokit/rest' +import { getOctokit } from '@/modules/repo/services/octo' export const getFiles = async ( owner: string, @@ -12,12 +11,7 @@ export const getFiles = async ( if (!owner || !repo) { return [] } - - const { accessToken } = useGitHubLogin() - - const octokit = new Octokit({ - auth: accessToken.value - }) + const octokit = await getOctokit() const commits = await octokit.request('GET /repos/{owner}/{repo}/commits', { owner, @@ -47,16 +41,14 @@ export const getMainReadme = async (owner: string, repo: string) => { if (!owner || !repo) { return null } + const { render } = useMarkdown() const { getCachedNote, saveCacheNote } = useNoteCache('README') const cachedReadme = await getCachedNote() try { - const { accessToken } = useGitHubLogin() - const octokit = new Octokit({ - auth: accessToken.value - }) + const octokit = await getOctokit() const README = await octokit.repos.getReadme({ owner, @@ -101,11 +93,7 @@ export const getFileContent = async ( repo: string, sha: string ) => { - const { accessToken } = useGitHubLogin() - - const octokit = new Octokit({ - auth: accessToken.value - }) + const octokit = await getOctokit() if (!user || !repo) { null diff --git a/src/modules/repo/store/userRepo.store.ts b/src/modules/repo/store/userRepo.store.ts index f4d844a..63f3bc8 100644 --- a/src/modules/repo/store/userRepo.store.ts +++ b/src/modules/repo/store/userRepo.store.ts @@ -28,10 +28,8 @@ export const useUserRepoStore = defineStore({ async setUserRepo(newUser: string, newRepo: string) { this.user = newUser this.repo = newRepo - const [readme, files] = await Promise.all([ - getMainReadme(newUser, newRepo), - getFiles(newUser, newRepo) - ]) + const readme = await getMainReadme(newUser, newRepo) + const files = await getFiles(newUser, newRepo) this.userSettings = await getUserSettingsContent(newUser, newRepo, files) this.readme = readme diff --git a/src/modules/user/interfaces/GithubToken.ts b/src/modules/user/interfaces/GithubToken.ts new file mode 100644 index 0000000..83919bc --- /dev/null +++ b/src/modules/user/interfaces/GithubToken.ts @@ -0,0 +1,8 @@ +export interface GithubToken { + access_token: string + expires_in: number + refresh_token: string + refresh_token_expires_in: number + scope: string + token_type: 'bearer' +} diff --git a/src/modules/user/interfaces/GithubTokenError.ts b/src/modules/user/interfaces/GithubTokenError.ts new file mode 100644 index 0000000..5b43646 --- /dev/null +++ b/src/modules/user/interfaces/GithubTokenError.ts @@ -0,0 +1,5 @@ +export interface GithubTokenError { + error: string + error_description: string + error_uri: string +} diff --git a/src/modules/user/service/signIn.ts b/src/modules/user/service/signIn.ts new file mode 100644 index 0000000..c9c9cf2 --- /dev/null +++ b/src/modules/user/service/signIn.ts @@ -0,0 +1,118 @@ +import { data } from '@/data/data' +import { DataType } from '@/data/DataType.enum' +import { GithubAccessToken } from '@/data/models/GithubAccessToken' +import { GithubToken } from '@/modules/user/interfaces/GithubToken' +import { GithubTokenError } from '@/modules/user/interfaces/GithubTokenError' +import { Octokit } from '@octokit/rest' +import { addSeconds } from 'date-fns' + +const AUTHENTICATION_SERVER = 'https://litenote.li212.fr' +const personalTokenId = 'token' + +export const signIn = async ( + code: string +): Promise => { + const authenticationServerURL = new URL(AUTHENTICATION_SERVER) + authenticationServerURL.searchParams.set('code', code) + + const response = await fetch(authenticationServerURL.toString()) + const body = (await response.json()) as GithubToken | GithubTokenError + + return body +} + +export const needToRefreshToken = async () => { + const accessToken = await data.get< + DataType.GithubAccessToken, + GithubAccessToken + >(data.generateId(DataType.GithubAccessToken, personalTokenId)) + + if (!accessToken) { + return false + } + + return new Date(accessToken.expirationDate) <= new Date() +} + +export const refreshToken = async () => { + const accessToken = await data.get< + DataType.GithubAccessToken, + GithubAccessToken + >(data.generateId(DataType.GithubAccessToken, personalTokenId)) + + if (!accessToken) { + return null + } + + console.log(accessToken.refreshToken) + + if (await needToRefreshToken()) { + const authenticationServerURL = new URL(AUTHENTICATION_SERVER) + authenticationServerURL.searchParams.set('type', 'refresh') + authenticationServerURL.searchParams.set('code', accessToken.refreshToken) + + const response = await fetch(authenticationServerURL.toString()) + const githubToken = (await response.json()) as + | GithubToken + | GithubTokenError + + console.log(githubToken) + + if ('error' in githubToken) { + return null + } + + return await saveAccessToken(githubToken) + } + + return accessToken +} + +export const getAccessToken = async () => { + const response = await data.get< + DataType.GithubAccessToken, + GithubAccessToken + >(data.generateId(DataType.GithubAccessToken, personalTokenId)) + + return response +} + +export const saveAccessToken = async (githubToken: GithubToken) => { + const actualPAT = await getAccessToken() + + const expirationDate = addSeconds( + new Date(), + githubToken.expires_in + ).toISOString() + + const refreshTokenExpirationDate = addSeconds( + new Date(), + githubToken.refresh_token_expires_in + ).toISOString() + + const accessToken: GithubAccessToken = { + ...actualPAT, + _id: data.generateId(DataType.GithubAccessToken, personalTokenId), + $type: DataType.GithubAccessToken, + token: githubToken.access_token, + expiresIn: githubToken.expires_in, + expirationDate, + refreshToken: githubToken.refresh_token, + refreshTokenExpiresIn: githubToken.refresh_token_expires_in, + refreshTokenExpirationDate, + username: '' + } + + console.log(accessToken) + + const octokit = new Octokit({ + auth: accessToken?.token + }) + + const user = await octokit.request('GET /user') + accessToken.username = user.data.login + + await data.add(accessToken) + + return accessToken +} diff --git a/src/views/About.vue b/src/views/About.vue index 305a9d4..c9d9f25 100644 --- a/src/views/About.vue +++ b/src/views/About.vue @@ -1,24 +1,22 @@