Merge branch 'main' of github.com:lite-note/lite-note into main
This commit is contained in:
@@ -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",
|
||||
|
||||
44
src/components/Authorize.vue
Normal file
44
src/components/Authorize.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="authorize">
|
||||
<div v-if="hasError">An error occured when sign in...</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook'
|
||||
import { defineComponent, onBeforeMount, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { signIn } from '@/modules/user/service/signIn'
|
||||
|
||||
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 body = await signIn(code.toString())
|
||||
|
||||
if ('error' in body) {
|
||||
hasError.value = true
|
||||
} else {
|
||||
body.access_token
|
||||
saveCredentials(body)
|
||||
}
|
||||
|
||||
router.push({ name: 'Home' })
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
code,
|
||||
hasError
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
41
src/components/SignInGithub.vue
Normal file
41
src/components/SignInGithub.vue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
const accessToken = ref<string | null>(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,
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
37
src/modules/repo/services/octo.ts
Normal file
37
src/modules/repo/services/octo.ts
Normal file
@@ -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<Octokit> => {
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
8
src/modules/user/interfaces/GithubToken.ts
Normal file
8
src/modules/user/interfaces/GithubToken.ts
Normal 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'
|
||||
}
|
||||
5
src/modules/user/interfaces/GithubTokenError.ts
Normal file
5
src/modules/user/interfaces/GithubTokenError.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface GithubTokenError {
|
||||
error: string
|
||||
error_description: string
|
||||
error_uri: string
|
||||
}
|
||||
118
src/modules/user/service/signIn.ts
Normal file
118
src/modules/user/service/signIn.ts
Normal file
@@ -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<GithubToken | GithubTokenError> => {
|
||||
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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
margin-top: 1rem;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.authorize {
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4355,6 +4355,11 @@ data-urls@^1.0.0, data-urls@^1.1.0:
|
||||
whatwg-mimetype "^2.2.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:
|
||||
version "0.1.20"
|
||||
resolved "https://registry.yarnpkg.com/deasync/-/deasync-0.1.20.tgz#546fd2660688a1eeed55edce2308c5cf7104f9da"
|
||||
|
||||
Reference in New Issue
Block a user