Feat/GitHub auth (#6)

*  (sign in) create sign in to github button

*  (github login) login via github button

* 💄 (homepage)

*  (date fns)

*  (github login) refresh token when token expired
This commit is contained in:
Julien Calixte
2021-05-01 23:35:38 +02:00
committed by GitHub
parent f934562834
commit 0e52b16b1b
12 changed files with 259 additions and 34 deletions

View File

@@ -16,6 +16,7 @@
"@vueuse/core": "^4.4.0", "@vueuse/core": "^4.4.0",
"bulma": "^0.9.2", "bulma": "^0.9.2",
"core-js": "^3.9.0", "core-js": "^3.9.0",
"date-fns": "^2.21.1",
"markdown-it": "^12.0.4", "markdown-it": "^12.0.4",
"markdown-it-block-embed": "^0.0.3", "markdown-it-block-embed": "^0.0.3",
"nanoid": "^3.1.22", "nanoid": "^3.1.22",

View File

@@ -0,0 +1,56 @@
<template>
<div class="authorize">
<div v-if="hasError">An error occured when sign in...</div>
</div>
</template>
<script lang="ts">
import { GithubToken } from '@/modules/user/interfaces/GithubToken'
import { GithubTokenError } from '@/modules/user/interfaces/GithubTokenError'
import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook'
import { defineComponent, onBeforeMount, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const AUTHENTICATION_SERVER = 'https://litenote.li212.fr'
export default defineComponent({
name: 'Authorize',
setup() {
const route = useRoute()
const router = useRouter()
const { saveCredentials } = useGitHubLogin()
const code = route.query.code
let hasError = ref(false)
onBeforeMount(async () => {
if (code) {
const authenticationServerURL = new URL(AUTHENTICATION_SERVER)
authenticationServerURL.searchParams.set('code', code.toString())
const response = await fetch(authenticationServerURL.toString())
const body = (await response.json()) as GithubToken | GithubTokenError
if ('error' in body) {
hasError.value = true
} else {
body.access_token
saveCredentials(body)
}
router.push({ name: 'Home' })
}
})
return {
code,
hasError
}
}
})
</script>
<style scoped lang="scss">
.authorize {
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<a :href="url" class="sign-in-github button is-primary">
<span>
Sign in with
<img src="@/assets/icons/github.svg" alt="GitHub" />
</span>
</a>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
const GITHUB_URL = 'https://github.com/login/oauth/authorize'
const CLIENT_ID = 'Iv1.12dc43d013ce3623'
const SCOPE = 'repo'
export default defineComponent({
name: 'SignInGitHub',
setup() {
const url = new URL(GITHUB_URL)
url.searchParams.set('client_id', CLIENT_ID)
url.searchParams.set('scope', SCOPE)
return {
url
}
}
})
</script>
<style scoped lang="scss">
.sign-in-github {
span {
display: flex;
align-items: flex-end;
gap: 0.5rem;
}
}
</style>

View File

@@ -3,6 +3,7 @@
<div class="columns is-vcentered"> <div class="columns is-vcentered">
<div class="column get-started"> <div class="column get-started">
<h3 class="title is-3">Lite Note</h3> <h3 class="title is-3">Lite Note</h3>
<div class="buttons is-centered">
<router-link <router-link
:to="{ :to="{
name: 'Home', name: 'Home',
@@ -11,6 +12,11 @@
class="button is-primary" class="button is-primary"
>Get started</router-link >Get started</router-link
> >
<router-link class="button" :to="{ name: 'About' }"
>about</router-link
>
</div>
<sign-in-github class="github-login" />
</div> </div>
<div class="column"> <div class="column">
<p> <p>
@@ -90,8 +96,6 @@
rel="noopener noreferrer" rel="noopener noreferrer"
>Julien</a >Julien</a
> >
|
<router-link :to="{ name: 'About' }">about</router-link>
</p> </p>
</footer> </footer>
</div> </div>
@@ -102,8 +106,10 @@ import { defineComponent } from 'vue'
import { useForm } from '@/hooks/useForm.hook' import { useForm } from '@/hooks/useForm.hook'
import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook' import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook'
import { useFavoriteRepos } from '@/modules/repo/hooks/useFavoriteRepos.hook' import { useFavoriteRepos } from '@/modules/repo/hooks/useFavoriteRepos.hook'
import SignInGithub from '@/components/SignInGithub.vue'
export default defineComponent({ export default defineComponent({
components: { SignInGithub },
name: 'WelcomeWord', name: 'WelcomeWord',
setup() { setup() {
const { isLogged, username } = useGitHubLogin() const { isLogged, username } = useGitHubLogin()
@@ -119,7 +125,9 @@ export default defineComponent({
padding: 1rem; padding: 1rem;
margin: auto; margin: auto;
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
justify-content: space-between;
.get-started { .get-started {
margin: center; margin: center;
@@ -130,6 +138,10 @@ export default defineComponent({
h4 { h4 {
text-align: center; text-align: center;
} }
.github-login {
margin-top: 1rem;
}
} }
footer { footer {

View File

@@ -3,5 +3,10 @@ import { Model } from '@/data/models/Model'
export interface GithubAccessToken extends Model<DataType.GithubAccessToken> { export interface GithubAccessToken extends Model<DataType.GithubAccessToken> {
username: string username: string
personalAccessToken: string token: string
expirationDate: string
expiresIn: number
refreshToken: string
refreshTokenExpiresIn: number
refreshTokenExpirationDate: string
} }

View File

@@ -1,11 +1,14 @@
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { DataType } from '@/data/DataType.enum' import { DataType } from '@/data/DataType.enum'
import { GithubAccessToken } from '@/data/models/GithubAccessToken'
import { data } from '@/data/data' import { data } from '@/data/data'
import { confirmMessage } from '@/utils/notif' import { confirmMessage } from '@/utils/notif'
import { GithubAccessToken } from '@/data/models/GithubAccessToken'
import { Octokit } from '@octokit/rest'
import { GithubToken } from '@/modules/user/interfaces/GithubToken'
import { addMilliseconds } from 'date-fns'
const personalAccessTokenId = 'PAT' const personalTokenId = 'token'
const username = ref<string | null>(null) const username = ref<string | null>(null)
const accessToken = ref<string | null>(null) const accessToken = ref<string | null>(null)
@@ -16,9 +19,9 @@ export const useGitHubLogin = () => {
const response = await data.get< const response = await data.get<
DataType.GithubAccessToken, DataType.GithubAccessToken,
GithubAccessToken GithubAccessToken
>(data.generateId(DataType.GithubAccessToken, personalAccessTokenId)) >(data.generateId(DataType.GithubAccessToken, personalTokenId))
username.value = response?.username || '' username.value = response?.username || ''
accessToken.value = response?.personalAccessToken || '' accessToken.value = response?.token || ''
return response return response
} }
@@ -28,24 +31,45 @@ export const useGitHubLogin = () => {
getAccessToken() getAccessToken()
} }
const saveCredentials = async (username: string, token: string) => { const saveCredentials = async (githubToken: GithubToken) => {
const actualPAT = await getAccessToken() const actualPAT = await getAccessToken()
const personalAccessToken: GithubAccessToken = { const accessToken: GithubAccessToken = {
...actualPAT, ...actualPAT,
_id: data.generateId(DataType.GithubAccessToken, personalAccessTokenId), _id: data.generateId(DataType.GithubAccessToken, personalTokenId),
$type: DataType.GithubAccessToken, $type: DataType.GithubAccessToken,
username, token: githubToken.access_token,
personalAccessToken: token expiresIn: githubToken.expires_in,
expirationDate: addMilliseconds(
new Date(),
githubToken.expires_in
).toISOString(),
refreshToken: githubToken.refresh_token,
refreshTokenExpiresIn: githubToken.refresh_token_expires_in,
refreshTokenExpirationDate: addMilliseconds(
new Date(),
githubToken.refresh_token_expires_in
).toISOString(),
username: ''
} }
await data.add(personalAccessToken) console.log(accessToken)
const octokit = new Octokit({
auth: accessToken.token
})
const user = await octokit.request('GET /user')
accessToken.username = user.data.login
username.value = accessToken.username
await data.add(accessToken)
getAccessToken() getAccessToken()
confirmMessage('token saved!') confirmMessage(`${accessToken.username} is logged in!`)
} }
return { return {
isLogged: !!username.value && !!accessToken.value, isLogged: !!accessToken.value,
isReady: computed(() => accessToken.value !== null), isReady: computed(() => accessToken.value !== null),
username, username,
accessToken, accessToken,

View File

@@ -1,9 +1,65 @@
import { data } from '@/data/data'
import { DataType } from '@/data/DataType.enum'
import { GithubAccessToken } from '@/data/models/GithubAccessToken'
import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook' import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook'
import { useMarkdown } from '@/hooks/useMarkdown.hook' import { useMarkdown } from '@/hooks/useMarkdown.hook'
import { useNoteCache } from '@/modules/note/hooks/useNoteCache' import { useNoteCache } from '@/modules/note/hooks/useNoteCache'
import { RepoFile } from '@/modules/repo/interfaces/RepoFile' import { RepoFile } from '@/modules/repo/interfaces/RepoFile'
import { UserSettings } from '@/modules/repo/interfaces/UserSettings' import { UserSettings } from '@/modules/repo/interfaces/UserSettings'
import { GithubToken } from '@/modules/user/interfaces/GithubToken'
import { GithubTokenError } from '@/modules/user/interfaces/GithubTokenError'
import { Octokit } from '@octokit/rest' import { Octokit } from '@octokit/rest'
import { addMilliseconds } from 'date-fns'
const personalTokenId = 'token'
const GITHUB_URL = 'https://github.com/login/oauth/access_token'
const refreshToken = async () => {
const accessToken = await data.get<
DataType.GithubAccessToken,
GithubAccessToken
>(data.generateId(DataType.GithubAccessToken, personalTokenId))
if (!accessToken) {
return
}
if (new Date(accessToken.expirationDate) >= new Date()) {
const response = await fetch(GITHUB_URL, {
body: JSON.stringify({
refresh_token: accessToken.refreshToken,
grant_type: 'refresh_token'
})
})
const githubToken = (await response.json()) as
| GithubToken
| GithubTokenError
if ('error' in githubToken) {
return
}
const updatedAccessToken: GithubAccessToken = {
...accessToken,
token: githubToken.access_token,
expiresIn: githubToken.expires_in,
expirationDate: addMilliseconds(
new Date(),
githubToken.expires_in
).toISOString(),
refreshToken: githubToken.refresh_token,
refreshTokenExpiresIn: githubToken.refresh_token_expires_in,
refreshTokenExpirationDate: addMilliseconds(
new Date(),
githubToken.refresh_token_expires_in
).toISOString()
}
await data.add<DataType.GithubAccessToken>({
...updatedAccessToken
})
}
}
export const getFiles = async ( export const getFiles = async (
owner: string, owner: string,
@@ -12,6 +68,7 @@ export const getFiles = async (
if (!owner || !repo) { if (!owner || !repo) {
return [] return []
} }
await refreshToken()
const { accessToken } = useGitHubLogin() const { accessToken } = useGitHubLogin()
@@ -47,6 +104,8 @@ export const getMainReadme = async (owner: string, repo: string) => {
if (!owner || !repo) { if (!owner || !repo) {
return null return null
} }
await refreshToken()
const { render } = useMarkdown() const { render } = useMarkdown()
const { getCachedNote, saveCacheNote } = useNoteCache('README') const { getCachedNote, saveCacheNote } = useNoteCache('README')
@@ -111,6 +170,8 @@ export const getFileContent = async (
null null
} }
await refreshToken()
const file = await octokit.request( const file = await octokit.request(
'GET /repos/{owner}/{repo}/git/blobs/{file_sha}', 'GET /repos/{owner}/{repo}/git/blobs/{file_sha}',
{ {

View File

@@ -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'
}

View File

@@ -0,0 +1,5 @@
export interface GithubTokenError {
error: string
error_description: string
error_uri: string
}

View File

@@ -1,24 +1,22 @@
<template> <template>
<div class="about content"> <div class="about content">
<h1 class="title is-1">Lite Note</h1>
<go-back /> <go-back />
<hr />
<main> <main>
Work in progress: Work in progress:
<ol> <ol>
<li>header note for quick actions </li> <li>header note for quick actions </li>
<li>offline notes</li> <li>offline notes </li>
<li>full path resolver between notes</li> <li>full path resolver between notes </li>
<li>draft & fleeting notes folders</li> <li>draft & fleeting notes folders </li>
<li>login with GitHub "Personal Access Token" tutorial</li> <li>private & public notes tutorial </li>
<li>private & public notes tutorial</li>
<li> <li>
custom settings: custom settings:
<ul> <ul>
<li>light & dark modes</li> <li>light & dark modes </li>
<li>font families</li> <li>font families </li>
</ul> </ul>
</li> </li>
<li>Share PDF section of a note (and its subnotes) </li>
</ol> </ol>
</main> </main>
</div> </div>

View File

@@ -1,5 +1,6 @@
<template> <template>
<div class="home content" v-if="!user || !repo"> <div class="home content" v-if="!user || !repo">
<authorize class="authorize" />
<new-version class="new-version" /> <new-version class="new-version" />
<welcome-world /> <welcome-world />
</div> </div>
@@ -10,6 +11,7 @@
import { defineComponent, defineAsyncComponent, computed } from 'vue' import { defineComponent, defineAsyncComponent, computed } from 'vue'
import { useQueryStackedNotes } from '@/hooks/useQueryStackedNotes.hook' import { useQueryStackedNotes } from '@/hooks/useQueryStackedNotes.hook'
import NewVersion from '@/components/NewVersion.vue' import NewVersion from '@/components/NewVersion.vue'
import Authorize from '@/components/Authorize.vue'
const FluxNote = defineAsyncComponent(() => import('@/components/FluxNote.vue')) const FluxNote = defineAsyncComponent(() => import('@/components/FluxNote.vue'))
@@ -22,7 +24,8 @@ export default defineComponent({
components: { components: {
WelcomeWorld, WelcomeWorld,
FluxNote, FluxNote,
NewVersion NewVersion,
Authorize
}, },
props: { props: {
user: { type: String, required: false, default: '' }, user: { type: String, required: false, default: '' },
@@ -47,7 +50,13 @@ export default defineComponent({
align-items: center; align-items: center;
.new-version { .new-version {
position: absolute;
margin-top: 1rem; margin-top: 1rem;
} }
} }
.authorize {
position: absolute;
margin: auto;
}
</style> </style>

View File

@@ -4284,6 +4284,11 @@ data-urls@^1.0.0, data-urls@^1.1.0:
whatwg-mimetype "^2.2.0" whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0" whatwg-url "^7.0.0"
date-fns@^2.21.1:
version "2.21.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.1.tgz#679a4ccaa584c0706ea70b3fa92262ac3009d2b0"
integrity sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA==
deasync@^0.1.15: deasync@^0.1.15:
version "0.1.20" version "0.1.20"
resolved "https://registry.yarnpkg.com/deasync/-/deasync-0.1.20.tgz#546fd2660688a1eeed55edce2308c5cf7104f9da" resolved "https://registry.yarnpkg.com/deasync/-/deasync-0.1.20.tgz#546fd2660688a1eeed55edce2308c5cf7104f9da"