Repository: 1834423612/Team-695-OfficialWebsite
Pull request #53: sync: dev-features -->master
Author: login: 1834423612
Created: 2025-03-27T01:44:45Z | Merged: 2025-03-27T02:23:44Z

feat: add login page & Casdoor Server Auth Functions

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Launched a streamlined authentication experience with dedicated sign-in, callback, and success screens.
	- Enhanced route protection to ensure only authenticated users can access restricted sections, with smooth redirection based on login status.
	- Introduced new global components for Material Design icons throughout the application.
	- Added a new dependency to support authentication functionality.
	- Provided a template for environment variables to assist with configuration.

- **Chores**
	- Updated underlying integrations to support the improved authentication flow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.env.example b/.env.example
new file mode 100644
index 00000000..bf56a767
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,14 @@
+# --------------------------------------------------------------------------------
+# Description: This file is an example of the environment variables that need to be set in the .env file.
+# You can copy this file to .env and set the values according to your own configuration.
+# --------------------------------------------------------------------------------
+
+# ENV
+ENV=development
+
+# Casdoor configuration
+VITE_CASDOOR_SERVER_URL=https://door.casbin.com
+VITE_CASDOOR_CLIENT_ID=<your-client-id>
+VITE_CASDOOR_CLIENT_SECRET=<your-client-secret>
+VITE_CASDOOR_APP_NAME=695_website
+VITE_CASDOOR_ORGANIZATION_NAME=Team695
diff --git a/package.json b/package.json
index 31ac632d..cea6e067 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
     "@tailwindcss/postcss": "^4.0.4",
     "@yeger/vue-masonry-wall": "^5.0.14",
     "axios": "^1.7.5",
+    "casdoor-js-sdk": "^0.16.0",
     "chart.js": "^4.4.8",
     "lodash": "^4.17.21",
     "lodash-es": "^4.17.21",
@@ -26,8 +27,7 @@
     "uuid": "^11.0.1",
     "vue": "^3.4.29",
     "vue-chartjs": "^5.3.1",
-    "vue-router": "4",
-    "vuedraggable": "^2.24.3"
+    "vue-router": "4"
   },
   "devDependencies": {
     "@iconify-json/mdi": "^1.1.67",
diff --git a/src/router/index.ts b/src/router/index.ts
index 8429fb03..83f2dfa9 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -1,6 +1,7 @@
 // Import vue-router dependencies 
 import { createRouter, createWebHistory } from 'vue-router';
 import Home from '../views/Home.vue';
+import { casdoorService } from '@/services/auth';
 // import About from '../views/About.vue';
 // import MentorDetail from '../views/MentorDetail.vue';
 // import Members from '../views/Members.vue';
@@ -38,12 +39,15 @@ const routes = [
   { path: '/gallery', component: () => import('@/views/Gallery.vue') },
   { path: '/resources', component: () => import('@/views/Resources.vue') },
   { path: '/contact', component: () => import('@/views/Contact.vue') },
-  { path: '/pit-scouting', component: () => import('@/views/Pit-scouting.vue') },
-  { path: '/pit-scouting/dashboard', component: () => import('@/views/Pit-scoutingDashboard.vue') },
+  { path: '/pit-scouting', component: () => import('@/views/Pit-scouting.vue'), meta: { requiresAuth: true } },
+  { path: '/pit-scouting/dashboard', component: () => import('@/views/Pit-scoutingDashboard.vue'), meta: { requiresAuth: true } },
   { path: '/2025strategy', component: () => import('@/views/strategy/2025chart.vue') },
   { path: '/2025strategy/html', component: () => import('@/views/strategy/2025chart-html.vue') },
   { path: '/legal/PrivacyPolicy', component: () => import('@/views/legal/PrivacyPolicy.vue') },
   { path: '/legal/TermsOfService', component: () => import('@/views/legal/TermsOfService.vue') },
+  { path: '/login', name: 'login', component: () => import('@/views/auth/CasdoorLogin.vue') },
+  { path: '/callback', component: () => import('@/views/auth/CasdoorCallback.vue') },
+  { path: '/login-success', name: 'login-success', component: () => import('@/views/auth/SucceedView.vue'), meta: { requiresAuth: true } },
 ];
 
 // Create a new router instance
@@ -52,4 +56,23 @@ const router = createRouter({
   routes,
 });
 
+// 全局路由守卫
+router.beforeEach((to, _from, next) => {
+  const isLoggedIn = casdoorService.isLoggedIn(); // 使用 casdoorService 检查登录状态
+
+  if (to.name === 'login' && isLoggedIn) {
+    // 如果用户已登录并试图访问登录页面，重定向到成功页面
+    next({ name: 'login-success' });
+  } else if (to.name === 'login-success' && !isLoggedIn) {
+    // 如果用户未登录并试图访问成功页面，重定向到登录页面
+    next({ name: 'login' });
+  } else if (to.meta.requiresAuth && !isLoggedIn) {
+    // 如果需要登录并且用户未登录，重定向到登录页面
+    next({ name: 'login' });
+  } else {
+    // 否则允许访问
+    next();
+  }
+});
+
 export default router;
diff --git a/src/services/auth.ts b/src/services/auth.ts
new file mode 100644
index 00000000..ff169092
--- /dev/null
+++ b/src/services/auth.ts
@@ -0,0 +1,329 @@
+import SDK from 'casdoor-js-sdk';
+
+// Casdoor configuration
+export const casdoorConfig = {
+    serverUrl: import.meta.env.VITE_CASDOOR_SERVER_URL,
+    clientId: import.meta.env.VITE_CASDOOR_CLIENT_ID,
+    clientSecret: import.meta.env.VITE_CASDOOR_CLIENT_SECRET,
+    appName: import.meta.env.VITE_CASDOOR_APP_NAME,
+    organizationName: import.meta.env.VITE_CASDOOR_ORGANIZATION_NAME,
+    redirectPath: '/callback',
+    signinPath: '/api/signin',
+    storage: localStorage, // Use localStorage to persist state across tabs
+};
+
+export interface UserInfo {
+    id: string;
+    name: string;
+    displayName?: string;
+    email: string;
+    phone?: string;
+    avatar?: string;
+    owner: string;
+    [key: string]: any;
+}
+
+class CasdoorService {
+    // Remove the sdk property to avoid the TypeScript warning
+    
+    constructor() {
+        // Initialize SDK but don't store it since we're not using it directly
+        new SDK(casdoorConfig);
+    }
+
+    /**
+     * Get the sign-in URL from Casdoor
+     * This will also generate and store a state parameter
+     */
+    getSigninUrl(): string {
+        // Generate a random state value and store it
+        const state = Math.random().toString(36).substring(2);
+        localStorage.setItem('casdoorState', state);
+
+        // Build the authorization URL manually to ensure state is included
+        const redirectUri = `${window.location.origin}${casdoorConfig.redirectPath}`;
+        const authUrl = `${casdoorConfig.serverUrl}/login/oauth/authorize?` +
+            `client_id=${casdoorConfig.clientId}` +
+            `&response_type=code` +
+            `&redirect_uri=${encodeURIComponent(redirectUri)}` +
+            `&scope=read` +
+            `&state=${state}`;
+
+        return authUrl;
+    }
+
+    /**
+     * Handle the OAuth callback
+     * @param code The authorization code from Casdoor
+     * @param state The state parameter for CSRF protection
+     * @returns Promise with the access token response
+     */
+    async handleCallback(code: string, state: string): Promise<any> {
+        try {
+            // Verify state to prevent CSRF attacks
+            const savedState = localStorage.getItem('casdoorState');
+            if (!state || state !== savedState) {
+                throw new Error('Invalid state parameter');
+            }
+
+            // Clear the stored state
+            localStorage.removeItem('casdoorState');
+
+            // Exchange the code for an access token
+            const redirectUri = `${window.location.origin}${casdoorConfig.redirectPath}`;
+
+            // Use the SDK's token endpoint directly
+            const tokenUrl = `${casdoorConfig.serverUrl}/api/login/oauth/access_token`;
+            const response = await fetch(tokenUrl, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify({
+                    grant_type: 'authorization_code',
+                    client_id: casdoorConfig.clientId,
+                    client_secret: casdoorConfig.clientSecret,
+                    code: code,
+                    redirect_uri: redirectUri,
+                }),
+            });
+
+            const tokenResponse = await response.json();
+
+            if (tokenResponse.error) {
+                throw tokenResponse;
+            }
+
+            // Store the token in localStorage
+            if (tokenResponse.access_token) {
+                localStorage.setItem('casdoorToken', tokenResponse.access_token);
+                if (tokenResponse.refresh_token) {
+                    localStorage.setItem('casdoorRefreshToken', tokenResponse.refresh_token);
+                }
+            }
+
+            return tokenResponse;
+        } catch (error) {
+            console.error('Authentication error:', error);
+            throw error;
+        }
+    }
+
+    /**
+     * Get user information using the access token
+     * @param accessToken The access token
+     * @returns Promise with user information
+     */
+    async getUserInfo(accessToken?: string): Promise<UserInfo> {
+        const token = accessToken || localStorage.getItem('casdoorToken');
+        if (!token) {
+            throw new Error('No access token available');
+        }
+
+        try {
+            // First, try to parse the JWT token to get basic user info
+            const tokenParts = token.split('.');
+            if (tokenParts.length === 3) {
+                try {
+                    const payload = JSON.parse(atob(tokenParts[1]));
+                    console.log("Token payload:", payload);
+                    if (payload.name && payload.owner) {
+                        // If we have basic info from the token, fetch complete user info
+                        const response = await fetch(`${casdoorConfig.serverUrl}/api/get-user?id=${payload.owner}/${payload.name}`, {
+                            headers: {
+                                'Authorization': `Bearer ${token}`
+                            }
+                        });
+
+                        if (response.ok) {
+                            const responseData = await response.json();
+                            console.log("Full response from API:", responseData);
+                            
+                            // Extract user data from the 'data' property
+                            if (responseData.status === "ok" && responseData.data) {
+                                console.log("User data extracted from 'data' property:", responseData.data);
+                                return responseData.data;
+                            }
+                            
+                            return responseData;
+                        }
+                    }
+                } catch (e) {
+                    console.error('Error parsing token:', e);
+                }
+            }
+
+            // Fallback to userinfo endpoint
+            const response = await fetch(`${casdoorConfig.serverUrl}/api/userinfo`, {
+                headers: {
+                    'Authorization': `Bearer ${token}`
+                }
+            });
+
+            if (!response.ok) {
+                throw new Error(`Failed to fetch user info: ${response.status} ${response.statusText}`);
+            }
+
+            const responseData = await response.json();
+            console.log("Full response from userinfo endpoint:", responseData);
+            
+            // Extract user data from the 'data' property if it exists
+            if (responseData.status === "ok" && responseData.data) {
+                console.log("User data extracted from 'data' property:", responseData.data);
+                return responseData.data;
+            }
+
+            // If we still don't have complete user info, try to get it by name
+            if (responseData.name && responseData.owner && (!responseData.email || !responseData.id)) {
+                try {
+                    const detailResponse = await fetch(`${casdoorConfig.serverUrl}/api/get-user?id=${responseData.owner}/${responseData.name}`, {
+                        headers: {
+                            'Authorization': `Bearer ${token}`
+                        }
+                    });
+
+                    if (detailResponse.ok) {
+                        const detailData = await detailResponse.json();
+                        console.log("Detail response:", detailData);
+                        
+                        // Extract user data from the 'data' property if it exists
+                        if (detailData.status === "ok" && detailData.data) {
+                            console.log("User data extracted from 'data' property:", detailData.data);
+                            return detailData.data;
+                        }
+                        
+                        return detailData;
+                    }
+                } catch (e) {
+                    console.error('Error fetching detailed user info:', e);
+                }
+            }
+
+            return responseData;
+        } catch (error) {
+            console.error('Error getting user info:', error);
+            throw error;
+        }
+    }
+
+    /**
+     * Get the Casdoor server URL
+     */
+    getServerUrl(): string {
+        return casdoorConfig.serverUrl;
+    }
+
+    /**
+     * Check if the user is logged in
+     */
+    isLoggedIn(): boolean {
+        return !!localStorage.getItem('casdoorToken');
+    }
+
+    /**
+     * Log out the user
+     */
+    logout(): void {
+        localStorage.removeItem('casdoorToken');
+        localStorage.removeItem('casdoorRefreshToken');
+        localStorage.removeItem('casdoorState');
+    }
+
+    /**
+     * Refresh the access token
+     */
+    async refreshToken(): Promise<any> {
+        const refreshToken = localStorage.getItem('casdoorRefreshToken');
+        if (!refreshToken) {
+            throw new Error('No refresh token available');
+        }
+
+        try {
+            const response = await fetch(`${casdoorConfig.serverUrl}/api/login/oauth/refresh_token`, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify({
+                    grant_type: 'refresh_token',
+                    refresh_token: refreshToken,
+                    client_id: casdoorConfig.clientId,
+                    client_secret: casdoorConfig.clientSecret,
+                }),
+            });
+
+            const tokenResponse = await response.json();
+
+            if (tokenResponse.access_token) {
+                localStorage.setItem('casdoorToken', tokenResponse.access_token);
+                if (tokenResponse.refresh_token) {
+                    localStorage.setItem('casdoorRefreshToken', tokenResponse.refresh_token);
+                }
+            }
+
+            return tokenResponse;
+        } catch (error) {
+            console.error('Error refreshing token:', error);
+            throw error;
+        }
+    }
+
+    /**
+     * Parse the JWT token to get basic user info
+     * @param token The JWT token
+     * @returns The decoded token payload
+     */
+    parseToken(token: string): any {
+        try {
+            const base64Url = token.split('.')[1];
+            const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+            const jsonPayload = decodeURIComponent(
+                atob(base64)
+                    .split('')
+                    .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
+                    .join('')
+            );
+            return JSON.parse(jsonPayload);
+        } catch (error) {
+            console.error('Error parsing token:', error);
+            return null;
+        }
+    }
+
+    /**
+     * Revoke the access token
+     */
+    async revokeToken(): Promise<void> {
+        const token = localStorage.getItem('casdoorToken');
+        if (!token) {
+            throw new Error('No access token available');
+        }
+
+        try {
+            const response = await fetch(`${casdoorConfig.serverUrl}/api/logout`, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify({
+                    token: token,
+                    client_id: casdoorConfig.clientId,
+                    client_secret: casdoorConfig.clientSecret,
+                }),
+            });
+
+            if (!response.ok) {
+                throw new Error(`Failed to revoke token: ${response.status} ${response.statusText}`);
+            }
+
+            // Clear local storage
+            this.logout();
+        } catch (error) {
+            console.error('Error revoking token:', error);
+            throw error;
+        }
+    }
+}
+
+// Export as a singleton
+export const casdoorService = new CasdoorService();
\ No newline at end of file
diff --git a/src/views/auth/CasdoorCallback.vue b/src/views/auth/CasdoorCallback.vue
new file mode 100644
index 00000000..16870ff0
--- /dev/null
+++ b/src/views/auth/CasdoorCallback.vue
@@ -0,0 +1,78 @@
+<template>
+    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+        <div class="max-w-md w-full text-center">
+            <div v-if="loading" class="space-y-4">
+                <Icon icon="mdi:loading" class="h-12 w-12 mx-auto animate-spin text-indigo-600" />
+                <p class="text-lg font-medium text-gray-700">Processing login...</p>
+            </div>
+            <div v-if="error" class="space-y-4 text-red-500">
+                <Icon icon="mdi:alert-circle" class="h-12 w-12 mx-auto text-red-500" />
+                <p class="text-lg font-medium">{{ error }}</p>
+                <button @click="goToLogin"
+                    class="mt-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
+                    Return to Login Page
+                </button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, onMounted } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { casdoorService } from '@/services/auth';
+
+export default defineComponent({
+    name: 'CallbackView',
+    setup() {
+        const loading = ref(true);
+        const error = ref('');
+        const route = useRoute();
+        const router = useRouter();
+
+        const goToLogin = () => {
+            router.push('/login');
+        };
+
+        onMounted(async () => {
+            try {
+                const code = route.query.code as string;
+                const state = route.query.state as string;
+
+                if (!code || !state) {
+                    throw new Error('Invalid authorization code or state');
+                }
+
+                // Process the callback
+                await casdoorService.handleCallback(code, state);
+
+                // Notify the parent window about the success
+                if (window.opener) {
+                    window.opener.postMessage({ success: true }, casdoorService.getServerUrl());
+                }
+
+                // Close the current window
+                window.close();
+            } catch (err: any) {
+                console.error('Callback error:', err);
+                error.value = err.message;
+                loading.value = false;
+
+                // Notify the parent window about the error
+                if (window.opener) {
+                    window.opener.postMessage({ success: false, error: err.message }, casdoorService.getServerUrl());
+                }
+
+                // Close the current window
+                window.close();
+            }
+        });
+
+        return {
+            loading,
+            error,
+            goToLogin
+        };
+    }
+});
+</script>
\ No newline at end of file
diff --git a/src/views/auth/CasdoorLogin.vue b/src/views/auth/CasdoorLogin.vue
new file mode 100644
index 00000000..64a66284
--- /dev/null
+++ b/src/views/auth/CasdoorLogin.vue
@@ -0,0 +1,107 @@
+<template>
+    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+        <div class="max-w-md w-full space-y-8">
+            <div class="text-center">
+                <h2 class="mt-6 text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
+                <p class="mt-2 text-sm text-gray-600">
+                    Authenticate with Casdoor
+                </p>
+            </div>
+
+            <div class="mt-8">
+                <button v-if="!isLoggedIn" @click="handleLogin"
+                    class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
+                    <span class="absolute left-0 inset-y-0 flex items-center pl-3">
+                        <Icon icon="mdi:login" class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" />
+                    </span>
+                    Sign in with Casdoor
+                </button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { casdoorService } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+    name: 'LoginView',
+    components: {
+        Icon
+    },
+    setup() {
+        const router = useRouter();
+        const isLoggedIn = ref(false);
+
+        const handleLogin = () => {
+            // Open Casdoor login page in a new window
+            const loginWindow = window.open(
+                casdoorService.getSigninUrl(),
+                '_blank',
+                'width=500,height=600'
+            );
+
+            if (!loginWindow || loginWindow.closed || typeof loginWindow.closed === 'undefined') {
+                console.error('Popup blocked or window failed to open');
+                // Show user-friendly error message about popup blockers
+                alert('Please allow popups for this site to continue with login');
+                return;
+            }
+
+            // Listen for messages from the login window
+            const messageListener = (event: MessageEvent) => {
+                if (event.origin !== casdoorService.getServerUrl()) {
+                    return; // Ignore messages from unknown origins
+                }
+
+                const { success, error } = event.data;
+
+                if (success) {
+                    // Login successful, navigate to success page
+                    router.push({ name: 'login-success' });
+                } else if (error) {
+                    // Handle login error
+                    console.error('Login failed:', error);
+                    isLoggedIn.value = false;
+                    // Provide user feedback about the error
+                    alert(`Login failed: ${error || 'Unknown error'}`);
+                }
+
+                // Clean up listener and close the login window
+                window.removeEventListener('message', messageListener);
+                loginWindow?.close();
+            };
+
+            window.addEventListener('message', messageListener);
+        };
+
+        // Check if user is already logged in  
+        const checkLoginStatus = () => {  
+            if (localStorage.getItem('casdoorToken')) {  
+                isLoggedIn.value = true;  
+                router.push({ name: 'login-success' });  
+            } else {  
+                isLoggedIn.value = false;  
+            }  
+        };
+
+        onMounted(() => {
+            // Check if user is already logged in
+            checkLoginStatus();
+
+            // Listen for changes in localStorage to detect login status updates
+            window.addEventListener('storage', () => {
+                checkLoginStatus();
+            });
+        });
+
+        return {
+            handleLogin,
+            isLoggedIn
+        };
+    }
+});
+</script>
\ No newline at end of file
diff --git a/src/views/auth/SucceedView.vue b/src/views/auth/SucceedView.vue
new file mode 100644
index 00000000..6c124941
--- /dev/null
+++ b/src/views/auth/SucceedView.vue
@@ -0,0 +1,372 @@
+<template>
+  <div class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+    <div class="max-w-4xl mx-auto">
+      <div class="text-center mb-10">
+    <h2 class="text-3xl font-extrabold text-gray-900">Login Successful</h2>
+    <p class="mt-2 text-lg text-gray-600">
+      You have successfully authenticated with Casdoor.
+    </p>
+  </div>
+
+  <div v-if="loading" class="text-center py-10">
+    <Icon icon="mdi:loading" class="h-10 w-10 mx-auto animate-spin text-indigo-600" />
+    <p class="mt-4 text-gray-600">Loading user information...</p>
+  </div>
+
+  <div v-else-if="error" class="text-center py-10 text-red-500">
+    <Icon icon="mdi:alert-circle" class="h-10 w-10 mx-auto text-red-500" />
+    <p class="mt-4">{{ error }}</p>
+    <button 
+      @click="retryFetchUserInfo" 
+      class="mt-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
+    >
+      Retry
+    </button>
+    <button 
+      @click="logout" 
+      class="mt-4 ml-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
+    >
+      Logout
+    </button>
+  </div>
+
+  <div v-else>
+    <!-- User Profile Card -->
+    <div class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+      <div class="px-4 py-5 sm:px-6 flex justify-between items-center">
+        <div class="flex items-center">
+          <img 
+            v-if="userInfo.avatar" 
+            :src="userInfo.avatar" 
+            alt="User Avatar" 
+            class="h-16 w-16 rounded-full mr-4 object-cover border-2 border-indigo-200"
+          />
+          <div>
+            <h3 class="text-lg leading-6 font-medium text-gray-900">{{ userInfo.displayName || userInfo.name }}</h3>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">{{ userInfo.email }}</p>
+          </div>
+        </div>
+        <!-- <div class="relative">
+          <button 
+            @click="toggleDropdown" 
+            class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
+          >
+            <Icon icon="mdi:logout" class="mr-2 h-4 w-4" />
+            Logout
+            <Icon icon="mdi:chevron-down" class="ml-2 h-4 w-4" />
+          </button>
+          <div v-show="dropdownVisible" class="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg">
+            <button 
+              @click="logout" 
+              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
+            >
+              Logout Locally
+            </button>
+            <button 
+              @click="logoutCasdoor" 
+              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
+            >
+              Logout from Casdoor
+            </button>
+          </div>
+        </div> -->
+        <div class="relative">
+          <button 
+            @click="logout" 
+            class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
+          >
+            <Icon icon="mdi:logout" class="mr-2 h-4 w-4" />
+            Logout
+          </button>
+        </div>
+      </div>
+          
+          <div class="border-t border-gray-200">
+            <dl>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Username</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.name || 'Not set' }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Display Name</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.displayName || 'Not set' }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Email</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.email || 'Not set' }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">ID</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.id || 'Not set' }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Created Time</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userInfo.createdTime) }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Updated Time</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userInfo.updatedTime) }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Organization</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.owner || 'Not set' }}</dd>
+              </div>
+            </dl>
+          </div>
+        </div>
+        
+        <!-- Google Authentication Card -->
+        <div v-if="userInfo.google" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+          <div class="px-4 py-5 sm:px-6">
+            <div class="flex items-center">
+              <Icon icon="mdi:google" class="h-6 w-6 text-red-500 mr-2" />
+              <h3 class="text-lg leading-6 font-medium text-gray-900">Google Account Linked</h3>
+            </div>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">Your account is linked to a Google account.</p>
+          </div>
+          
+          <div class="border-t border-gray-200">
+            <dl>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Google ID</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.google }}</dd>
+              </div>
+              
+              <div v-if="userInfo.properties && Object.keys(userInfo.properties).some(key => key.startsWith('oauth_Google'))" class="bg-white px-4 py-5 sm:px-6">
+                <details class="w-full">
+                  <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none">
+                    View Google Account Details
+                  </summary>
+                  <div class="mt-4 border border-gray-200 rounded-md overflow-hidden">
+                    <dl>
+                      <div v-for="(value, key, index) in googleProperties" :key="key" 
+                           :class="{'bg-gray-50': index % 2 === 0, 'bg-white': index % 2 === 1}"
+                           class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                        <dt class="text-sm font-medium text-gray-500">{{ formatPropertyKey(key) }}</dt>
+                        <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
+                          <div v-if="key.includes('avatarUrl')">
+                            <img :src="value" alt="Google Avatar" class="h-10 w-10 rounded-full" />
+                          </div>
+                          <div v-else>{{ value }}</div>
+                        </dd>
+                      </div>
+                    </dl>
+                  </div>
+                </details>
+              </div>
+            </dl>
+          </div>
+        </div>
+        
+        <!-- Properties Card -->
+        <div v-if="userInfo.properties && Object.keys(userInfo.properties).length > 0" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+          <div class="px-4 py-5 sm:px-6">
+            <h3 class="text-lg leading-6 font-medium text-gray-900">Other Properties</h3>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">Additional user properties.</p>
+          </div>
+          
+          <div class="border-t border-gray-200">
+            <details class="w-full">
+              <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none px-4 py-3">
+                View All Properties
+              </summary>
+              <div class="border-t border-gray-200">
+                <dl>
+                  <div v-for="(value, key, index) in otherProperties" :key="key" 
+                       :class="{'bg-gray-50': index % 2 === 0, 'bg-white': index % 2 === 1}"
+                       class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                    <dt class="text-sm font-medium text-gray-500">{{ key }}</dt>
+                    <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ value }}</dd>
+                  </div>
+                </dl>
+              </div>
+            </details>
+          </div>
+        </div>
+
+        <div v-if="!loading && !error && isDev" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+          <div class="px-4 py-5 sm:px-6">
+            <h3 class="text-lg leading-6 font-medium text-gray-900">Debug Information</h3>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">Raw user data</p>
+          </div>
+        
+          <div class="border-t border-gray-200">
+            <!-- open -->
+            <details class="w-full">
+              <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none px-4 py-3">
+                View Raw Data
+              </summary>
+              <div class="border-t border-gray-200 p-4">
+                <pre class="text-xs overflow-auto bg-gray-100 p-2 rounded">{{ JSON.stringify(userInfo, null, 2) }}</pre>
+              </div>
+            </details>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, onMounted, computed } from 'vue';
+import { useRouter } from 'vue-router';
+import { casdoorService, UserInfo } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+  name: 'SucceedView',
+  components: {
+    Icon
+  },
+  setup() {
+    const router = useRouter();
+    const loading = ref(true);
+    const error = ref('');
+    const userInfo = ref<UserInfo>({
+      id: '',
+      name: '',
+      email: '',
+      owner: ''
+    });
+    const dropdownVisible = ref(false);
+
+    // Check if in development mode
+    const isDev = import.meta.env.DEV;
+
+    // Computed property to get Google-related properties
+    const googleProperties = computed(() => {
+      if (!userInfo.value.properties) return {};
+      
+      return Object.entries(userInfo.value.properties)
+        .filter(([key]) => key.startsWith('oauth_Google'))
+        .reduce((acc, [key, value]) => {
+          acc[key] = value;
+          return acc;
+        }, {} as Record<string, any>);
+    });
+    
+    // Computed property to get non-Google properties
+    const otherProperties = computed(() => {
+      if (!userInfo.value.properties) return {};
+      
+      return Object.entries(userInfo.value.properties)
+        .filter(([key]) => !key.startsWith('oauth_Google'))
+        .reduce((acc, [key, value]) => {
+          acc[key] = value;
+          return acc;
+        }, {} as Record<string, any>);
+    });
+
+    // Format property keys for display
+    const formatPropertyKey = (key: string) => {
+      if (key.startsWith('oauth_Google_')) {
+        return key.replace('oauth_Google_', '');
+      }
+      return key;
+    };
+    
+    // Format date for display
+    const formatDate = (dateString?: string) => {
+      if (!dateString) return 'Not set';
+      
+      try {
+        const date = new Date(dateString);
+        return new Intl.DateTimeFormat('en-US', {
+          year: 'numeric',
+          month: 'long',
+          day: 'numeric',
+          hour: '2-digit',
+          minute: '2-digit',
+          second: '2-digit',
+          hour12: false
+        }).format(date);
+      } catch (e) {
+        return dateString;
+      }
+    };
+
+    const fetchUserInfo = async () => {
+      loading.value = true;
+      error.value = '';
+      
+      try {
+        // Get user info
+        const userData = await casdoorService.getUserInfo();
+        // console.log('Raw user data from service:', userData);
+        
+        // Make sure we're getting all properties
+        userInfo.value = { ...userData };
+        
+        // console.log('User info after assignment:', userInfo.value);
+        loading.value = false;
+      } catch (err: any) {
+        console.error('Error loading user info:', err);
+        error.value = err.message || 'Failed to load user information';
+        loading.value = false;
+      }
+    };
+
+    const retryFetchUserInfo = () => {
+      fetchUserInfo();
+    };
+
+    const logout = () => {
+      casdoorService.logout();
+      router.push({ name: 'login' }).catch(err => {
+        console.error('Failed to navigate to login:', err);
+      });
+    };
+
+    const logoutCasdoor = async () => {
+      const originalError = error.value;
+      try {
+        await casdoorService.revokeToken();
+        router.push({ name: 'login' }).catch(err => {
+          if (import.meta.env.DEV) {
+            console.error('Failed to navigate to login:', err);
+          }
+        });
+      } catch (err) {
+        if (import.meta.env.DEV) {
+          console.error('Failed to revoke token:', err);
+        }
+        error.value = 'Failed to revoke login session. Please try again.';
+        // Reset error after 5 seconds
+        setTimeout(() => {
+          error.value = originalError;
+        }, 5000);
+      }
+    };
+
+    const toggleDropdown = () => {
+      dropdownVisible.value = !dropdownVisible.value;
+    };
+
+    onMounted(async () => {
+      // Check if user is logged in
+      if (!casdoorService.isLoggedIn()) {
+        router.push({ name: 'login' });
+        return;
+      }
+      
+      await fetchUserInfo();
+    });
+
+    return {
+      userInfo,
+      loading,
+      error,
+      isDev,
+      googleProperties,
+      otherProperties,
+      formatPropertyKey,
+      formatDate,
+      retryFetchUserInfo,
+      logout,
+      logoutCasdoor,
+      toggleDropdown,
+      dropdownVisible
+    };
+  }
+});
+</script>
\ No newline at end of file


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR implements a login page and integrates Casdoor server authentication into the application. The key changes include addition of a dedicated authentication service, new Vue views for login, callback handling and a success page, and updates to the router to enforce authentication routes.

### Reviewed Changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                            | Description                                                             |
| ------------------------------- | ----------------------------------------------------------------------- |
| src/services/auth.ts            | Adds Casdoor authentication functions and configuration.              |
| src/router/index.ts             | Updates router to include authentication checks and new login routes.    |
| src/views/auth/CasdoorCallback.vue | Introduces a callback view to process OAuth responses from Casdoor.      |
| src/views/auth/CasdoorLogin.vue  | Creates a login view to initiate the authentication flow with Casdoor.   |
| src/views/auth/SucceedView.vue   | Adds a success view to display user info after a successful login.        |
</details>


<details>
<summary>Files not reviewed (1)</summary>

* **package.json**: Language not supported
</details>



state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T01:45:27Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 7**

<details>
<summary>🧹 Nitpick comments (14)</summary><blockquote>

<details>
<summary>package.json (1)</summary><blockquote>

`30-30`: **Vue Router version should use semantic versioning.**

The Vue Router dependency is specified as just "4" without a patch or minor version. This could lead to unexpected breaking changes when new major versions are released.

```diff
-    "vue-router": "4"
+    "vue-router": "^4.0.0"
```

</blockquote></details>
<details>
<summary>src/views/auth/CasdoorLogin.vue (3)</summary><blockquote>

`48-51`: **Validate origin more securely.**

The current origin validation is a direct string comparison which may lead to issues if there are subtle variations in URL format. Consider using URL objects for more robust origin comparison.

```diff
            const messageListener = (event: MessageEvent) => {
-                if (event.origin !== casdoorService.getServerUrl()) {
+                const serverUrl = new URL(casdoorService.getServerUrl());
+                const eventOrigin = new URL(event.origin);
+                if (serverUrl.origin !== eventOrigin.origin) {
                    return; // Ignore messages from unknown origins
                }
```

---

`71-74`: **Add timeout handling for login check.**

The login status check has no timeout handling, which might lead to users being stuck if there's an issue with localStorage or the redirect doesn't work as expected.

```diff
        const checkLoginStatus = () => {
            if (localStorage.getItem('isLoggedIn') === 'true') {
-                router.push({ name: 'login-success' });
+                router.push({ name: 'login-success' }).catch(err => {
+                    console.error('Failed to navigate to login success page:', err);
+                });
            }
        };
```

---

`86-86`: **Translate comment to English for consistency.**

The comment is in Chinese while the rest of the codebase appears to be in English. For consistency and to improve code maintainability in a global environment, all comments should be in English.

```diff
-            // 强制重定向已登录用户
+            // Force redirect for already logged in users
```

</blockquote></details>
<details>
<summary>src/views/auth/SucceedView.vue (4)</summary><blockquote>

`49-72`: **Remove or implement commented code.**

There's a large block of commented code related to a dropdown menu for logout options. Either implement this feature if it's planned or remove the commented code entirely to improve readability and maintenance.

```diff
-        <!-- <div class="relative">
-          <button 
-            @click="toggleDropdown" 
-            class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
-          >
-            <Icon icon="mdi:logout" class="mr-2 h-4 w-4" />
-            Logout
-            <Icon icon="mdi:chevron-down" class="ml-2 h-4 w-4" />
-          </button>
-          <div v-show="dropdownVisible" class="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg">
-            <button 
-              @click="logout" 
-              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
-            >
-              Logout Locally
-            </button>
-            <button 
-              @click="logoutCasdoor" 
-              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
-            >
-              Logout from Casdoor
-            </button>
-          </div>
-        </div> -->
```

---

`317-326`: **Unused `logoutCasdoor` function.**

The `logoutCasdoor` function is defined but never used in the active code. It's only referenced in the commented-out code. Consider removing it or implementing the dropdown feature that would use it.

```diff
-    const logoutCasdoor = async () => {
-      try {
-        await casdoorService.revokeToken();
-        router.push({ name: 'login' }).catch(err => {
-          console.error('Failed to navigate to login:', err);
-        });
-      } catch (err) {
-        console.error('Failed to revoke token:', err);
-      }
-    };
```

---

`328-330`: **Unused `toggleDropdown` function.**

The `toggleDropdown` function is defined but never used in the active code. It's only used in the commented-out code. Consider removing it or implementing the dropdown feature that would use it.

```diff
-    const toggleDropdown = () => {
-      dropdownVisible.value = !dropdownVisible.value;
-    };
```

---

`352-354`: **Remove unused exports.**

The `logoutCasdoor`, `toggleDropdown`, and `dropdownVisible` exports are not used in the active code and should be removed.

```diff
      retryFetchUserInfo,
      logout,
-      logoutCasdoor,
-      toggleDropdown,
-      dropdownVisible
+      // Remove unused exports
```

</blockquote></details>
<details>
<summary>src/views/auth/CasdoorCallback.vue (2)</summary><blockquote>

`20-37`: **Good Composition API setup with minor suggestion.**

The reactive references (`loading` and `error`) and the `goToLogin` function are well-defined. However, consider updating `loading.value` to `false` either after successful callback or in the catch block if the window doesn't close, preventing perpetually showing the loading state in edge cases.

---

`38-59`: **Callback handling logic is correct but consider finalizing the loading state.**

The code properly retrieves `code` and `state` from the route and invokes `casdoorService.handleCallback`. Storing `isLoggedIn` in localStorage ensures the wider app recognizes login status. As an enhancement, you may explicitly set `loading.value = false` if you decide not to close the window in certain scenarios. This is purely an edge-case improvement.

</blockquote></details>
<details>
<summary>src/services/auth.ts (4)</summary><blockquote>

`27-33`: **Potential memory usage with new SDK instance.**

You instantiate `new SDK(casdoorConfig)` without retaining a reference. If future methods rely on it, consider storing it in a class property or removing it entirely. Otherwise, this line incurs slight overhead without usage.

---

`56-111`: **Callback flow is comprehensive with correct CSRF check.**

Verifying the saved state is essential to prevent CSRF attacks. The token exchange and error handling are logically sound. Consider stamping the token retrieval time to manage expiration more gracefully (optional improvement).

---

`113-208`: **User info retrieval is thoroughly implemented.**

Parsing the JWT payload, then conditionally fetching full user details from multiple endpoints ensures robust coverage. The fallback logic is well structured. Consider reducing repeated fetch calls by caching user info once it's fetched.

---

`233-270`: **Refresh token logic is well handled.**

Posting to Casdoor’s refresh endpoint and storing the new tokens is consistent. Consider gracefully handling refresh failures (e.g., logging out the user if the refresh token is invalid).

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b7a3fc1ae693b7c694449c8594f4087b9755451e and 0a162c516f975dbd2422b691400cf8d783342181.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `components.d.ts` (1 hunks)
* `package.json` (2 hunks)
* `src/router/index.ts` (3 hunks)
* `src/services/auth.ts` (1 hunks)
* `src/views/auth/CasdoorCallback.vue` (1 hunks)
* `src/views/auth/CasdoorLogin.vue` (1 hunks)
* `src/views/auth/SucceedView.vue` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>src/router/index.ts (1)</summary><blockquote>

<details>
<summary>src/services/auth.ts (2)</summary>

* `isLoggedIn` (220-222)
* `casdoorService` (330-330)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>package.json (1)</summary>

`21-21`: **Added Casdoor SDK for authentication.**

The addition of the `casdoor-js-sdk` dependency is aligned with the PR objective to implement authentication functions for the Casdoor server.

</details>
<details>
<summary>components.d.ts (1)</summary>

`28-32`: **Material Design icons are properly integrated.**

The addition of Material Design icons as global components enables consistent icon usage throughout the application, particularly in the newly added authentication components.

</details>
<details>
<summary>src/views/auth/SucceedView.vue (1)</summary>

`300-300`: **Keep error logging for troubleshooting.**

This console.error is appropriate to maintain as it helps with troubleshooting authentication issues.

</details>
<details>
<summary>src/views/auth/CasdoorCallback.vue (2)</summary>

`1-18`: **Template structure looks solid.**

The template is clear, providing two distinct sections for a loading indicator and an error message. The usage of `Icon` clarifies the status to the user. No immediate issues.

---

`72-80`: **Return object is straightforward.**

Exposing `loading`, `error`, and `goToLogin` from the setup function is good. Just ensure they are kept synchronized with any post-callback actions so that the template reflects the actual state.

</details>
<details>
<summary>src/services/auth.ts (8)</summary>

`16-25`: **Interface `UserInfo` is well defined.**

This interface neatly captures Casdoor user fields. Using an index signature for additional fields is a good extension approach.

---

`35-54`: **Sign-in URL generation is logical and robust.**

Generating a random `state` and building the URL with `redirectUri` ensures safer sign-in. Good usage of `encodeURIComponent` for the redirect URI.

---

`210-215`: **Server URL getter is straightforward.**

No issues here. This is a clear and concise function for external usage.

---

`217-222`: **Login status check is simple and direct.**

Returning a boolean based on the presence of `casdoorToken` in local storage is effective for quick checks.

---

`224-231`: **Logout approach is correct.**

Clearing all token-related items in local storage is a good practice to ensure the user is fully signed out.

---

`272-292`: **JWT parsing method is fine.**

Decoding base64 payload with error handling is properly done. Note that `atob` may not work in some older browsers without polyfills, but typically acceptable in modern environments.

---

`294-326`: **Token revocation method is correct and logs out.**

Removing the token from local storage after a successful server-side revoke call ensures a fully terminated session.

---

`329-330`: **Singleton export is convenient.**

Exposing a single instance of the `CasdoorService` class simplifies usage across the application.

</details>
<details>
<summary>src/router/index.ts (4)</summary>

`4-4`: **Importing `casdoorService` is appropriate.**

Bringing the service into the router is necessary for authentication checks in the global guard.

---

`42-43`: **Secured routes for pit-scouting.**

Adding `{ requiresAuth: true }` enforces login status, improving data access control.

---

`48-50`: **New authentication routes.**

The `/login`, `/callback`, and `/login-success` routes form a coherent authentication flow. Good separation of concerns with dedicated components.

---

`59-76`: **Global navigation guard is well structured.**

- Redirecting already-logged-in users away from the login page prevents confusion.  
- Routing unauthenticated users to login if they attempt a protected route is correct.  
- Handling success and error states in the guard is neatly implemented.  
[kudos for thoughtful design]

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T01:47:40Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/views/auth/CasdoorCallback.vue (1)</summary><blockquote>

`58-69`: **Add fallback for window.close() failure.**

The component relies on `window.close()` which may fail in certain browsers or scenarios (e.g., if the window wasn't opened by JavaScript).

Add a fallback for when `window.close()` fails:

```diff
// Success case
window.opener.postMessage({ success: true }, casdoorService.getServerUrl());
}

// Close the current window
window.close();
+ // Fallback if window.close() fails
+ setTimeout(() => {
+   if (!window.closed) {
+     loading.value = false;
+     router.push('/login-success');
+   }
+ }, 500);

} catch (err: any) {
```

Apply the same pattern to the error case as well.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0a162c516f975dbd2422b691400cf8d783342181 and d6629a9687a6b302efa8ff8e738a17ea232be24c.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/views/auth/CasdoorCallback.vue` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>src/views/auth/CasdoorCallback.vue (6)</summary>

`49-51`: **Authentication state management is inconsistent.**

The callback component stores 'isLoggedIn' in localStorage, which is different from the 'casdoorToken' likely used by the auth service. This creates two separate sources of truth for authentication state.

Consider unifying authentication state management:

```diff
- localStorage.setItem('isLoggedIn', 'true');
+ // Let the auth service handle authentication state consistently
```

---

`59-70`: **Error handling doesn't update the UI before closing the window.**

If `window.close()` fails (e.g., due to popup blocking), the user won't see any error feedback because you're not updating the UI state.

Update the error and loading states before attempting to close:

```diff
catch (err: any) {
    console.error('Callback error:', err);
+   error.value = err.message;
+   loading.value = false;

    // Notify the parent window about the error
    if (window.opener) {
        window.opener.postMessage({ success: false, error: err.message }, casdoorService.getServerUrl());
    }

    // Close the current window
    window.close();
}
```

---

`1-18`: **LGTM: Well-structured responsive UI with appropriate feedback.**

The template provides good user feedback with loading indicators and error messages, and follows responsive design principles.

---

`20-36`: **LGTM: Clean component setup with proper dependency imports.**

The component is well-structured using Vue's Composition API with appropriate reactive references and navigation functions.

---

`37-48`: **LGTM: Proper validation of callback parameters.**

The component correctly validates the presence of required 'code' and 'state' parameters before proceeding with the authentication flow.

---

`53-55`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify origin in postMessage for security.**

Using `postMessage` with the Casdoor server URL as the target origin is secure, but ensure `casdoorService.getServerUrl()` returns the correct origin value.

---

🏁 Script executed:

```shell
#!/bin/bash
# Verify how getServerUrl() is implemented
rg -A 5 "getServerUrl" --type ts
```

Length of output: 280

---



</details>

**Attention: Verify the target origin for postMessage security**

The `casdoorService.getServerUrl()` method (implemented in `src/services/auth.ts`) simply returns `casdoorConfig.serverUrl`. Please ensure that this configuration value is a fully qualified origin (i.e., it includes the protocol, host, and port only, with no trailing paths) so that the target origin in the `postMessage` call is correctly validated.

- Confirm that `casdoorConfig.serverUrl` is set to a trusted and well-formed origin.
- Validate the URL format (scheme, host, port) to prevent any potential misconfiguration.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T01:49:18Z
user: login: 1834423612
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-27T01:57:23Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/views/auth/CasdoorLogin.vue (2)</summary><blockquote>

`92-96`: **Be cautious about the storage event listener.**

The `storage` event only fires when localStorage is modified from other tabs/windows, not from the current tab. Consider using a more reliable approach for tracking login state changes in the current tab.


```diff
// Listen for changes in localStorage to detect login status updates
window.addEventListener('storage', () => {
    checkLoginStatus();
});

+ // Create a wrapper function to update localStorage and the local state
+ const updateLoginState = (state: boolean) => {
+     isLoggedIn.value = state;
+     if (state) {
+         // Only modify other login-related state here
+     }
+ };
```

---

`98-101`: **Consider exposing more state variables to the template.**

The component might benefit from exposing more state variables to the template, such as a loading state during authentication.


```diff
return {
    handleLogin,
-    isLoggedIn
+    isLoggedIn,
+    isLoading: ref(false)
};
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bbe2f6b5be1fde5b6ba12ca7acf38976a50e09dd and 6d24d4016ffae96c605f8ff1b81a781155c0c596.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `components.d.ts` (1 hunks)
* `src/views/auth/CasdoorLogin.vue` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>src/views/auth/CasdoorLogin.vue (3)</summary>

`12-18`: **Login button implementation looks good.**

The login button implementation with conditional rendering based on login status and proper visual feedback through icons provides a good user experience.

---

`55-58`: **Good origin verification for message handling.**

Properly restricting message event processing to only those from the expected origin is a good security practice.

---

`73-79`: **Event listener cleanup is properly implemented.**

The code correctly removes the message event listener after processing the authentication response, which prevents memory leaks.

</details>
<details>
<summary>components.d.ts (1)</summary>

`28-32`: **Icon components added correctly.**

The Material Design icons added to the global components interface will support the authentication UI elements. These are properly typed and follow the existing pattern.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T02:00:58Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>src/views/auth/SucceedView.vue (4)</summary><blockquote>

`49-72`: **Remove commented-out dropdown code.**

There's a substantial block of commented-out code for a dropdown menu that seems to be replaced by a simpler logout button. Clean up the codebase by removing this unused code to improve maintainability.

```diff
-        <!-- <div class="relative">
-          <button 
-            @click="toggleDropdown" 
-            class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
-          >
-            <Icon icon="mdi:logout" class="mr-2 h-4 w-4" />
-            Logout
-            <Icon icon="mdi:chevron-down" class="ml-2 h-4 w-4" />
-          </button>
-          <div v-show="dropdownVisible" class="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg">
-            <button 
-              @click="logout" 
-              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
-            >
-              Logout Locally
-            </button>
-            <button 
-              @click="logoutCasdoor" 
-              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
-            >
-              Logout from Casdoor
-            </button>
-          </div>
-        </div> -->
```

---

`300-302`: **Consider replacing console.error with a logging service in production.**

Direct console.error statements might expose sensitive information in production. Consider implementing a proper logging service that handles different environments appropriately.

```diff
-        console.error('Error loading user info:', err);
+        if (import.meta.env.DEV) {
+          console.error('Error loading user info:', err);
+        }
        error.value = err.message || 'Failed to load user information';
        loading.value = false;
```

---

`312-315`: **Handle console errors conditionally based on environment.**

Similar to the previous issue, console.error statements should be conditionally used only in development environments.

```diff
      router.push({ name: 'login' }).catch(err => {
-        console.error('Failed to navigate to login:', err);
+        if (import.meta.env.DEV) {
+          console.error('Failed to navigate to login:', err);
+        }
      });
```

---

`328-354`: **Remove unused `toggleDropdown` method and related state.**

The `toggleDropdown` method and `dropdownVisible` state are exposed in the return object but the related UI is commented out. Since you're not using the dropdown functionality anymore, these can be removed.

```diff
-    const dropdownVisible = ref(false);
...
-    const toggleDropdown = () => {
-      dropdownVisible.value = !dropdownVisible.value;
-    };
...
    return {
      userInfo,
      loading,
      error,
      googleProperties,
      otherProperties,
      formatPropertyKey,
      formatDate,
      retryFetchUserInfo,
      logout,
      logoutCasdoor,
-      toggleDropdown,
-      dropdownVisible
    };
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6d24d4016ffae96c605f8ff1b81a781155c0c596 and 323fcc414ebce28f1a866fb96af322bb5c961354.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `.env.example` (1 hunks)
* `src/services/auth.ts` (1 hunks)
* `src/views/auth/CasdoorCallback.vue` (1 hunks)
* `src/views/auth/SucceedView.vue` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* .env.example

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* src/views/auth/CasdoorCallback.vue
* src/services/auth.ts

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>src/views/auth/SucceedView.vue (5)</summary>

`187-204`: **Debug information section is correctly implemented for development only.**

Good implementation of showing debug information only in development environments, which addresses a previous review comment.

---

`291-298`: **Remove commented-out console.log statements.**

The console.log statements are currently commented out but should be completely removed before deploying to production to maintain clean code.

```diff
        // Get user info
        const userData = await casdoorService.getUserInfo();
-        // console.log('Raw user data from service:', userData);
        
        // Make sure we're getting all properties
        userInfo.value = { ...userData };
        
-        // console.log('User info after assignment:', userInfo.value);
        loading.value = false;
```

---

`1-208`: **Well-structured UI with good loading, error, and success states.**

The component has a well-organized UI with clear sections for different states (loading, error, success). The use of icons and appropriate messaging provides good user feedback, and the presentation of user information is clean and organized.

---

`244-255`: **Good use of computed properties for filtering and organizing data.**

Using computed properties to filter and organize the user properties provides clean, reusable access to specific data subsets, which is a good practice in Vue components.

---

`265-283`: **Well-implemented date formatting with proper error handling.**

The date formatting function includes proper error handling to gracefully handle invalid date strings, which is a good defensive programming practice.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T02:13:46Z

Review comments:
diff_hunk: @@ -0,0 +1,80 @@
+<template>
+    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+        <div class="max-w-md w-full text-center">
+            <div v-if="loading" class="space-y-4">
+                <Icon icon="mdi:loading" class="h-12 w-12 mx-auto animate-spin text-indigo-600" />
+                <p class="text-lg font-medium text-gray-700">Processing login...</p>
+            </div>
+            <div v-if="error" class="space-y-4 text-red-500">
+                <Icon icon="mdi:alert-circle" class="h-12 w-12 mx-auto text-red-500" />
+                <p class="text-lg font-medium">{{ error }}</p>
+                <button @click="goToLogin"
+                    class="mt-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
+                    Return to Login Page
+                </button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, onMounted } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { casdoorService } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+    name: 'CallbackView',
+    setup() {
+        const loading = ref(true);
+        const error = ref('');
+        const route = useRoute();
+        const router = useRouter();
+
+        const goToLogin = () => {
+            router.push('/login');
+        };
+
+        onMounted(async () => {
+            try {
+                const code = route.query.code as string;
+                const state = route.query.state as string;
+
+                if (!code || !state) {
+                    throw new Error('Invalid authorization code or state');
+                }
+
+                // Process the callback
+                await casdoorService.handleCallback(code, state);
+
+                // Store login status in localStorage
+                localStorage.setItem('isLoggedIn', 'true');
+
path: src/views/auth/CasdoorCallback.vue
user: login: Copilot
body: The callback component sets an 'isLoggedIn' flag in localStorage, which is inconsistent with the authentication state managed using 'casdoorToken' in the auth service. Consider unifying the login status representation to avoid potential state inconsistencies.
```suggestion

```
created_at: 2025-03-27T01:45:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+<template>
+    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+        <div class="max-w-md w-full space-y-8">
+            <div class="text-center">
+                <h2 class="mt-6 text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
+                <p class="mt-2 text-sm text-gray-600">
+                    Authenticate with Casdoor
+                </p>
+            </div>
+
+            <div class="mt-8">
+                <button v-if="!isLoggedIn" @click="handleLogin"
+                    class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
+                    <span class="absolute left-0 inset-y-0 flex items-center pl-3">
+                        <Icon icon="mdi:login" class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" />
+                    </span>
+                    Sign in with Casdoor
+                </button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { casdoorService } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+    name: 'LoginView',
+    components: {
+        Icon
+    },
+    setup() {
+        const router = useRouter();
+        const isLoggedIn = ref(false);
+
+        const handleLogin = () => {
+            // Open Casdoor login page in a new window
+            const loginWindow = window.open(
+                casdoorService.getSigninUrl(),
+                '_blank',
+                'width=500,height=600'
+            );
+
+            // Listen for messages from the login window
+            const messageListener = (event: MessageEvent) => {
+                if (event.origin !== casdoorService.getServerUrl()) {
+                    return; // Ignore messages from unknown origins
+                }
+
+                const { success, error } = event.data;
+
+                if (success) {
+                    // Login successful, navigate to success page
+                    router.push({ name: 'login-success' });
+                } else if (error) {
+                    // Handle login error
+                    console.error('Login failed:', error);
+                }
+
+                // Clean up listener and close the login window
+                window.removeEventListener('message', messageListener);
+                loginWindow?.close();
+            };
+
+            window.addEventListener('message', messageListener);
+        };
+
+        const checkLoginStatus = () => {
+            if (localStorage.getItem('isLoggedIn') === 'true') {
path: src/views/auth/CasdoorLogin.vue
user: login: Copilot
body: The login view relies on a separate 'isLoggedIn' flag, while the auth service uses the presence of 'casdoorToken' for authentication state. Consolidate the approach by using a single source of truth (e.g., 'casdoorToken') for determining if a user is logged in.
```suggestion
            if (localStorage.getItem('casdoorToken')) {
```
created_at: 2025-03-27T01:45:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+<template>
+    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+        <div class="max-w-md w-full space-y-8">
+            <div class="text-center">
+                <h2 class="mt-6 text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
+                <p class="mt-2 text-sm text-gray-600">
+                    Authenticate with Casdoor
+                </p>
+            </div>
+
+            <div class="mt-8">
+                <button v-if="!isLoggedIn" @click="handleLogin"
+                    class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
+                    <span class="absolute left-0 inset-y-0 flex items-center pl-3">
+                        <Icon icon="mdi:login" class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" />
+                    </span>
+                    Sign in with Casdoor
+                </button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { casdoorService } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+    name: 'LoginView',
+    components: {
+        Icon
+    },
+    setup() {
+        const router = useRouter();
+        const isLoggedIn = ref(false);
+
+        const handleLogin = () => {
+            // Open Casdoor login page in a new window
+            const loginWindow = window.open(
+                casdoorService.getSigninUrl(),
+                '_blank',
+                'width=500,height=600'
+            );
path: src/views/auth/CasdoorLogin.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Handle potential popup blocker issues.**

The login window is opened with `window.open()` but there's no check to ensure the window was successfully created. Popup blockers could prevent the window from opening, resulting in a confusing user experience.

```diff
            const loginWindow = window.open(
                casdoorService.getSigninUrl(),
                '_blank',
                'width=500,height=600'
            );
+            
+            if (!loginWindow || loginWindow.closed || typeof loginWindow.closed === 'undefined') {
+                console.error('Popup blocked or window failed to open');
+                // Show user-friendly error message about popup blockers
+                alert('Please allow popups for this site to continue with login');
+                return;
+            }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        const handleLogin = () => {
            // Open Casdoor login page in a new window
            const loginWindow = window.open(
                casdoorService.getSigninUrl(),
                '_blank',
                'width=500,height=600'
            );
            
            if (!loginWindow || loginWindow.closed || typeof loginWindow.closed === 'undefined') {
                console.error('Popup blocked or window failed to open');
                // Show user-friendly error message about popup blockers
                alert('Please allow popups for this site to continue with login');
                return;
            }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-27T01:47:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,98 @@
+<template>
+    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+        <div class="max-w-md w-full space-y-8">
+            <div class="text-center">
+                <h2 class="mt-6 text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
+                <p class="mt-2 text-sm text-gray-600">
+                    Authenticate with Casdoor
+                </p>
+            </div>
+
+            <div class="mt-8">
+                <button v-if="!isLoggedIn" @click="handleLogin"
+                    class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
+                    <span class="absolute left-0 inset-y-0 flex items-center pl-3">
+                        <Icon icon="mdi:login" class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" />
+                    </span>
+                    Sign in with Casdoor
+                </button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { casdoorService } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+    name: 'LoginView',
+    components: {
+        Icon
+    },
+    setup() {
+        const router = useRouter();
+        const isLoggedIn = ref(false);
+
+        const handleLogin = () => {
+            // Open Casdoor login page in a new window
+            const loginWindow = window.open(
+                casdoorService.getSigninUrl(),
+                '_blank',
+                'width=500,height=600'
+            );
+
+            // Listen for messages from the login window
+            const messageListener = (event: MessageEvent) => {
+                if (event.origin !== casdoorService.getServerUrl()) {
+                    return; // Ignore messages from unknown origins
+                }
+
+                const { success, error } = event.data;
+
+                if (success) {
+                    // Login successful, navigate to success page
+                    router.push({ name: 'login-success' });
+                } else if (error) {
+                    // Handle login error
+                    console.error('Login failed:', error);
+                }
+
+                // Clean up listener and close the login window
+                window.removeEventListener('message', messageListener);
+                loginWindow?.close();
+            };
+
+            window.addEventListener('message', messageListener);
+        };
+
+        const checkLoginStatus = () => {
+            if (localStorage.getItem('isLoggedIn') === 'true') {
+                router.push({ name: 'login-success' });
+            }
+        };
+
+        onMounted(() => {
+            // Check if user is already logged in
+            checkLoginStatus();
+
+            // Listen for changes in localStorage to detect login status updates
+            window.addEventListener('storage', () => {
+                checkLoginStatus();
+            });
+
+            // 强制重定向已登录用户
+            if (localStorage.getItem('isLoggedIn') === 'true') {
+                router.push({ name: 'login-success' });
+            }
path: src/views/auth/CasdoorLogin.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Remove duplicate code.**

The code at lines 87-89 duplicates the login check and redirect logic that was already performed in the `checkLoginStatus()` function called at line 79.

```diff
            // 强制重定向已登录用户
-            if (localStorage.getItem('isLoggedIn') === 'true') {
-                router.push({ name: 'login-success' });
-            }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            // 强制重定向已登录用户
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-27T01:47:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,98 @@
+<template>
+    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+        <div class="max-w-md w-full space-y-8">
+            <div class="text-center">
+                <h2 class="mt-6 text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
+                <p class="mt-2 text-sm text-gray-600">
+                    Authenticate with Casdoor
+                </p>
+            </div>
+
+            <div class="mt-8">
+                <button v-if="!isLoggedIn" @click="handleLogin"
+                    class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
+                    <span class="absolute left-0 inset-y-0 flex items-center pl-3">
+                        <Icon icon="mdi:login" class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" />
+                    </span>
+                    Sign in with Casdoor
+                </button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { casdoorService } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+    name: 'LoginView',
+    components: {
+        Icon
+    },
+    setup() {
+        const router = useRouter();
+        const isLoggedIn = ref(false);
+
+        const handleLogin = () => {
+            // Open Casdoor login page in a new window
+            const loginWindow = window.open(
+                casdoorService.getSigninUrl(),
+                '_blank',
+                'width=500,height=600'
+            );
+
+            // Listen for messages from the login window
+            const messageListener = (event: MessageEvent) => {
+                if (event.origin !== casdoorService.getServerUrl()) {
+                    return; // Ignore messages from unknown origins
+                }
+
+                const { success, error } = event.data;
+
+                if (success) {
+                    // Login successful, navigate to success page
+                    router.push({ name: 'login-success' });
+                } else if (error) {
+                    // Handle login error
+                    console.error('Login failed:', error);
+                }
path: src/views/auth/CasdoorLogin.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve error handling for login failures.**

The current implementation logs errors to the console, but provides no user feedback when login fails. Users won't know what went wrong.

```diff
                const { success, error } = event.data;

                if (success) {
                    // Login successful, navigate to success page
                    router.push({ name: 'login-success' });
                } else if (error) {
                    // Handle login error
                    console.error('Login failed:', error);
+                    isLoggedIn.value = false;
+                    // Provide user feedback about the error
+                    alert(`Login failed: ${error || 'Unknown error'}`);
                }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                const { success, error } = event.data;

                if (success) {
                    // Login successful, navigate to success page
                    router.push({ name: 'login-success' });
                } else if (error) {
                    // Handle login error
                    console.error('Login failed:', error);
                    isLoggedIn.value = false;
                    // Provide user feedback about the error
                    alert(`Login failed: ${error || 'Unknown error'}`);
                }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-27T01:47:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,358 @@
+<template>
+  <div class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+    <div class="max-w-4xl mx-auto">
+      <div class="text-center mb-10">
+    <h2 class="text-3xl font-extrabold text-gray-900">Login Successful</h2>
+    <p class="mt-2 text-lg text-gray-600">
+      You have successfully authenticated with Casdoor.
+    </p>
+  </div>
+
+  <div v-if="loading" class="text-center py-10">
+    <Icon icon="mdi:loading" class="h-10 w-10 mx-auto animate-spin text-indigo-600" />
+    <p class="mt-4 text-gray-600">Loading user information...</p>
+  </div>
+
+  <div v-else-if="error" class="text-center py-10 text-red-500">
+    <Icon icon="mdi:alert-circle" class="h-10 w-10 mx-auto text-red-500" />
+    <p class="mt-4">{{ error }}</p>
+    <button 
+      @click="retryFetchUserInfo" 
+      class="mt-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
+    >
+      Retry
+    </button>
+    <button 
+      @click="logout" 
+      class="mt-4 ml-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
+    >
+      Logout
+    </button>
+  </div>
+
+  <div v-else>
+    <!-- User Profile Card -->
+    <div class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+      <div class="px-4 py-5 sm:px-6 flex justify-between items-center">
+        <div class="flex items-center">
+          <img 
+            v-if="userInfo.avatar" 
+            :src="userInfo.avatar" 
+            alt="User Avatar" 
+            class="h-16 w-16 rounded-full mr-4 object-cover border-2 border-indigo-200"
+          />
+          <div>
+            <h3 class="text-lg leading-6 font-medium text-gray-900">{{ userInfo.displayName || userInfo.name }}</h3>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">{{ userInfo.email }}</p>
+          </div>
+        </div>
+        <!-- <div class="relative">
+          <button 
+            @click="toggleDropdown" 
+            class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
+          >
+            <Icon icon="mdi:logout" class="mr-2 h-4 w-4" />
+            Logout
+            <Icon icon="mdi:chevron-down" class="ml-2 h-4 w-4" />
+          </button>
+          <div v-show="dropdownVisible" class="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg">
+            <button 
+              @click="logout" 
+              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
+            >
+              Logout Locally
+            </button>
+            <button 
+              @click="logoutCasdoor" 
+              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
+            >
+              Logout from Casdoor
+            </button>
+          </div>
+        </div> -->
+        <div class="relative">
+          <button 
+            @click="logout" 
+            class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
+          >
+            <Icon icon="mdi:logout" class="mr-2 h-4 w-4" />
+            Logout
+          </button>
+        </div>
+      </div>
+          
+          <div class="border-t border-gray-200">
+            <dl>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Username</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.name || 'Not set' }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Display Name</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.displayName || 'Not set' }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Email</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.email || 'Not set' }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">ID</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.id || 'Not set' }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Created Time</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userInfo.createdTime) }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Updated Time</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userInfo.updatedTime) }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Organization</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.owner || 'Not set' }}</dd>
+              </div>
+            </dl>
+          </div>
+        </div>
+        
+        <!-- Google Authentication Card -->
+        <div v-if="userInfo.google" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+          <div class="px-4 py-5 sm:px-6">
+            <div class="flex items-center">
+              <Icon icon="mdi:google" class="h-6 w-6 text-red-500 mr-2" />
+              <h3 class="text-lg leading-6 font-medium text-gray-900">Google Account Linked</h3>
+            </div>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">Your account is linked to a Google account.</p>
+          </div>
+          
+          <div class="border-t border-gray-200">
+            <dl>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Google ID</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.google }}</dd>
+              </div>
+              
+              <div v-if="userInfo.properties && Object.keys(userInfo.properties).some(key => key.startsWith('oauth_Google'))" class="bg-white px-4 py-5 sm:px-6">
+                <details class="w-full">
+                  <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none">
+                    View Google Account Details
+                  </summary>
+                  <div class="mt-4 border border-gray-200 rounded-md overflow-hidden">
+                    <dl>
+                      <div v-for="(value, key, index) in googleProperties" :key="key" 
+                           :class="{'bg-gray-50': index % 2 === 0, 'bg-white': index % 2 === 1}"
+                           class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                        <dt class="text-sm font-medium text-gray-500">{{ formatPropertyKey(key) }}</dt>
+                        <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
+                          <div v-if="key.includes('avatarUrl')">
+                            <img :src="value" alt="Google Avatar" class="h-10 w-10 rounded-full" />
+                          </div>
+                          <div v-else>{{ value }}</div>
+                        </dd>
+                      </div>
+                    </dl>
+                  </div>
+                </details>
+              </div>
+            </dl>
+          </div>
+        </div>
+        
+        <!-- Properties Card -->
+        <div v-if="userInfo.properties && Object.keys(userInfo.properties).length > 0" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+          <div class="px-4 py-5 sm:px-6">
+            <h3 class="text-lg leading-6 font-medium text-gray-900">Other Properties</h3>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">Additional user properties.</p>
+          </div>
+          
+          <div class="border-t border-gray-200">
+            <details class="w-full">
+              <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none px-4 py-3">
+                View All Properties
+              </summary>
+              <div class="border-t border-gray-200">
+                <dl>
+                  <div v-for="(value, key, index) in otherProperties" :key="key" 
+                       :class="{'bg-gray-50': index % 2 === 0, 'bg-white': index % 2 === 1}"
+                       class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                    <dt class="text-sm font-medium text-gray-500">{{ key }}</dt>
+                    <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ value }}</dd>
+                  </div>
+                </dl>
+              </div>
+            </details>
+          </div>
+        </div>
+
+        <div v-if="!loading && !error" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+        <div class="px-4 py-5 sm:px-6">
+          <h3 class="text-lg leading-6 font-medium text-gray-900">Debug Information</h3>
+          <p class="mt-1 max-w-2xl text-sm text-gray-500">Raw user data</p>
+        </div>
+        
+        <div class="border-t border-gray-200">
+          <!-- open -->
+          <details class="w-full">
+            <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none px-4 py-3">
+              View Raw Data
+            </summary>
+            <div class="border-t border-gray-200 p-4">
+              <pre class="text-xs overflow-auto bg-gray-100 p-2 rounded">{{ JSON.stringify(userInfo, null, 2) }}</pre>
+            </div>
+          </details>
+        </div>
+      </div>
path: src/views/auth/SucceedView.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider removing debug information in production.**

The Debug Information section with raw user data might expose sensitive information. Consider adding a conditional to only show this in development environments.

```diff
-        <div v-if="!loading && !error" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+        <div v-if="!loading && !error && import.meta.env.DEV" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
        <div class="px-4 py-5 sm:px-6">
          <h3 class="text-lg leading-6 font-medium text-gray-900">Debug Information</h3>
          <p class="mt-1 max-w-2xl text-sm text-gray-500">Raw user data</p>
        </div>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        <div v-if="!loading && !error && import.meta.env.DEV" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
        <div class="px-4 py-5 sm:px-6">
          <h3 class="text-lg leading-6 font-medium text-gray-900">Debug Information</h3>
          <p class="mt-1 max-w-2xl text-sm text-gray-500">Raw user data</p>
        </div>
        
        <div class="border-t border-gray-200">
          <!-- open -->
          <details class="w-full">
            <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none px-4 py-3">
              View Raw Data
            </summary>
            <div class="border-t border-gray-200 p-4">
              <pre class="text-xs overflow-auto bg-gray-100 p-2 rounded">{{ JSON.stringify(userInfo, null, 2) }}</pre>
            </div>
          </details>
        </div>
      </div>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-27T01:47:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,358 @@
+<template>
+  <div class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+    <div class="max-w-4xl mx-auto">
+      <div class="text-center mb-10">
+    <h2 class="text-3xl font-extrabold text-gray-900">Login Successful</h2>
+    <p class="mt-2 text-lg text-gray-600">
+      You have successfully authenticated with Casdoor.
+    </p>
+  </div>
+
+  <div v-if="loading" class="text-center py-10">
+    <Icon icon="mdi:loading" class="h-10 w-10 mx-auto animate-spin text-indigo-600" />
+    <p class="mt-4 text-gray-600">Loading user information...</p>
+  </div>
+
+  <div v-else-if="error" class="text-center py-10 text-red-500">
+    <Icon icon="mdi:alert-circle" class="h-10 w-10 mx-auto text-red-500" />
+    <p class="mt-4">{{ error }}</p>
+    <button 
+      @click="retryFetchUserInfo" 
+      class="mt-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
+    >
+      Retry
+    </button>
+    <button 
+      @click="logout" 
+      class="mt-4 ml-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
+    >
+      Logout
+    </button>
+  </div>
+
+  <div v-else>
+    <!-- User Profile Card -->
+    <div class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+      <div class="px-4 py-5 sm:px-6 flex justify-between items-center">
+        <div class="flex items-center">
+          <img 
+            v-if="userInfo.avatar" 
+            :src="userInfo.avatar" 
+            alt="User Avatar" 
+            class="h-16 w-16 rounded-full mr-4 object-cover border-2 border-indigo-200"
+          />
+          <div>
+            <h3 class="text-lg leading-6 font-medium text-gray-900">{{ userInfo.displayName || userInfo.name }}</h3>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">{{ userInfo.email }}</p>
+          </div>
+        </div>
+        <!-- <div class="relative">
+          <button 
+            @click="toggleDropdown" 
+            class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
+          >
+            <Icon icon="mdi:logout" class="mr-2 h-4 w-4" />
+            Logout
+            <Icon icon="mdi:chevron-down" class="ml-2 h-4 w-4" />
+          </button>
+          <div v-show="dropdownVisible" class="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg">
+            <button 
+              @click="logout" 
+              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
+            >
+              Logout Locally
+            </button>
+            <button 
+              @click="logoutCasdoor" 
+              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
+            >
+              Logout from Casdoor
+            </button>
+          </div>
+        </div> -->
+        <div class="relative">
+          <button 
+            @click="logout" 
+            class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
+          >
+            <Icon icon="mdi:logout" class="mr-2 h-4 w-4" />
+            Logout
+          </button>
+        </div>
+      </div>
+          
+          <div class="border-t border-gray-200">
+            <dl>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Username</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.name || 'Not set' }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Display Name</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.displayName || 'Not set' }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Email</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.email || 'Not set' }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">ID</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.id || 'Not set' }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Created Time</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userInfo.createdTime) }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Updated Time</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userInfo.updatedTime) }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Organization</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.owner || 'Not set' }}</dd>
+              </div>
+            </dl>
+          </div>
+        </div>
+        
+        <!-- Google Authentication Card -->
+        <div v-if="userInfo.google" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+          <div class="px-4 py-5 sm:px-6">
+            <div class="flex items-center">
+              <Icon icon="mdi:google" class="h-6 w-6 text-red-500 mr-2" />
+              <h3 class="text-lg leading-6 font-medium text-gray-900">Google Account Linked</h3>
+            </div>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">Your account is linked to a Google account.</p>
+          </div>
+          
+          <div class="border-t border-gray-200">
+            <dl>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Google ID</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.google }}</dd>
+              </div>
+              
+              <div v-if="userInfo.properties && Object.keys(userInfo.properties).some(key => key.startsWith('oauth_Google'))" class="bg-white px-4 py-5 sm:px-6">
+                <details class="w-full">
+                  <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none">
+                    View Google Account Details
+                  </summary>
+                  <div class="mt-4 border border-gray-200 rounded-md overflow-hidden">
+                    <dl>
+                      <div v-for="(value, key, index) in googleProperties" :key="key" 
+                           :class="{'bg-gray-50': index % 2 === 0, 'bg-white': index % 2 === 1}"
+                           class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                        <dt class="text-sm font-medium text-gray-500">{{ formatPropertyKey(key) }}</dt>
+                        <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
+                          <div v-if="key.includes('avatarUrl')">
+                            <img :src="value" alt="Google Avatar" class="h-10 w-10 rounded-full" />
+                          </div>
+                          <div v-else>{{ value }}</div>
+                        </dd>
+                      </div>
+                    </dl>
+                  </div>
+                </details>
+              </div>
+            </dl>
+          </div>
+        </div>
+        
+        <!-- Properties Card -->
+        <div v-if="userInfo.properties && Object.keys(userInfo.properties).length > 0" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+          <div class="px-4 py-5 sm:px-6">
+            <h3 class="text-lg leading-6 font-medium text-gray-900">Other Properties</h3>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">Additional user properties.</p>
+          </div>
+          
+          <div class="border-t border-gray-200">
+            <details class="w-full">
+              <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none px-4 py-3">
+                View All Properties
+              </summary>
+              <div class="border-t border-gray-200">
+                <dl>
+                  <div v-for="(value, key, index) in otherProperties" :key="key" 
+                       :class="{'bg-gray-50': index % 2 === 0, 'bg-white': index % 2 === 1}"
+                       class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                    <dt class="text-sm font-medium text-gray-500">{{ key }}</dt>
+                    <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ value }}</dd>
+                  </div>
+                </dl>
+              </div>
+            </details>
+          </div>
+        </div>
+
+        <div v-if="!loading && !error" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+        <div class="px-4 py-5 sm:px-6">
+          <h3 class="text-lg leading-6 font-medium text-gray-900">Debug Information</h3>
+          <p class="mt-1 max-w-2xl text-sm text-gray-500">Raw user data</p>
+        </div>
+        
+        <div class="border-t border-gray-200">
+          <!-- open -->
+          <details class="w-full">
+            <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none px-4 py-3">
+              View Raw Data
+            </summary>
+            <div class="border-t border-gray-200 p-4">
+              <pre class="text-xs overflow-auto bg-gray-100 p-2 rounded">{{ JSON.stringify(userInfo, null, 2) }}</pre>
+            </div>
+          </details>
+        </div>
+      </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, onMounted, computed } from 'vue';
+import { useRouter } from 'vue-router';
+import { casdoorService, UserInfo } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+  name: 'SucceedView',
+  components: {
+    Icon
+  },
+  setup() {
+    const router = useRouter();
+    const loading = ref(true);
+    const error = ref('');
+    const userInfo = ref<UserInfo>({
+      id: '',
+      name: '',
+      email: '',
+      owner: ''
+    });
+    const dropdownVisible = ref(false);
+
+    // Computed property to get Google-related properties
+    const googleProperties = computed(() => {
+      if (!userInfo.value.properties) return {};
+      
+      return Object.entries(userInfo.value.properties)
+        .filter(([key]) => key.startsWith('oauth_Google'))
+        .reduce((acc, [key, value]) => {
+          acc[key] = value;
+          return acc;
+        }, {} as Record<string, any>);
+    });
+    
+    // Computed property to get non-Google properties
+    const otherProperties = computed(() => {
+      if (!userInfo.value.properties) return {};
+      
+      return Object.entries(userInfo.value.properties)
+        .filter(([key]) => !key.startsWith('oauth_Google'))
+        .reduce((acc, [key, value]) => {
+          acc[key] = value;
+          return acc;
+        }, {} as Record<string, any>);
+    });
+
+    // Format property keys for display
+    const formatPropertyKey = (key: string) => {
+      if (key.startsWith('oauth_Google_')) {
+        return key.replace('oauth_Google_', '');
+      }
+      return key;
+    };
+    
+    // Format date for display
+    const formatDate = (dateString?: string) => {
+      if (!dateString) return 'Not set';
+      
+      try {
+        const date = new Date(dateString);
+        return new Intl.DateTimeFormat('en-US', {
+          year: 'numeric',
+          month: 'long',
+          day: 'numeric',
+          hour: '2-digit',
+          minute: '2-digit',
+          second: '2-digit',
+          hour12: false
+        }).format(date);
+      } catch (e) {
+        return dateString;
+      }
+    };
+
+    const fetchUserInfo = async () => {
+      loading.value = true;
+      error.value = '';
+      
+      try {
+        // Get user info
+        const userData = await casdoorService.getUserInfo();
+        console.log('Raw user data from service:', userData);
+        
+        // Make sure we're getting all properties
+        userInfo.value = { ...userData };
+        
+        console.log('User info after assignment:', userInfo.value);
+        loading.value = false;
path: src/views/auth/SucceedView.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Remove development console logs.**

There are multiple console.log statements that should be removed before deploying to production to avoid potential information leakage and improve performance.

```diff
        // Get user info
        const userData = await casdoorService.getUserInfo();
-        console.log('Raw user data from service:', userData);
        
        // Make sure we're getting all properties
        userInfo.value = { ...userData };
        
-        console.log('User info after assignment:', userInfo.value);
        loading.value = false;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        // Get user info
        const userData = await casdoorService.getUserInfo();
        // Make sure we're getting all properties
        userInfo.value = { ...userData };
        loading.value = false;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-27T01:47:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,80 @@
+<template>
+    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+        <div class="max-w-md w-full text-center">
+            <div v-if="loading" class="space-y-4">
+                <Icon icon="mdi:loading" class="h-12 w-12 mx-auto animate-spin text-indigo-600" />
+                <p class="text-lg font-medium text-gray-700">Processing login...</p>
+            </div>
+            <div v-if="error" class="space-y-4 text-red-500">
+                <Icon icon="mdi:alert-circle" class="h-12 w-12 mx-auto text-red-500" />
+                <p class="text-lg font-medium">{{ error }}</p>
+                <button @click="goToLogin"
+                    class="mt-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
+                    Return to Login Page
+                </button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, onMounted } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { casdoorService } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+    name: 'CallbackView',
+    setup() {
+        const loading = ref(true);
+        const error = ref('');
+        const route = useRoute();
+        const router = useRouter();
+
+        const goToLogin = () => {
+            router.push('/login');
+        };
+
+        onMounted(async () => {
+            try {
+                const code = route.query.code as string;
+                const state = route.query.state as string;
+
+                if (!code || !state) {
+                    throw new Error('Invalid authorization code or state');
+                }
+
+                // Process the callback
+                await casdoorService.handleCallback(code, state);
+
+                // Store login status in localStorage
+                localStorage.setItem('isLoggedIn', 'true');
+
+                // Notify the parent window about the success
+                if (window.opener) {
+                    window.opener.postMessage({ success: true }, casdoorService.getServerUrl());
+                }
+
+                // Close the current window
+                window.close();
+            } catch (err: any) {
+                console.error('Callback error:', err);
+
+                // Notify the parent window about the error
+                if (window.opener) {
+                    window.opener.postMessage({ success: false, error: err.message }, casdoorService.getServerUrl());
+                }
+
+                // Close the current window
+                window.close();
+            }
+        });
path: src/views/auth/CasdoorCallback.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Error handling is adequate but does not surface the error message on-screen.**

You log the error and close the window, but you never assign `error.value = err.message` or set `loading.value = false`. If the window remains open (e.g., blocked pop-ups), the user will not see the error text. Set these fields to improve user feedback when the window cannot close automatically.

```diff
- console.error('Callback error:', err);
+ console.error('Callback error:', err);
+ error.value = err.message;
+ loading.value = false;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            } catch (err: any) {
                console.error('Callback error:', err);
                error.value = err.message;
                loading.value = false;

                // Notify the parent window about the error
                if (window.opener) {
                    window.opener.postMessage({ success: false, error: err.message }, casdoorService.getServerUrl());
                }

                // Close the current window
                window.close();
            }
        });
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-27T01:47:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,330 @@
+import SDK from 'casdoor-js-sdk';
+
+// Casdoor configuration
+export const casdoorConfig = {
+    serverUrl: 'https://695cas.fastbirdcdn.online',
+    clientId: '300932808273326bac0c',
+    clientSecret: 'b134d448a828a80685c4fd5d0b661cc41f1a841f',
+    appName: '695_website',
+    organizationName: 'Team695',
+    redirectPath: '/callback',
+    signinPath: '/api/signin',
+    // Use localStorage instead of sessionStorage to persist state across tabs
+    storage: localStorage
+};
path: src/services/auth.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Avoid embedding client secret in frontend code.**

While the SDK requires these values, exposing `clientSecret` in client code is risky for production. Consider moving this secret to a secured server endpoint or environment variables to mitigate exposure.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-27T01:47:40Z
author_association: NONE
diff_hunk: @@ -0,0 +1,98 @@
+<template>
+    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+        <div class="max-w-md w-full space-y-8">
+            <div class="text-center">
+                <h2 class="mt-6 text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
+                <p class="mt-2 text-sm text-gray-600">
+                    Authenticate with Casdoor
+                </p>
+            </div>
+
+            <div class="mt-8">
+                <button v-if="!isLoggedIn" @click="handleLogin"
+                    class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
+                    <span class="absolute left-0 inset-y-0 flex items-center pl-3">
+                        <Icon icon="mdi:login" class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" />
+                    </span>
+                    Sign in with Casdoor
+                </button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { casdoorService } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+    name: 'LoginView',
+    components: {
+        Icon
+    },
+    setup() {
+        const router = useRouter();
+        const isLoggedIn = ref(false);
+
+        const handleLogin = () => {
+            // Open Casdoor login page in a new window
+            const loginWindow = window.open(
+                casdoorService.getSigninUrl(),
+                '_blank',
+                'width=500,height=600'
+            );
+
+            // Listen for messages from the login window
+            const messageListener = (event: MessageEvent) => {
+                if (event.origin !== casdoorService.getServerUrl()) {
+                    return; // Ignore messages from unknown origins
+                }
+
+                const { success, error } = event.data;
+
+                if (success) {
+                    // Login successful, navigate to success page
+                    router.push({ name: 'login-success' });
+                } else if (error) {
+                    // Handle login error
+                    console.error('Login failed:', error);
+                }
+
+                // Clean up listener and close the login window
+                window.removeEventListener('message', messageListener);
+                loginWindow?.close();
+            };
+
+            window.addEventListener('message', messageListener);
+        };
+
+        const checkLoginStatus = () => {
+            if (localStorage.getItem('isLoggedIn') === 'true') {
path: src/views/auth/CasdoorLogin.vue
user: login: 1834423612
body: done
created_at: 2025-03-27T01:57:22Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,104 @@
+<template>
+    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+        <div class="max-w-md w-full space-y-8">
+            <div class="text-center">
+                <h2 class="mt-6 text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
+                <p class="mt-2 text-sm text-gray-600">
+                    Authenticate with Casdoor
+                </p>
+            </div>
+
+            <div class="mt-8">
+                <button v-if="!isLoggedIn" @click="handleLogin"
+                    class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
+                    <span class="absolute left-0 inset-y-0 flex items-center pl-3">
+                        <Icon icon="mdi:login" class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" />
+                    </span>
+                    Sign in with Casdoor
+                </button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { casdoorService } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+    name: 'LoginView',
+    components: {
+        Icon
+    },
+    setup() {
+        const router = useRouter();
+        const isLoggedIn = ref(false);
+
path: src/views/auth/CasdoorLogin.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Ensure login state consistency.**

The `isLoggedIn` ref is used for UI rendering but isn't being updated to reflect the actual login state from localStorage. This could lead to inconsistent UI states.


```diff
const isLoggedIn = ref(false);

// Check if user is already logged in
const checkLoginStatus = () => {
    if (localStorage.getItem('casdoorToken')) {
+        isLoggedIn.value = true;
        router.push({ name: 'login-success' });
    } else {
+        isLoggedIn.value = false;
    }
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        const isLoggedIn = ref(false);

        // Check if user is already logged in
        const checkLoginStatus = () => {
            if (localStorage.getItem('casdoorToken')) {
                isLoggedIn.value = true;
                router.push({ name: 'login-success' });
            } else {
                isLoggedIn.value = false;
            }
        };
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-27T02:00:57Z
author_association: NONE
diff_hunk: @@ -0,0 +1,358 @@
+<template>
+  <div class="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+    <div class="max-w-4xl mx-auto">
+      <div class="text-center mb-10">
+    <h2 class="text-3xl font-extrabold text-gray-900">Login Successful</h2>
+    <p class="mt-2 text-lg text-gray-600">
+      You have successfully authenticated with Casdoor.
+    </p>
+  </div>
+
+  <div v-if="loading" class="text-center py-10">
+    <Icon icon="mdi:loading" class="h-10 w-10 mx-auto animate-spin text-indigo-600" />
+    <p class="mt-4 text-gray-600">Loading user information...</p>
+  </div>
+
+  <div v-else-if="error" class="text-center py-10 text-red-500">
+    <Icon icon="mdi:alert-circle" class="h-10 w-10 mx-auto text-red-500" />
+    <p class="mt-4">{{ error }}</p>
+    <button 
+      @click="retryFetchUserInfo" 
+      class="mt-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
+    >
+      Retry
+    </button>
+    <button 
+      @click="logout" 
+      class="mt-4 ml-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
+    >
+      Logout
+    </button>
+  </div>
+
+  <div v-else>
+    <!-- User Profile Card -->
+    <div class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+      <div class="px-4 py-5 sm:px-6 flex justify-between items-center">
+        <div class="flex items-center">
+          <img 
+            v-if="userInfo.avatar" 
+            :src="userInfo.avatar" 
+            alt="User Avatar" 
+            class="h-16 w-16 rounded-full mr-4 object-cover border-2 border-indigo-200"
+          />
+          <div>
+            <h3 class="text-lg leading-6 font-medium text-gray-900">{{ userInfo.displayName || userInfo.name }}</h3>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">{{ userInfo.email }}</p>
+          </div>
+        </div>
+        <!-- <div class="relative">
+          <button 
+            @click="toggleDropdown" 
+            class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
+          >
+            <Icon icon="mdi:logout" class="mr-2 h-4 w-4" />
+            Logout
+            <Icon icon="mdi:chevron-down" class="ml-2 h-4 w-4" />
+          </button>
+          <div v-show="dropdownVisible" class="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg">
+            <button 
+              @click="logout" 
+              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
+            >
+              Logout Locally
+            </button>
+            <button 
+              @click="logoutCasdoor" 
+              class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
+            >
+              Logout from Casdoor
+            </button>
+          </div>
+        </div> -->
+        <div class="relative">
+          <button 
+            @click="logout" 
+            class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
+          >
+            <Icon icon="mdi:logout" class="mr-2 h-4 w-4" />
+            Logout
+          </button>
+        </div>
+      </div>
+          
+          <div class="border-t border-gray-200">
+            <dl>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Username</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.name || 'Not set' }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Display Name</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.displayName || 'Not set' }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Email</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.email || 'Not set' }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">ID</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.id || 'Not set' }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Created Time</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userInfo.createdTime) }}</dd>
+              </div>
+              <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Updated Time</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userInfo.updatedTime) }}</dd>
+              </div>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Organization</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.owner || 'Not set' }}</dd>
+              </div>
+            </dl>
+          </div>
+        </div>
+        
+        <!-- Google Authentication Card -->
+        <div v-if="userInfo.google" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+          <div class="px-4 py-5 sm:px-6">
+            <div class="flex items-center">
+              <Icon icon="mdi:google" class="h-6 w-6 text-red-500 mr-2" />
+              <h3 class="text-lg leading-6 font-medium text-gray-900">Google Account Linked</h3>
+            </div>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">Your account is linked to a Google account.</p>
+          </div>
+          
+          <div class="border-t border-gray-200">
+            <dl>
+              <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                <dt class="text-sm font-medium text-gray-500">Google ID</dt>
+                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userInfo.google }}</dd>
+              </div>
+              
+              <div v-if="userInfo.properties && Object.keys(userInfo.properties).some(key => key.startsWith('oauth_Google'))" class="bg-white px-4 py-5 sm:px-6">
+                <details class="w-full">
+                  <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none">
+                    View Google Account Details
+                  </summary>
+                  <div class="mt-4 border border-gray-200 rounded-md overflow-hidden">
+                    <dl>
+                      <div v-for="(value, key, index) in googleProperties" :key="key" 
+                           :class="{'bg-gray-50': index % 2 === 0, 'bg-white': index % 2 === 1}"
+                           class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                        <dt class="text-sm font-medium text-gray-500">{{ formatPropertyKey(key) }}</dt>
+                        <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
+                          <div v-if="key.includes('avatarUrl')">
+                            <img :src="value" alt="Google Avatar" class="h-10 w-10 rounded-full" />
+                          </div>
+                          <div v-else>{{ value }}</div>
+                        </dd>
+                      </div>
+                    </dl>
+                  </div>
+                </details>
+              </div>
+            </dl>
+          </div>
+        </div>
+        
+        <!-- Properties Card -->
+        <div v-if="userInfo.properties && Object.keys(userInfo.properties).length > 0" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+          <div class="px-4 py-5 sm:px-6">
+            <h3 class="text-lg leading-6 font-medium text-gray-900">Other Properties</h3>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">Additional user properties.</p>
+          </div>
+          
+          <div class="border-t border-gray-200">
+            <details class="w-full">
+              <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none px-4 py-3">
+                View All Properties
+              </summary>
+              <div class="border-t border-gray-200">
+                <dl>
+                  <div v-for="(value, key, index) in otherProperties" :key="key" 
+                       :class="{'bg-gray-50': index % 2 === 0, 'bg-white': index % 2 === 1}"
+                       class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
+                    <dt class="text-sm font-medium text-gray-500">{{ key }}</dt>
+                    <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ value }}</dd>
+                  </div>
+                </dl>
+              </div>
+            </details>
+          </div>
+        </div>
+
+        <div v-if="!loading && !error && import.meta.env.DEV" class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
+          <div class="px-4 py-5 sm:px-6">
+            <h3 class="text-lg leading-6 font-medium text-gray-900">Debug Information</h3>
+            <p class="mt-1 max-w-2xl text-sm text-gray-500">Raw user data</p>
+          </div>
+        
+          <div class="border-t border-gray-200">
+            <!-- open -->
+            <details class="w-full">
+              <summary class="cursor-pointer text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none px-4 py-3">
+                View Raw Data
+              </summary>
+              <div class="border-t border-gray-200 p-4">
+                <pre class="text-xs overflow-auto bg-gray-100 p-2 rounded">{{ JSON.stringify(userInfo, null, 2) }}</pre>
+              </div>
+            </details>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, onMounted, computed } from 'vue';
+import { useRouter } from 'vue-router';
+import { casdoorService, UserInfo } from '@/services/auth';
+import { Icon } from '@iconify/vue';
+
+export default defineComponent({
+  name: 'SucceedView',
+  components: {
+    Icon
+  },
+  setup() {
+    const router = useRouter();
+    const loading = ref(true);
+    const error = ref('');
+    const userInfo = ref<UserInfo>({
+      id: '',
+      name: '',
+      email: '',
+      owner: ''
+    });
+    const dropdownVisible = ref(false);
+
+    // Computed property to get Google-related properties
+    const googleProperties = computed(() => {
+      if (!userInfo.value.properties) return {};
+      
+      return Object.entries(userInfo.value.properties)
+        .filter(([key]) => key.startsWith('oauth_Google'))
+        .reduce((acc, [key, value]) => {
+          acc[key] = value;
+          return acc;
+        }, {} as Record<string, any>);
+    });
+    
+    // Computed property to get non-Google properties
+    const otherProperties = computed(() => {
+      if (!userInfo.value.properties) return {};
+      
+      return Object.entries(userInfo.value.properties)
+        .filter(([key]) => !key.startsWith('oauth_Google'))
+        .reduce((acc, [key, value]) => {
+          acc[key] = value;
+          return acc;
+        }, {} as Record<string, any>);
+    });
+
+    // Format property keys for display
+    const formatPropertyKey = (key: string) => {
+      if (key.startsWith('oauth_Google_')) {
+        return key.replace('oauth_Google_', '');
+      }
+      return key;
+    };
+    
+    // Format date for display
+    const formatDate = (dateString?: string) => {
+      if (!dateString) return 'Not set';
+      
+      try {
+        const date = new Date(dateString);
+        return new Intl.DateTimeFormat('en-US', {
+          year: 'numeric',
+          month: 'long',
+          day: 'numeric',
+          hour: '2-digit',
+          minute: '2-digit',
+          second: '2-digit',
+          hour12: false
+        }).format(date);
+      } catch (e) {
+        return dateString;
+      }
+    };
+
+    const fetchUserInfo = async () => {
+      loading.value = true;
+      error.value = '';
+      
+      try {
+        // Get user info
+        const userData = await casdoorService.getUserInfo();
+        // console.log('Raw user data from service:', userData);
+        
+        // Make sure we're getting all properties
+        userInfo.value = { ...userData };
+        
+        // console.log('User info after assignment:', userInfo.value);
+        loading.value = false;
+      } catch (err: any) {
+        console.error('Error loading user info:', err);
+        error.value = err.message || 'Failed to load user information';
+        loading.value = false;
+      }
+    };
+
+    const retryFetchUserInfo = () => {
+      fetchUserInfo();
+    };
+
+    const logout = () => {
+      casdoorService.logout();
+      router.push({ name: 'login' }).catch(err => {
+        console.error('Failed to navigate to login:', err);
+      });
+    };
+
+    const logoutCasdoor = async () => {
+      try {
+        await casdoorService.revokeToken();
+        router.push({ name: 'login' }).catch(err => {
+          console.error('Failed to navigate to login:', err);
+        });
+      } catch (err) {
+        console.error('Failed to revoke token:', err);
+      }
+    };
path: src/views/auth/SucceedView.vue
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add user-facing error message for token revocation failures.**

The `logoutCasdoor` method catches errors but only logs them to the console without showing any user-facing error message. Consider adding a user notification for better user experience.

```diff
    const logoutCasdoor = async () => {
+      const originalError = error.value;
      try {
        await casdoorService.revokeToken();
        router.push({ name: 'login' }).catch(err => {
-          console.error('Failed to navigate to login:', err);
+          if (import.meta.env.DEV) {
+            console.error('Failed to navigate to login:', err);
+          }
        });
      } catch (err) {
-        console.error('Failed to revoke token:', err);
+        if (import.meta.env.DEV) {
+          console.error('Failed to revoke token:', err);
+        }
+        error.value = 'Failed to revoke login session. Please try again.';
+        // Reset error after 5 seconds
+        setTimeout(() => {
+          error.value = originalError;
+        }, 5000);
      }
    };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    const logoutCasdoor = async () => {
      const originalError = error.value;
      try {
        await casdoorService.revokeToken();
        router.push({ name: 'login' }).catch(err => {
          if (import.meta.env.DEV) {
            console.error('Failed to navigate to login:', err);
          }
        });
      } catch (err) {
        if (import.meta.env.DEV) {
          console.error('Failed to revoke token:', err);
        }
        error.value = 'Failed to revoke login session. Please try again.';
        // Reset error after 5 seconds
        setTimeout(() => {
          error.value = originalError;
        }, 5000);
      }
    };
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-27T02:13:46Z
author_association: NONE

PR comments:
user: login: bolt-new-by-stackblitz[bot]
created_at: 2025-03-27T01:44:49Z
body: <a href="https://stackblitz.com/~/github.com/1834423612/Team-695-OfficialWebsite/pull/53"><img src="https://developer.stackblitz.com/img/review_pr_small.svg" alt="Review PR in StackBlitz Codeflow" align="left" width="103" height="20"></a> _Run & review this pull request in [StackBlitz Codeflow](https://stackblitz.com/~/github.com/1834423612/Team-695-OfficialWebsite/pull/53)._

author_association: NONE
user: login: vercel[bot]
created_at: 2025-03-27T01:44:50Z
body: [vc]: #WxdKKFNRq+WIypTdvgwADIzyFQkPsm9M+9mF48f9zn4=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0ZWFtLTY5NS1vZmZpY2lhbC13ZWJzaXRlIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InRlYW0tNjk1LW9mZmljaWFsLXdlYnNpdGUtZ2l0LWRldi1mZWF0dXJlcy1ramNocy1wcm9qZWN0cy52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9ramNocy1wcm9qZWN0cy90ZWFtLTY5NS1vZmZpY2lhbC13ZWJzaXRlL0Q1eTJFTVZpeHJ6ZllCbkdyTWFweWlSdVhnNnAiLCJwcmV2aWV3VXJsIjoidGVhbS02OTUtb2ZmaWNpYWwtd2Vic2l0ZS1naXQtZGV2LWZlYXR1cmVzLWtqY2hzLXByb2plY3RzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJyb290RGlyZWN0b3J5IjpudWxsfV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **team-695-official-website** | ✅ Ready ([Inspect](https://vercel.com/kjchs-projects/team-695-official-website/D5y2EMVixrzfYBnGrMapyiRuXg6p)) | [Visit Preview](https://team-695-official-website-git-dev-features-kjchs-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/team-695-official-website-git-dev-features-kjchs-projects.vercel.app?via=pr-comment-feedback-link) | Mar 27, 2025 2:23am |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-03-27T01:44:55Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: rate limited by coderabbit.ai -->

> [!WARNING]
> ## Rate limit exceeded
> 
> @1834423612 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait **6 minutes and 26 seconds** before requesting another review.
> 
> <details>
> <summary>⌛ How to resolve this issue?</summary>
> 
> After the wait time has elapsed, a review can be triggered using the `@coderabbitai review` command as a PR comment. Alternatively, push new commits to this PR.
> 
> We recommend that you space out your commits to avoid hitting the rate limit.
> 
> </details>
> 
> 
> <details>
> <summary>🚦 How do rate limits work?</summary>
> 
> CodeRabbit enforces hourly rate limits for each developer per organization.
> 
> Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.
> 
> Please see our [FAQ](https://docs.coderabbit.ai/faq) for further information.
> 
> </details>
> 
> <details>
> <summary>📥 Commits</summary>
> 
> Reviewing files that changed from the base of the PR and between 323fcc414ebce28f1a866fb96af322bb5c961354 and fd8efe60bd179f7cc160905dd73fb218eeab564a.
> 
> </details>
> 
> <details>
> <summary>📒 Files selected for processing (3)</summary>
> 
> * `.env.example` (1 hunks)
> * `src/views/auth/CasdoorLogin.vue` (1 hunks)
> * `src/views/auth/SucceedView.vue` (1 hunks)
> 
> </details>

<!-- end of auto-generated comment: rate limited by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
The pull request updates the project's dependencies and authentication flow. In `package.json`, it adds the `casdoor-js-sdk` dependency and removes `vuedraggable`. The router configuration now includes new routes for login, callback, and login-success, with added authentication guards on existing routes. A new authentication service based on the Casdoor SDK is introduced in `src/services/auth.ts`, and three Vue components have been added to handle login, callback processing, and post-login success messaging.

## Changes

| File(s)                                           | Change Summary                                                                                                                                                               |
|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `package.json`                                    | Added `"casdoor-js-sdk": "^0.16.0"`; removed `"vuedraggable": "^2.24.3"` from dependencies.                                                                                |
| `src/router/index.ts`                             | Added new routes (`/login`, `/callback`, `/login-success`) and updated existing routes (`/pit-scouting`, `/pit-scouting/dashboard`) with meta authentication requirements; implemented a global navigation guard validating login status through the Casdoor service. |
| `src/services/auth.ts`                            | Introduced a new authentication service (`CasdoorService`) including configuration, methods for sign-in URL generation, OAuth callback handling, token management, and user status checking. |
| `src/views/auth/CasdoorCallback.vue`, <br>`src/views/auth/CasdoorLogin.vue`, <br>`src/views/auth/SucceedView.vue` | Added new Vue components for authentication: `CallbackView` for processing OAuth callbacks, `LoginView` for initiating user login via Casdoor, and `SucceedView` for displaying a successful login state with user information. |
| `components.d.ts`                                 | Added global component declarations for Material Design icons: `IMdiAlertCircle`, `IMdiGoogle`, `IMdiLoading`, `IMdiLogin`, and `IMdiLogout`.                              |
| `.env.example`                                    | Created a new file as a template for environment variables, including placeholders for Casdoor-related configurations.                                                        |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant U as User
    participant LV as LoginView
    participant CS as CasdoorService
    participant CV as CallbackView
    participant R as Router

    U->>LV: Click "Login" button
    LV->>CS: Request sign-in URL
    CS-->>LV: Return sign-in URL
    Note right of LV: Opens new window for Casdoor login
    U->>CV: Redirected back with code & state
    CV->>CS: Process OAuth callback (handleCallback)
    CS-->>CV: Return authentication token
    CV->>R: Redirect to success page
    R->>CS: Global auth check via guard
    CS-->>R: Confirm authentication
    R->>U: Render SucceedView
```

## Poem
> I'm a little rabbit, hopping through code all day,  
> New routes and services light my joyful way.  
> I nibble on dependencies, adding and pruning with flair,  
> With login paths and callbacks, success fills the air.  
> In code meadows I dance, where auth flows like a stream,  
> Celebrating these changes with a bright, digital gleam!  
> 🐇🌟

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpADK2VmgqyjcEj2wAbb5AokAI7Y0riQ4rjedJAARIiyGAxcShJgAGYk1NgByPrMaIg0FDGQAO4FkIjYAszqNPRy4bCe2IiUkACMABwAzAAsfQBMPQBsHYPoGPSoGE4ClNEArD0akACSYQyYkPPoDAzSbfTUTZ5p+L74pfAYRJAAqjYAMlwA2rC4uNyIHAD0v0R1LBqhomMxft1+kNRuNftBMswwCMAJyLMAAeTSaXgDHgaG8AHUSAJEOoSL9uD5vL9lgBdAAUHy+P3+gNwwIEoJYEN6A2GY0GcIRSNRGKxOLxhOJpJoFKpNJ6AEoNO5mpAlIgGBR4NxxPgsPg0qcvL5/EEQoUUFMcdRpE0TuorbgKIpsAdkGhIORSpAMlkAgAaSrcUTwbFbXzydmeNC0WjqeD6/hGz3efCArDcNCkSbHPDNDDiLZ6rBpbCJEvIc58aOQADCBVo+HwfDaFCkFBVBn0xnAUDI9ENOAIxDIynqClY7C4vH4wlE4ikMnkTCUVFU6i0Oh7JigcFQqG2aDwhFI5CoE7BbELXCoPqqzlcOxXimUG802l0YEMvdMBizDAANbZiQGhCIg+ocAYMQwQYFiQAAgmso7nra9APvkT5DgwsCYKQiCqp4OF4XaNzGgABgBwGkGBEEYORvrwFEVoMN42BKMasbxiWyboF6JA+koIZTGQDDyJg9C1gEzD4BI+K8dsJAAB5IOItz8OQqz2CGuLhvi3iyEGtbeDcnjkQA1JAVmxFsiBNi2YDgWAdmATEXAxAAegADBoHQjBoXkxAGDG4TIJBkOgcbRAQFFCQOonwNIDFtGIiYYEGNzOq6uLqbW5G2fZFCOYgzm0IBDFUSBZRApAHakkm5Heb5/leeRqwIXGCb6vphnGiZ5CQORUDWTEEghLQVBEEQKhRG5sQeYMGiDH0Gg9DEIUVPMEXSbJ0RpC6zCxSQwlKIkiWIMlC5pRl1rFjcdzsiceVjXQk3TQIUQVWgQFVTM+CQGmtztDcrHsdEZG1rw+AiGIqweGa+Q3Pd6rHfFZ3SDdoPxup5EvWALp4JQ5FBrhUg7OFWABLg2jkPQ1zsvgeCQDJ8Z6SWXbmJYCHeEU1BpcgMW1korEuHz+rIEOyncC2E4tl4H04pA7AJtIRhQAAIqjImJOJUW0Fw5ExAVzZFU5LlzZ5Pl+QF61WoNcXa7iSW8ZR33UaB4H6uR3aQJrJ2ifIO1SPrg2jeNb0zSQFsLUtK1rQxZHkQ7p1OxdLuVTRnv0dBsHdn+iAUAwvwE0Uvw3EoSkaLgPw5zEcFc8hZ7jtEGEuPI2G4UDBEGPDxFd06Lq0G6njev4jM0B6Ux8UQaYCPJGBoBI8DTTxRDYC4kn/bWABqISQDY4/tEwGDYmvF5pVpOlhjakZGaU/0l3a1bDgWRZi1gLgxnrXCpumZGP5Aek5FfhpgzORRUuY+IRm8HPICY9CaAOAdA2B5VlSIU6iWHqQZPRVH2IceBNBEEgL/hgZybp3QXQgQeQsg9h60CDKUBAOEUDIChjQMQ0RGhHnzMrO6SYAjBHgNJdgcM1TKVUsjR+VY5bAO4OoZyTA8D3QYhJQaFJ5GanHvdX4tACiwAEPgDeKiAjM0UNfaK/0QZsQ4p6cibBqYVRdCGCguB5DlxtGpB6uEwjHmjIWDxaUWFmkEQETeewKGnDaDwagsBEBdgQpAGegh5IALXhvIJ8BmDcCiNeCcrRkbkUfp2eY1YSAAFFvqwGJmUJhsBGDNCAgLNUrRKAAHJkCgLIoULIyAl6ejysbFs9hKBLwOG1GAao0kUHoKEoRC5kAtIoDIAo0QkzRiES/Xh79KjUxoFwMMfFQGkFoGAMiiz0CfBIFk6u4R/rfQicZEh0TSBGWaOJExsyAgcLCbWXBESsykAANwoBTF6fUYAjl0FOVgc5zpzq3PCfg355D8EApIK8kg7zPCfIXBYvqTy0WrAAGLZGjBQGSgYQV8QAQI7AcyPQ8P8Xwj+U9aznL+mESF0x0qnCxWaeMXyJyCzVJ0zMIFVhrCNBgfUnghzRiicfbi/N0AmPsRir0i9l7bKhgcOgHpkDSvJfiLs6IOz6XVX3fCSsMCdwOMaEuyNOkMGfE6EgRBz7qV8a/AJSYcKiEApPeg+QF4ZjuOc+5SL/qICvuGAh0gOb10QjzccyrhWeGFt4UWlZeJSxlqsvglIFbOuVuIVWPsAByAk42RSUCHciABvaJ7IuCtOIRmVpQYF5sBbaKjtk5pbkBvIAiBABePQKAsky3pK0gAAr8JeAlEC/C9b8BsdkTZPBIRoF6rSIEAF8E5YHIgXIuRSy4iUrtXb2UBK0+gAVxOgBtG1ZmbZAVtyC3Z9rBAO6cw7IBjondLFx0650LtKEulda7CoNl8Cg7dIRd2QAPXbY9hdi6HwoOeiuVcLoVqrfe7+g1n0xJbW2m4ZC8GIEQH2rtJAe0kIoxQr9LAf1DvpKO8dmSgO4BA/OxK4Hl35l+I4PBdBt78fgyQXdQZ7FoC4I22l9KEL5i4M6Pee6kOHsGie9DhNMPl2Ujh69+9D6QGwNwXR9Qn1NtgKRuRuAFFaNuMxydg7OB/oA1xqds6+OLt+FYDRijPGScQ8hxOOmz0GcvWnQASYREZs3ZwLTmiAudY+59j/7OOTuAz5sDS6AsOc0Uo24IXFQyZINTeTwS6U5GU6+tTngNNhaPRFjDWHDNXp9gfBB5nLOPviy+2zb71GFaC9o3RiB9GGOmalmVbGOOAe86B/j+WkvFaIOrPRBiN6lc0yh1ren2vRYYnFhtCXhv2cc+tnRW3pu0Fm25rgGXPPZZ47llb/m1ueM25N7b0zSvlcq5ABT5olMqfCBQdTe3wtocixeoztc1b5zQ22UZ0hBPshw1BGCdd4JIRQs3dCThMLtyNJastCTR7YmYqhouqOcTo69UZyAoVyYRUyrQg49DAM5OVp6waUGTbDPbAzhiIsqO+jlp3Wg/U7heq2TxPpxpBdy3sOrAA0qIs4TFPBWLBh6BQJ9l7ZG2YIGGYR8qNhNnWfUp8GKPTCNG0M2I7So/aI8J4QZWKJULOsdWXuTLsEqKIKm2DuDZJ9VgWjQYWzTQwPAAAXts6PkDBvSL4Di1KSZVGkiIKQm4WvBr3DbGsE++BD1FDSN9dNJBsTkCaZ4QokOxDZDOHLdlZejUlkxtY5G+BdRpXki77wtB0/qiQNktA8gU/cFgDK7BU9F7UBcF2eG5EVcUGF2jsXmaJfH2pjcZA9i5+j8l3wJuHq5eVGXvnrAHvEGkFwPYG/Nx7gUG8OAkmElZeQHRHVupH6jSiC0uUQMGMCbsn+ZocKJAS86kHe1Y+QPEQCj+xelApe5wkBfqQEjqTy3SuArQiCSAm6U0dApekBRyfeTMQCoC48kBAQ+00gCAnqlGAs+AgEZAyAQC9BOQsA0AbBZAkBWYSyyMAAUgSNALcuwRgJwa7EsiQHwVIZAaogEBIGwcjAQFITISofwQoQIWgmviASQGASggxMfooCxNYnaJQC6HwCAcjM/HgZ4B2NfMygirwDAUHnWPYDYEShctTEAaosGiBKwVITsi2L9FgGmBGGEZNKBBMmZCgSXmXqYRVifn4VcrqKwUrEpM6N9GEOcpZp6PtCwMaGIRIRoRFKolXr4MsnAjFAhFYGsIwPpMgAcuQBQm3IXuLq0cgLmi4tEBUDgvdFEAQFgIftTIkKBIRIxMxE2HaNKpsPqAfiyvICzC4e/FkeIoUMjKuJ4AIEzBztlHaJ6N+gEAWKSGTPLkytsvTnavTHUk3m6PgaEkrBQDYSzt/sjKovvi6H4GkGmD6M/EEaGmZm2MHlRvzF2HjsmufOLAikLKIJmrCdITmkpNxvmvLCZMWv4qWt3FADbkbmfKbvOGIDWv1uRH0YsdIZsFbi2ASXbvtijiMgzhBvmAjlAKXpXtXmSXWpSa6hQFXnaqgRQOgeXoyXTsye6BjrAOyfWLvh6IRvlPKfWLSZvpKSQFprTr8LcYzmyZ1lAM/rcCMUmOMZgHapSeSXycfJaIMmqSLmMuKdqeqayZjp1jjkjmAEYDpnltKauqqcYW7JJtjrBHjo3GOBeC3MTm3LxOTt3JTlWrvERCxnNmELRvQOvvpCguJgJBtGFOzjQkcdysaNTmZN6R9pBv6ZmYGS9OMr3MmW5r6JkM8cceEBkZmoQg7uPtGpmrIAbmmLGMjO4sWHLIwjrjwC6BQl8QDE8qoopG8XLGwFRr9CmFgNYXLPgPsNkMgEPNqLlGqIAYBB8VMLLhKvkeICZAnnaIme0vWMmTKIEg0WsBlK9sjO0YcNGWWBWMqsUYdLjCENUn+SQPjBhtUrOcHvaW3rYZ8Z6oym/DxAeXEkYKXhRG0PgdwAxJ+VnjyrgPfGaHkZINirXgsJMYgEGORP2djEQColPBSfORQKBSYkjOIPiInnQIXuREQPgHwcQTcBheWFhUEkoHXnigvEvCvJ4GymCSgvCSKgSuKvEYNPqAALKMyFh0AMQmQZBiSsSeBz5sFBJtn4CyB4qGHGgHnTmAjOoFAJA4QujSqtAGSnlQHageEN6DS7HUXpmOFfRUD2KUBVgHT2qmbBCUCyASpGgkBAjtBCFoB+V8CoC1AQm3AL6vHvGoDsguilAYBdj3ADqVAopUZlh+BUwuVyR+BypqgxVxWkUUS2lb4M4aCGEBlAQpEMzTDIDQJ4o6pIqyUZjhX5UsFFXqqio7I9JBKFAtjgxHpRH4j2AECxGgWL7Mzvk5hDl8zqR/L4KoBtC+5prRIBC+7XBTBXBdHZAHVhBHVNg+jpUFiMBphHASpYC2SyormpUtjqprnxUdLpjHIL7HC+jaBsSqorW67IA7VhB7VCFB6XVXDkylJ3X4Ckh7lERnXQ3lwnXTHfopnoDeAQQWH65vVQXHnqH/TAkxgYBuJUYWj2g+LahRI7nqH7lVlwLdVJVEBBgcHZCM0nBoDh5OLai2jLVLlWqfyTjMDlgeKcJuwyWeBQ2HXo2lBQlcwwkbEy0owizIkSwRVol5qDgFrVBYnWoRDnRqyQC3r9rY0Pp1rNWATZmlCalll+YVnrp0nM2ASSbezul5yekGCO0CbO2FQ8XZUvTBm44NwE4RlE6Pik71IkRxn8Q+iJkW0NlpmDRB1225ls5jEFlDxc52y1glnaZoY+kB0bpbo1ldH1lB5QxLwah8Qd5cl2oOEv7qRnJI13Ab5Bh+jPFTkHBqUvE9njzVTsh8QRDMRgX7GfBJjPyiqF6T2jFBKKpdQLyRhmgiQvFzxHAaQUREE/WkH0SjU0CrC5W+pYmASM17F4CjHYIJ3VTHU+j94cHK6qkWVkRop/V8SLmIBVQmSFDhnjUVZmbcAIqmU5ADptABUlGPIZh31XX9U4IFWIBFWv1jHbkVa4rc6SXAzICZ5Cp3IDX/IgTAoHJznvEbkMBbkZRhCoBcpq3WkXDoqQKeiLLpAuXHniRRAuKCUT49lsUKVY0NlYEBrGiLI3kjV4EEFJgyTlg+JTy/00AonPyxkoYzXeBzXhGkD27/S9aC3QNdK7IEH3ItiUWOXrBGhYNfU40BCxjyC0M3AYp8q4N4rIosHPK65Tjxi2iOWcxJq8zZp7UZpZrKqSw639F62YmKwlom14Y+gCNB5W0Gzp38YO3F3llCYb5B0e2I7e1empNO1CYia6q0B21BmI6hkR1oT5XR0xmdz4TTFU5jnkSFPhTFMSY1k85XLsAG5J1xO+4ag35dU5DxOgnAz92CmQV8SbWFU+AoOVB90uCJiV2uZB7d2t59mGKUWH2MOfVHky73Qf3xjdlT4LJglQyF03AIHvzH15VTNIMzNXFwXXQoCO5z7gaTOINC3f0vI1Jjm/wDkbVyIYDnjcNHPGX0AM3qSFHOWJRlWPXNFRJDikPvWTCE2fO/Rg2vM8r4j6h3D3H8AD5wkxQlXyAZC4A4TIzwEtiIGBJywxSgL8B4Cr77lV2+6GW9mQDXkdV3nL2ISNFBhCVIzqTWOpRkyOG1QLORxj4UXIwSM1U7Nf3BEf2Utd4XzrDUOJCWFctZKEz0BQzOK4kIrU5FCQKXP5GnNOKUC4lBhO66Q3wGRdmeJ0qTbIwADizYM8QFAQ7Z0Qerlr8KP5/AZKcSCl5Eprm2NAfFX509VL1A25gtTe90Y+hzk+vUeLOzdhuUD8FW2QH8KMVePgYQCrq1RoQh7dANTEiFPcaoobFWOEwpopkbAl1liQsAdljMiA9rJVMLdoBRy+vogVOC6pQYOjnixoYrRj0ywx4kU8wJyMn1y4wenwXxq5dFaLNE/DLLPiuN/0Nd8AddZhp+z8xLF9Iz8Vne1L09tbsAQYFB6k48H9BAU0P+noE0/eV1pYcsXEy98kNBTM/elY7Fylql9QGlYYogsgOlLOzYh5QjrR5jzSYJNDe9RZVRV7PbiH572y5mvqm7piMjJqZqvgFquH21amLeLxMUu7Ni/aZxHBBFtU/GvEyrF7mYiNDmoqDCNUvNerAthC0rnqU8Ozjhlb0JfjqaW8aogTmtqJ6J4ThahtUTZa+JuHCTg0zTYmyTjpJdBTKKGnAkWTXtEAPtvT1cGgtAWOZT4dF+lTrcWEZOtTZa8MrMRohxudT89Ho8SSc8fgJn6tSJqt9xEM1bLrs8+INuyzhYacmUlA4zvEO8e8LMPgcR8MVtydXTKqZkawSl8Y3MlrdYQiOlAFWX8Ybr6Yn0ZFxX8Am6/zVFFX2XVXJCi16ZlXxBtBHNlSi2LiF9GyYDV0ZMQlx4PM2R3GkDh0eUAAfjiOLL8MwPGAnMfADPAAIFQK4KIqgOTsNxJG5UvkxJHIkqFz55u60VgPFzGOHlie/NgpcEdXcM/Fg3F80FEkpbaALX4JrLnmMdaU0ATEQHPkzLWNxxd+zEYD49zGJ4SxJzXhrarSE7J/wPrUWkbSrHiWbVWr56p+RJV7ly4vl4XFEKprICGEOF5jlpN9aTN/GMupww5riHj1JoqK8K0gNwW60rSFpolzTjWTE2l77hj5V6Vx6wT0Ty569tOmT9N7N/AACO61ELuoz8zzzKz+z66Jz/+dz+j4qS1xs/dELyQMT6L60uL9IRT1L/xylgz0z7XoN7gEr3bBz2ZFzzemjyp5r/V0Hbr/r9xmL1N8b5L2RhgHL5b/m4r2z3byrw72r077Ey77WgbFr5xXgB7yL174bz70un77+zbxbwrzb6H2RPb4NFz4Z3+BoGQBIKX0pLFdknRpZ4hGGahBOLZzHbGUYPGYCWOanW1GXxX1X59B8XmdnVlG59zqOcwm7vqqPW2YLc/GX0IvqLkuKwLR9HaIpi8Uo7bsbsiYXucxqwTS+9IFqDqDxGtaO+oKwtkNLFEqouMc3tmuv9wLIIORDfg13xgBIBhWOc/M4nRPJMfKfCbuzDVbBhnc8KNoIRxVIu0ioXrSprPzsoL85IS/KIDVT1ybNyI28NYNADKQAB9OsAhDVzoh0QNgLAfYDKQ2Bt4pArAR7gAroDMBOAvAerAIFEC6wTwNYGUnLTQAsBawdWNQIwHYDcB+AwgTgJYFsCOBJAusDYDKTQAeBtA/gQwMEENErAWA8tAhCUplImug0GgXwPoGMCsBhAl1ghHLRrAAAWghGgBrB0Q5aJQSoLUHtdmEeLSfAcDnwj52gZVams/HOQUMJqtQJPMDxB6icU0EPY0FJxh7a04ecseTpExxLRMo+MxIiNYysyDRS+b/Hvlkj7774aYc7N/nPwwDwCJWy/PzkE3FhQQrIUANAbwLoECCiBJAsgRQKoEGAShGg8obIJ0HMDWB7AzgdwPqG6BGhMg7QYINaEiDiBZScQZIO9gNCyhvQyoVgIUFWDVBYw7oRMK0FTC9BBg4waYPMGWDlBcw7Jj2AMC7hrUg4FMCeAqaXgWAuSW8GgHvBRknwjQXYuuDUAfhtw34PYX2DFrqAsBe7RAFgJUL8Y6AWA7pFw12H7CvIaAPyIMAYCLA/IaQZEAAHZFgtAAQLQGWiDBBgAgFEB0D6BeQvIDANIF0FoAwjeg/QQYN0A6BfhDA+w2gCMBGCDBkQaAZECMC6Awi0AIwAQD0C8iDAreXQLEF0BIAwiegXQUETCMyDDBURJAZaM6h3CvDVAYotICyMWDzAOgaQJQPKJZGgjwRaAJkTiP5GwiRgaARYF5BIBeRkQcYMkS8IgCQARgSIvoLQExFQiq8JAekQwBGBeRFguIrEB0AEBdAOgGor0R0EWCLAGA2IxYPSNNH7CegwwNIPsD6AYjiQBwQYFyO9FdAqRaQAQPSLQBpBwxqIgQAGPpEdAegiwPoKaLNFQArw7wz4d8JgK/DaAWAgcGSKAA== -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=1834423612/Team-695-OfficialWebsite&utm_content=53):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE