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

@@ -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,14 +3,20 @@
<div class="columns is-vcentered">
<div class="column get-started">
<h3 class="title is-3">Lite Note</h3>
<router-link
:to="{
name: 'Home',
params: { user: 'lite-note', repo: 'getting-started' }
}"
class="button is-primary"
>Get started</router-link
>
<div class="buttons is-centered">
<router-link
:to="{
name: 'Home',
params: { user: 'lite-note', repo: 'getting-started' }
}"
class="button is-primary"
>Get started</router-link
>
<router-link class="button" :to="{ name: 'About' }"
>about</router-link
>
</div>
<sign-in-github class="github-login" />
</div>
<div class="column">
<p>
@@ -90,8 +96,6 @@
rel="noopener noreferrer"
>Julien</a
>
|
<router-link :to="{ name: 'About' }">about</router-link>
</p>
</footer>
</div>
@@ -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 {

View File

@@ -3,5 +3,10 @@ import { Model } from '@/data/models/Model'
export interface GithubAccessToken extends Model<DataType.GithubAccessToken> {
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 { DataType } from '@/data/DataType.enum'
import { GithubAccessToken } from '@/data/models/GithubAccessToken'
import { data } from '@/data/data'
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 accessToken = ref<string | null>(null)
@@ -16,9 +19,9 @@ export const useGitHubLogin = () => {
const response = await data.get<
DataType.GithubAccessToken,
GithubAccessToken
>(data.generateId(DataType.GithubAccessToken, personalAccessTokenId))
>(data.generateId(DataType.GithubAccessToken, personalTokenId))
username.value = response?.username || ''
accessToken.value = response?.personalAccessToken || ''
accessToken.value = response?.token || ''
return response
}
@@ -28,24 +31,45 @@ export const useGitHubLogin = () => {
getAccessToken()
}
const saveCredentials = async (username: string, token: string) => {
const saveCredentials = async (githubToken: GithubToken) => {
const actualPAT = await getAccessToken()
const personalAccessToken: GithubAccessToken = {
const accessToken: GithubAccessToken = {
...actualPAT,
_id: data.generateId(DataType.GithubAccessToken, personalAccessTokenId),
_id: data.generateId(DataType.GithubAccessToken, personalTokenId),
$type: DataType.GithubAccessToken,
username,
personalAccessToken: token
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(),
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()
confirmMessage('token saved!')
confirmMessage(`${accessToken.username} is logged in!`)
}
return {
isLogged: !!username.value && !!accessToken.value,
isLogged: !!accessToken.value,
isReady: computed(() => accessToken.value !== null),
username,
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 { 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 { GithubToken } from '@/modules/user/interfaces/GithubToken'
import { GithubTokenError } from '@/modules/user/interfaces/GithubTokenError'
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 (
owner: string,
@@ -12,6 +68,7 @@ export const getFiles = async (
if (!owner || !repo) {
return []
}
await refreshToken()
const { accessToken } = useGitHubLogin()
@@ -47,6 +104,8 @@ export const getMainReadme = async (owner: string, repo: string) => {
if (!owner || !repo) {
return null
}
await refreshToken()
const { render } = useMarkdown()
const { getCachedNote, saveCacheNote } = useNoteCache('README')
@@ -111,6 +170,8 @@ export const getFileContent = async (
null
}
await refreshToken()
const file = await octokit.request(
'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>
<div class="about content">
<h1 class="title is-1">Lite Note</h1>
<go-back />
<hr />
<main>
Work in progress:
<ol>
<li>header note for quick actions </li>
<li>offline notes</li>
<li>full path resolver between notes</li>
<li>draft & fleeting notes folders</li>
<li>login with GitHub "Personal Access Token" tutorial</li>
<li>private & public notes tutorial</li>
<li>header note for quick actions </li>
<li>offline notes </li>
<li>full path resolver between notes </li>
<li>draft & fleeting notes folders </li>
<li>private & public notes tutorial </li>
<li>
custom settings:
<ul>
<li>light & dark modes</li>
<li>font families</li>
<li>light & dark modes </li>
<li>font families </li>
</ul>
</li>
<li>Share PDF section of a note (and its subnotes) </li>
</ol>
</main>
</div>

View File

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