feat: implement authentication flow with OAuth login page
This commit is contained in:
parent
10b2056954
commit
77538a7dfd
5 changed files with 146 additions and 10 deletions
32
main.go
32
main.go
|
@ -89,7 +89,7 @@ func main() {
|
|||
AuthURL: cfg.OAuth.AuthURL,
|
||||
TokenURL: cfg.OAuth.TokenURL,
|
||||
},
|
||||
RedirectURL: cfg.OAuth.AuthURL,
|
||||
RedirectURL: fmt.Sprintf("http://localhost:%d/oauth/callback", cfg.Server.Port),
|
||||
Scopes: []string{"profile", "email"},
|
||||
}
|
||||
|
||||
|
@ -127,9 +127,7 @@ func main() {
|
|||
e := echo.New()
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(middleware.Recover())
|
||||
|
||||
// Serve static files
|
||||
e.GET("/*", echo.WrapHandler(http.FileServer(http.FS(staticFiles))))
|
||||
e.Use(middleware.CORS())
|
||||
|
||||
// OAuth2 routes
|
||||
e.GET("/oauth/login", handleOAuthLogin)
|
||||
|
@ -146,6 +144,9 @@ func main() {
|
|||
})
|
||||
})
|
||||
|
||||
// Serve static files - must be after API routes
|
||||
e.GET("/*", echo.WrapHandler(http.FileServer(http.FS(staticFiles))))
|
||||
|
||||
// Start server
|
||||
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
||||
logger.Info("Starting server", zap.Int("port", cfg.Server.Port))
|
||||
|
@ -190,10 +191,25 @@ func handleOAuthCallback(c echo.Context) error {
|
|||
}
|
||||
|
||||
logger.Info("OAuth token exchanged successfully", zap.String("access_token", token.AccessToken))
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"access_token": token.AccessToken,
|
||||
"expires_in": token.Expiry,
|
||||
})
|
||||
|
||||
// Return HTML that stores the token and redirects to the main app
|
||||
html := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Authentication Successful</title>
|
||||
<script>
|
||||
localStorage.setItem('auth_token', '%s');
|
||||
window.location.href = '/';
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Authentication successful. Redirecting...</p>
|
||||
</body>
|
||||
</html>
|
||||
`, token.AccessToken)
|
||||
|
||||
return c.HTML(http.StatusOK, html)
|
||||
}
|
||||
|
||||
// Middleware to enforce OAuth authentication
|
||||
|
|
|
@ -1,9 +1,21 @@
|
|||
<template>
|
||||
<v-app>
|
||||
<router-view />
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
onMounted(async () => {
|
||||
// If user has a token, fetch user info
|
||||
if (authStore.isAuthenticated) {
|
||||
await authStore.fetchUserInfo();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
39
web/src/pages/auth.vue
Normal file
39
web/src/pages/auth.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<v-container class="fill-height">
|
||||
<v-responsive class="align-center fill-height mx-auto" max-width="500">
|
||||
<v-card class="pa-6" elevation="8">
|
||||
<div class="text-center mb-6">
|
||||
<v-img
|
||||
class="mx-auto mb-6"
|
||||
height="100"
|
||||
src="@/assets/logo.png"
|
||||
width="100"
|
||||
/>
|
||||
<h1 class="text-h4 font-weight-bold mb-2">Welcome</h1>
|
||||
<p class="text-body-1">Please sign in to continue</p>
|
||||
</div>
|
||||
|
||||
<v-divider class="mb-6"></v-divider>
|
||||
|
||||
<v-btn
|
||||
block
|
||||
color="primary"
|
||||
size="large"
|
||||
@click="login"
|
||||
prepend-icon="mdi-login"
|
||||
>
|
||||
Sign in with OAuth
|
||||
</v-btn>
|
||||
</v-card>
|
||||
</v-responsive>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const router = useRouter();
|
||||
|
||||
function login() {
|
||||
// Redirect to the OAuth login endpoint
|
||||
window.location.href = '/oauth/login';
|
||||
}
|
||||
</script>
|
|
@ -14,6 +14,22 @@ const router = createRouter({
|
|||
routes: setupLayouts(routes),
|
||||
})
|
||||
|
||||
// Add authentication guard
|
||||
router.beforeEach((to, from, next) => {
|
||||
// Check if the user is authenticated
|
||||
const isAuthenticated = !!localStorage.getItem('auth_token');
|
||||
|
||||
// If route requires auth and user is not authenticated, redirect to auth page
|
||||
if (to.path !== '/auth' && !isAuthenticated) {
|
||||
next('/auth');
|
||||
} else if (to.path === '/auth' && isAuthenticated) {
|
||||
// If user is already authenticated and tries to access auth page, redirect to home
|
||||
next('/');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// Workaround for https://github.com/vitejs/vite/issues/11804
|
||||
router.onError((err, to) => {
|
||||
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
|
||||
|
|
53
web/src/stores/auth.ts
Normal file
53
web/src/stores/auth.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Auth store to manage authentication state
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
token: localStorage.getItem('auth_token') || null,
|
||||
user: null as any | null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isAuthenticated: (state) => !!state.token,
|
||||
},
|
||||
|
||||
actions: {
|
||||
setToken(token: string) {
|
||||
this.token = token;
|
||||
localStorage.setItem('auth_token', token);
|
||||
},
|
||||
|
||||
setUser(user: any) {
|
||||
this.user = user;
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.token = null;
|
||||
this.user = null;
|
||||
localStorage.removeItem('auth_token');
|
||||
},
|
||||
|
||||
async fetchUserInfo() {
|
||||
if (!this.token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/health', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.setUser(data.user);
|
||||
} else {
|
||||
// If token is invalid, logout
|
||||
this.logout();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user info:', error);
|
||||
this.logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Add table
Reference in a new issue