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,
|
AuthURL: cfg.OAuth.AuthURL,
|
||||||
TokenURL: cfg.OAuth.TokenURL,
|
TokenURL: cfg.OAuth.TokenURL,
|
||||||
},
|
},
|
||||||
RedirectURL: cfg.OAuth.AuthURL,
|
RedirectURL: fmt.Sprintf("http://localhost:%d/oauth/callback", cfg.Server.Port),
|
||||||
Scopes: []string{"profile", "email"},
|
Scopes: []string{"profile", "email"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,9 +127,7 @@ func main() {
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.Use(middleware.Logger())
|
e.Use(middleware.Logger())
|
||||||
e.Use(middleware.Recover())
|
e.Use(middleware.Recover())
|
||||||
|
e.Use(middleware.CORS())
|
||||||
// Serve static files
|
|
||||||
e.GET("/*", echo.WrapHandler(http.FileServer(http.FS(staticFiles))))
|
|
||||||
|
|
||||||
// OAuth2 routes
|
// OAuth2 routes
|
||||||
e.GET("/oauth/login", handleOAuthLogin)
|
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
|
// Start server
|
||||||
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
||||||
logger.Info("Starting server", zap.Int("port", 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))
|
logger.Info("OAuth token exchanged successfully", zap.String("access_token", token.AccessToken))
|
||||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
||||||
"access_token": token.AccessToken,
|
// Return HTML that stores the token and redirects to the main app
|
||||||
"expires_in": token.Expiry,
|
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
|
// Middleware to enforce OAuth authentication
|
||||||
|
|
|
@ -1,9 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<router-view />
|
<v-main>
|
||||||
|
<router-view />
|
||||||
|
</v-main>
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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>
|
</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),
|
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
|
// Workaround for https://github.com/vitejs/vite/issues/11804
|
||||||
router.onError((err, to) => {
|
router.onError((err, to) => {
|
||||||
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
|
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