Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions frontend/src/pages/AuthCallback.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<template>
<q-page class="flex flex-center">
<q-spinner size="50px" />
</q-page>
</template>

<script setup>
import { onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth.js'
import { exchangeGitlabCode } from '../services/gitlab.js'

const route = useRoute()
const router = useRouter()
const auth = useAuthStore()

onMounted(async () => {
if (route.path.includes('github')) {
const token = route.query.token
if (token) {
await auth.loginWithToken('github', token)
router.push('/repos')
} else {
router.push('/login?error=no_token')
}
} else if (route.path.includes('gitlab')) {
const code = route.query.code
if (code) {
const gitlabHost = sessionStorage.getItem('gitlab_host') || 'gitlab.com'
const { access_token } = await exchangeGitlabCode(code)
await auth.loginWithToken('gitlab', access_token, gitlabHost)
router.push('/repos')
} else {
router.push('/login?error=no_code')
}
}
})
</script>
69 changes: 69 additions & 0 deletions frontend/src/pages/LoginPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<q-page class="flex flex-center">
<q-card style="min-width: 360px">
<q-card-section>
<div class="text-h6">Sign in</div>
</q-card-section>

<q-card-section v-if="errorMessage">
<q-banner class="bg-negative text-white" rounded>
{{ errorMessage }}
</q-banner>
</q-card-section>

<q-card-section class="q-gutter-md">
<q-btn
color="dark"
icon="fab fa-github"
label="Login with GitHub"
class="full-width"
@click="loginGithub"
/>
</q-card-section>

<q-separator />

<q-card-section class="q-gutter-md">
<q-input v-model="gitlabHost" label="GitLab host" outlined dense />
<q-btn
color="orange"
icon="fab fa-gitlab"
label="Login with GitLab"
class="full-width"
@click="loginGitlab"
/>
</q-card-section>
</q-card>
</q-page>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import { startGitlabLogin } from '../services/gitlab.js'

const route = useRoute()

const gitlabHost = ref('gitlab.com')

const errorMessage = computed(() => {
if (!route.query.error) return null
const messages = {
no_token: 'Authentication failed: no token received.',
no_code: 'Authentication failed: no code received.',
}
return messages[route.query.error] ?? `Authentication error: ${route.query.error}`
})

function loginGithub() {
const workerUrl = process.env.WORKER_URL
const clientId = process.env.GITHUB_CLIENT_ID
const redirectUri = `${workerUrl}/auth/github/callback`
const params = new URLSearchParams({ client_id: clientId, scope: 'repo user', redirect_uri: redirectUri })
window.location.href = `https://github.com/login/oauth/authorize?${params}`
}

async function loginGitlab() {
await startGitlabLogin(gitlabHost.value, process.env.GITLAB_CLIENT_ID)
}
</script>
7 changes: 6 additions & 1 deletion frontend/src/router/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ const routes = [
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/IndexPage.vue') }],
children: [
{ path: '', component: () => import('pages/IndexPage.vue') },
{ path: '/login', component: () => import('pages/LoginPage.vue') },
{ path: '/auth/github', component: () => import('pages/AuthCallback.vue') },
{ path: '/auth/gitlab', component: () => import('pages/AuthCallback.vue') },
],
},
{
path: '/:catchAll(.*)*',
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/services/github.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const BASE = 'https://api.github.com'

export async function fetchCurrentUser(token) {
const res = await fetch(`${BASE}/user`, {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' },
})
return res.json()
}
61 changes: 61 additions & 0 deletions frontend/src/stores/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { fetchCurrentUser } from '../services/github.js'

const STORAGE_KEY = 'auth'

export const useAuthStore = defineStore('auth', () => {
const provider = ref(null)
const token = ref(null)
const gitlabHost = ref(null)
const user = ref(null)

const isLoggedIn = computed(() => !!token.value)

// Hydrate from localStorage on store creation
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
try {
const parsed = JSON.parse(stored)
provider.value = parsed.provider
token.value = parsed.token
gitlabHost.value = parsed.gitlabHost
user.value = parsed.user
} catch {
/* ignore */
}
}

async function loginWithToken(providerName, accessToken, host = 'gitlab.com') {
let fetchedUser = null

if (providerName === 'github') {
fetchedUser = await fetchCurrentUser(accessToken)
} else if (providerName === 'gitlab') {
const res = await fetch(`https://${host}/api/v4/user`, {
headers: { Authorization: `Bearer ${accessToken}` },
})
fetchedUser = await res.json()
}

provider.value = providerName
token.value = accessToken
gitlabHost.value = host
user.value = fetchedUser

localStorage.setItem(
STORAGE_KEY,
JSON.stringify({ provider: providerName, token: accessToken, gitlabHost: host, user: fetchedUser }),
)
}

function logout() {
provider.value = null
token.value = null
gitlabHost.value = null
user.value = null
localStorage.removeItem(STORAGE_KEY)
}

return { provider, token, gitlabHost, user, isLoggedIn, loginWithToken, logout }
})
Loading