diff --git a/frontend/src/pages/AuthCallback.vue b/frontend/src/pages/AuthCallback.vue
new file mode 100644
index 0000000..4daae8d
--- /dev/null
+++ b/frontend/src/pages/AuthCallback.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/LoginPage.vue b/frontend/src/pages/LoginPage.vue
new file mode 100644
index 0000000..fd4ae6d
--- /dev/null
+++ b/frontend/src/pages/LoginPage.vue
@@ -0,0 +1,69 @@
+
+
+
+
+ Sign in
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/router/routes.js b/frontend/src/router/routes.js
index 8050556..9b3b6db 100644
--- a/frontend/src/router/routes.js
+++ b/frontend/src/router/routes.js
@@ -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(.*)*',
diff --git a/frontend/src/services/github.js b/frontend/src/services/github.js
new file mode 100644
index 0000000..ae66329
--- /dev/null
+++ b/frontend/src/services/github.js
@@ -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()
+}
diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js
new file mode 100644
index 0000000..f7e234e
--- /dev/null
+++ b/frontend/src/stores/auth.js
@@ -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 }
+})