feat(auth): integrate Firebase authentication and update auth flow
- Added Firebase authentication methods for login, signup, and password reset. - Replaced mock user database with Firebase user management. - Updated auth store to handle Firebase user state and authentication. - Implemented middleware for Firebase authentication in RPC routes. - Enhanced error handling and user feedback with toast notifications. - Added Toast component for user notifications in the UI. - Updated API client to include authorization headers for authenticated requests. - Removed unused CSRF token logic and related code.
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -32,6 +32,7 @@ declare module 'vue' {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
||||
Toast: typeof import('primevue/toast')['default']
|
||||
Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||
UploadFilled: typeof import('./src/components/icons/UploadFilled.vue')['default']
|
||||
Video: typeof import('./src/components/icons/Video.vue')['default']
|
||||
@@ -62,6 +63,7 @@ declare global {
|
||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||
const RouterView: typeof import('vue-router')['RouterView']
|
||||
const TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
||||
const Toast: typeof import('primevue/toast')['default']
|
||||
const Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||
const UploadFilled: typeof import('./src/components/icons/UploadFilled.vue')['default']
|
||||
const Video: typeof import('./src/components/icons/Video.vue')['default']
|
||||
|
||||
@@ -17,15 +17,17 @@
|
||||
"@primeuix/themes": "^2.0.2",
|
||||
"@primevue/forms": "^4.5.4",
|
||||
"@unhead/vue": "^2.1.1",
|
||||
"is-mobile": "^5.0.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"firebase": "^12.8.0",
|
||||
"firebase-admin": "^13.6.0",
|
||||
"hono": "^4.11.3",
|
||||
"is-mobile": "^5.0.0",
|
||||
"pinia": "^3.0.4",
|
||||
"primevue": "^4.5.4",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^4.6.4",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"zod": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -6,6 +6,7 @@ const GET_PAYLOAD_PARAM = "payload";
|
||||
export function httpClientAdapter(opts: {
|
||||
url: string;
|
||||
pathsForGET?: string[];
|
||||
headers?: () => Promise<Record<string, string>> | Record<string, string>;
|
||||
}): TinyRpcClientAdapter {
|
||||
return {
|
||||
send: async (data) => {
|
||||
@@ -14,12 +15,18 @@ export function httpClientAdapter(opts: {
|
||||
const method = opts.pathsForGET?.includes(data.path)
|
||||
? "GET"
|
||||
: "POST";
|
||||
|
||||
const extraHeaders = opts.headers ? await opts.headers() : {};
|
||||
|
||||
let req: Request;
|
||||
if (method === "GET") {
|
||||
req = new Request(
|
||||
url +
|
||||
"?" +
|
||||
new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload })
|
||||
new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload }),
|
||||
{
|
||||
headers: extraHeaders
|
||||
}
|
||||
);
|
||||
} else {
|
||||
req = new Request(url, {
|
||||
@@ -27,6 +34,7 @@ export function httpClientAdapter(opts: {
|
||||
body: payload,
|
||||
headers: {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
...extraHeaders
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ const GET_PAYLOAD_PARAM = "payload";
|
||||
export function httpClientAdapter(opts: {
|
||||
url: string;
|
||||
pathsForGET?: string[];
|
||||
headers?: () => Promise<Record<string, string>> | Record<string, string>;
|
||||
}): TinyRpcClientAdapter {
|
||||
return {
|
||||
send: async (data) => {
|
||||
|
||||
@@ -1,242 +1,22 @@
|
||||
import { getContext } from "hono/context-storage";
|
||||
import { setCookie, deleteCookie, getCookie } from 'hono/cookie';
|
||||
import { HonoVarTypes } from "types";
|
||||
import { sign, verify } from "hono/jwt";
|
||||
|
||||
interface RegisterModel {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Mock user database (in-memory)
|
||||
const mockUsers: Map<string, { password: string; user: User }> = new Map([
|
||||
['admin', {
|
||||
password: 'admin123',
|
||||
user: {
|
||||
id: '1',
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User'
|
||||
}
|
||||
}],
|
||||
['user@example.com', {
|
||||
password: 'password',
|
||||
user: {
|
||||
id: '2',
|
||||
username: 'user',
|
||||
email: 'user@example.com',
|
||||
name: 'Test User'
|
||||
}
|
||||
}]
|
||||
]);
|
||||
|
||||
// CSRF token storage (in-memory, in production use Redis or similar)
|
||||
const csrfTokens = new Map<string, { token: string; expires: number }>();
|
||||
|
||||
// Secret for JWT signing
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
||||
|
||||
function generateCSRFToken(): string {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
function validateCSRFToken(sessionId: string, token: string): boolean {
|
||||
const stored = csrfTokens.get(sessionId);
|
||||
if (!stored) return false;
|
||||
if (stored.expires < Date.now()) {
|
||||
csrfTokens.delete(sessionId);
|
||||
return false;
|
||||
}
|
||||
return stored.token === token;
|
||||
}
|
||||
|
||||
const register = async (registerModel: RegisterModel) => {
|
||||
// Check if user already exists
|
||||
if (mockUsers.has(registerModel.username) || mockUsers.has(registerModel.email)) {
|
||||
throw new Error('User already exists');
|
||||
}
|
||||
|
||||
const newUser: User = {
|
||||
id: crypto.randomUUID(),
|
||||
username: registerModel.username,
|
||||
email: registerModel.email,
|
||||
name: registerModel.username
|
||||
};
|
||||
|
||||
mockUsers.set(registerModel.username, {
|
||||
password: registerModel.password,
|
||||
user: newUser
|
||||
});
|
||||
|
||||
mockUsers.set(registerModel.email, {
|
||||
password: registerModel.password,
|
||||
user: newUser
|
||||
});
|
||||
|
||||
const context = getContext<HonoVarTypes>();
|
||||
const sessionId = crypto.randomUUID();
|
||||
const csrfToken = generateCSRFToken();
|
||||
|
||||
// Store CSRF token (expires in 1 hour)
|
||||
csrfTokens.set(sessionId, {
|
||||
token: csrfToken,
|
||||
expires: Date.now() + 60 * 60 * 1000
|
||||
});
|
||||
|
||||
// Create JWT token with user info
|
||||
const token = await sign({
|
||||
sub: newUser.id,
|
||||
username: newUser.username,
|
||||
email: newUser.email,
|
||||
sessionId,
|
||||
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 // 24 hours
|
||||
}, JWT_SECRET);
|
||||
|
||||
// Set HTTP-only cookie
|
||||
setCookie(context, 'auth_token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'Lax',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 // 24 hours
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: newUser,
|
||||
csrfToken // Return CSRF token to client for subsequent requests
|
||||
};
|
||||
};
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
// Try to find user by username or email
|
||||
const userRecord = mockUsers.get(username);
|
||||
|
||||
if (!userRecord) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
|
||||
if (userRecord.password !== password) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
|
||||
const context = getContext<HonoVarTypes>();
|
||||
const sessionId = crypto.randomUUID();
|
||||
const csrfToken = generateCSRFToken();
|
||||
|
||||
// Store CSRF token (expires in 1 hour)
|
||||
csrfTokens.set(sessionId, {
|
||||
token: csrfToken,
|
||||
expires: Date.now() + 60 * 60 * 1000
|
||||
});
|
||||
|
||||
// Create JWT token with user info
|
||||
const token = await sign({
|
||||
sub: userRecord.user.id,
|
||||
username: userRecord.user.username,
|
||||
email: userRecord.user.email,
|
||||
sessionId,
|
||||
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 // 24 hours
|
||||
}, JWT_SECRET);
|
||||
|
||||
// Set HTTP-only cookie
|
||||
setCookie(context, 'auth_token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'Lax',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 // 24 hours
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: userRecord.user,
|
||||
csrfToken // Return CSRF token to client for subsequent requests
|
||||
};
|
||||
};
|
||||
|
||||
// We can keep checkAuth to return the current user profile from the context
|
||||
// which is populated by the firebaseAuthMiddleware
|
||||
async function checkAuth() {
|
||||
const context = getContext<HonoVarTypes>();
|
||||
const token = getCookie(context, 'auth_token');
|
||||
const user = context.get('user');
|
||||
|
||||
if (!token) {
|
||||
return { authenticated: false, user: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await verify(token, JWT_SECRET) as any;
|
||||
|
||||
// Find user
|
||||
const userRecord = Array.from(mockUsers.values()).find(
|
||||
record => record.user.id === payload.sub
|
||||
);
|
||||
|
||||
if (!userRecord) {
|
||||
if (!user) {
|
||||
return { authenticated: false, user: null };
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
user: userRecord.user
|
||||
user: user
|
||||
};
|
||||
} catch (error) {
|
||||
return { authenticated: false, user: null };
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
const context = getContext<HonoVarTypes>();
|
||||
const token = getCookie(context, 'auth_token');
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const payload = await verify(token, JWT_SECRET) as any;
|
||||
// Remove CSRF token
|
||||
if (payload.sessionId) {
|
||||
csrfTokens.delete(payload.sessionId);
|
||||
}
|
||||
} catch (error) {
|
||||
// Token invalid, just delete cookie
|
||||
}
|
||||
}
|
||||
|
||||
deleteCookie(context, 'auth_token', { path: '/' });
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function getCSRFToken() {
|
||||
const context = getContext<HonoVarTypes>();
|
||||
const token = getCookie(context, 'auth_token');
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const payload = await verify(token, JWT_SECRET) as any;
|
||||
const stored = csrfTokens.get(payload.sessionId);
|
||||
|
||||
// if (!stored) {
|
||||
// throw new Error('CSRF token not found');
|
||||
// }
|
||||
|
||||
return { csrfToken: stored?.token || null };
|
||||
}
|
||||
|
||||
export const authMethods = {
|
||||
register,
|
||||
login,
|
||||
checkAuth,
|
||||
logout,
|
||||
getCSRFToken,
|
||||
};
|
||||
|
||||
export { validateCSRFToken };
|
||||
@@ -6,11 +6,9 @@ import {
|
||||
import { tinyassert } from "@hiogawa/utils";
|
||||
import { MiddlewareHandler, type Context, type Next } from "hono";
|
||||
import { getContext } from "hono/context-storage";
|
||||
import { csrf } from 'hono/csrf'
|
||||
// import { adminAuth } from "../../lib/firebaseAdmin";
|
||||
import { z } from "zod";
|
||||
import { authMethods } from "./auth";
|
||||
import { jwt } from "hono/jwt";
|
||||
import { secret } from "./commom";
|
||||
import { abortChunk, chunkedUpload, completeChunk, createPresignedUrls, imageContentTypes, nanoid, presignedPut, videoContentTypes } from "./s3_handle";
|
||||
// import { createElement } from "react";
|
||||
|
||||
@@ -222,7 +220,7 @@ const routes = {
|
||||
),
|
||||
|
||||
// access context
|
||||
components: async () => {},
|
||||
components: async () => { },
|
||||
getHomeCourses: async () => {
|
||||
return listCourses.slice(0, 3);
|
||||
},
|
||||
@@ -298,24 +296,35 @@ const routes = {
|
||||
export type RpcRoutes = typeof routes;
|
||||
export const endpoint = "/rpc";
|
||||
export const pathsForGET: (keyof typeof routes)[] = ["getCounter"];
|
||||
export const jwtRpc: MiddlewareHandler = async (c, next) => {
|
||||
const publicPaths: (keyof typeof routes)[] = ["getHomeCourses", "getCourses", "getCourseBySlug", "getCourseContent", "login", "register"];
|
||||
|
||||
export const firebaseAuthMiddleware: MiddlewareHandler = async (c, next) => {
|
||||
const publicPaths: (keyof typeof routes)[] = ["getHomeCourses", "getCourses", "getCourseBySlug", "getCourseContent"];
|
||||
const isPublic = publicPaths.some((path) => c.req.path.split("/").includes(path));
|
||||
c.set("isPublic", isPublic);
|
||||
// return await next();
|
||||
|
||||
if (c.req.path !== endpoint && !c.req.path.startsWith(endpoint + "/") || isPublic) {
|
||||
return await next();
|
||||
}
|
||||
console.log("JWT RPC Middleware:", c.req.path);
|
||||
const jwtMiddleware = jwt({
|
||||
secret,
|
||||
cookie: 'auth_token',
|
||||
verification: {
|
||||
aud: "ez.lms_users",
|
||||
|
||||
const authHeader = c.req.header("Authorization");
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
// Option: return 401 or let it pass with no user?
|
||||
// Old logic seemed to require it for non-public paths.
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
})
|
||||
return jwtMiddleware(c, next)
|
||||
|
||||
const token = authHeader.split("Bearer ")[1];
|
||||
try {
|
||||
// const decodedToken = await adminAuth.verifyIdToken(token);
|
||||
// c.set("user", decodedToken);
|
||||
} catch (error) {
|
||||
console.error("Firebase Auth Error:", error);
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
return await next();
|
||||
}
|
||||
|
||||
export const rpcServer = async (c: Context, next: Next) => {
|
||||
if (c.req.path !== endpoint && !c.req.path.startsWith(endpoint + "/")) {
|
||||
return await next();
|
||||
|
||||
@@ -5,15 +5,26 @@ import {
|
||||
} from "@hiogawa/tiny-rpc";
|
||||
import type { RpcRoutes } from "./rpc";
|
||||
import { Result } from "@hiogawa/utils";
|
||||
import {httpClientAdapter} from "@httpClientAdapter";
|
||||
import { httpClientAdapter } from "@httpClientAdapter";
|
||||
// console.log("httpClientAdapter module:", httpClientAdapter.toString());
|
||||
declare let __host__: string;
|
||||
const endpoint = "/rpc";
|
||||
const url = import.meta.env.SSR ? "http://localhost" : "";
|
||||
const headers: Record<string, string> = {}; // inject headers to demonstrate context
|
||||
import { auth } from "../lib/firebase";
|
||||
|
||||
export const client = proxyTinyRpc<RpcRoutes>({
|
||||
adapter: httpClientAdapter({
|
||||
url: url + endpoint,
|
||||
pathsForGET: [],
|
||||
headers: async () => {
|
||||
if (import.meta.env.SSR) return {}; // No client auth on server for now
|
||||
const user = auth.currentUser;
|
||||
if (user) {
|
||||
// Force refresh if needed or just get token
|
||||
const token = await user.getIdToken();
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { renderSSRHead } from '@unhead/vue/server';
|
||||
import { buildBootstrapScript, getHrefFromManifest, loadCssByModules } from './lib/manifest';
|
||||
import { contextStorage } from 'hono/context-storage';
|
||||
import { cors } from "hono/cors";
|
||||
import { jwtRpc, rpcServer } from './api/rpc';
|
||||
import { firebaseAuthMiddleware, rpcServer } from './api/rpc';
|
||||
import isMobile from 'is-mobile';
|
||||
import { useAuthStore } from './stores/auth';
|
||||
import { cssContent } from './lib/primeCssContent';
|
||||
@@ -25,7 +25,7 @@ app.use(cors(), async (c, next) => {
|
||||
};
|
||||
c.set("isMobile", isMobile({ ua }));
|
||||
await next();
|
||||
}, rpcServer);
|
||||
}, firebaseAuthMiddleware, rpcServer);
|
||||
app.get("/.well-known/*", (c) => {
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
47
src/lib/firebase.ts
Normal file
47
src/lib/firebase.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { initializeApp } from "firebase/app";
|
||||
import { createUserWithEmailAndPassword, getAuth, GoogleAuthProvider, sendPasswordResetEmail, signInWithEmailAndPassword, signInWithPopup } from "firebase/auth";
|
||||
// TODO: Add SDKs for Firebase products that you want to use
|
||||
// https://firebase.google.com/docs/web/setup#available-libraries
|
||||
|
||||
// Your web app's Firebase configuration
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyBTr0L5qxdrVEtWuP2oAicJXQvVyeXkMts",
|
||||
authDomain: "trello-7ea39.firebaseapp.com",
|
||||
projectId: "trello-7ea39",
|
||||
storageBucket: "trello-7ea39.firebasestorage.app",
|
||||
messagingSenderId: "321067890572",
|
||||
appId: "1:321067890572:web:e34e1e657125d37be688a9"
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
const appFirebase = initializeApp(firebaseConfig);
|
||||
const provider = new GoogleAuthProvider();
|
||||
export const auth = getAuth(appFirebase);
|
||||
export const googleAuth = () => signInWithPopup(auth, provider).then((result) => {
|
||||
console.log('User signed in:', result.user);
|
||||
return result;
|
||||
})
|
||||
export const emailAuth = (username: string, password: string) => {
|
||||
return signInWithEmailAndPassword(auth, username, password)
|
||||
}
|
||||
export const forgotPassword = (email: string) => {
|
||||
return sendPasswordResetEmail(auth, email)
|
||||
.then(() => {
|
||||
console.log('Password reset email sent');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error sending password reset email:', error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
export const signUp = (email: string, password: string) => {
|
||||
return createUserWithEmailAndPassword(auth, email, password)
|
||||
.then((userCredential) => {
|
||||
console.log('User signed up:', userCredential.user);
|
||||
return userCredential.user;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error signing up:', error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
12
src/lib/firebaseAdmin.ts
Normal file
12
src/lib/firebaseAdmin.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// import { initializeApp, getApps, cert } from 'firebase-admin/app';
|
||||
// import { getAuth } from 'firebase-admin/auth';
|
||||
// import certJson from './cert.json';
|
||||
// const firebaseAdminConfig = {
|
||||
// credential: cert(certJson as any)
|
||||
// };
|
||||
|
||||
// if (getApps().length === 0) {
|
||||
// initializeApp(firebaseAdminConfig);
|
||||
// }
|
||||
|
||||
// export const adminAuth = getAuth();
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<Toast />
|
||||
<Form v-slot="$form" :resolver="resolver" :initialValues="initialValues" @submit="onFormSubmit"
|
||||
class="flex flex-col gap-4 w-full">
|
||||
<div class="text-sm text-gray-600 mb-2">
|
||||
@@ -36,6 +37,13 @@ import { zodResolver } from '@primevue/forms/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { forgotPassword } from '@/lib/firebase';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const toast = useToast();
|
||||
|
||||
const initialValues = reactive({
|
||||
email: ''
|
||||
});
|
||||
@@ -48,9 +56,11 @@ const resolver = zodResolver(
|
||||
|
||||
const onFormSubmit = ({ valid, values }: FormSubmitEvent) => {
|
||||
if (valid) {
|
||||
console.log('Form submitted:', values);
|
||||
// toast.add({ severity: 'success', summary: 'Success', detail: 'Reset link sent', life: 3000 });
|
||||
// Handle actual forgot password logic here
|
||||
forgotPassword(values.email).then(() => {
|
||||
toast.add({ severity: 'success', summary: 'Success', detail: 'Reset link sent', life: 3000 });
|
||||
}).catch(() => {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: auth.error, life: 3000 });
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -4,8 +4,8 @@
|
||||
<Form v-slot="$form" :resolver="resolver" :initialValues="initialValues" @submit="onFormSubmit"
|
||||
class="flex flex-col gap-4 w-full">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="email" class="text-sm font-medium text-gray-700">Email or Username</label>
|
||||
<InputText name="email" type="text" placeholder="admin or user@example.com" fluid
|
||||
<label for="email" class="text-sm font-medium text-gray-700">Email</label>
|
||||
<InputText name="email" type="text" placeholder="user@example.com" fluid
|
||||
:disabled="auth.loading" />
|
||||
<Message v-if="$form.email?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.email.error?.message }}</Message>
|
||||
@@ -56,15 +56,6 @@
|
||||
<router-link to="/sign-up" class="font-medium text-blue-600 hover:text-blue-500 hover:underline">Sign up
|
||||
for free</router-link>
|
||||
</p>
|
||||
|
||||
<!-- Hint for demo credentials -->
|
||||
<div class="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p class="text-xs text-blue-800 font-medium mb-1">Demo Credentials:</p>
|
||||
<p class="text-xs text-blue-600">Username: <code class="bg-blue-100 px-1 rounded">admin</code> |
|
||||
Password: <code class="bg-blue-100 px-1 rounded">admin123</code></p>
|
||||
<p class="text-xs text-blue-600">Email: <code class="bg-blue-100 px-1 rounded">user@example.com</code> |
|
||||
Password: <code class="bg-blue-100 px-1 rounded">password</code></p>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
@@ -104,7 +95,6 @@ const onFormSubmit = async ({ valid, values }: FormSubmitEvent) => {
|
||||
};
|
||||
|
||||
const loginWithGoogle = () => {
|
||||
console.log('Login with Google');
|
||||
// Handle Google login logic here
|
||||
auth.loginWithGoogle();
|
||||
};
|
||||
</script>
|
||||
@@ -43,6 +43,12 @@ import { zodResolver } from '@primevue/forms/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useToast } from "primevue/usetoast";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const toast = useToast();
|
||||
|
||||
const initialValues = reactive({
|
||||
name: '',
|
||||
email: '',
|
||||
@@ -59,9 +65,9 @@ const resolver = zodResolver(
|
||||
|
||||
const onFormSubmit = ({ valid, values }: FormSubmitEvent) => {
|
||||
if (valid) {
|
||||
console.log('Form submitted:', values);
|
||||
// toast.add({ severity: 'success', summary: 'Success', detail: 'Account created successfully', life: 3000 });
|
||||
// Handle actual signup logic here
|
||||
auth.register(values.name, values.email, values.password).catch(() => {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: auth.error, life: 3000 });
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,7 +1,9 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { client } from '@/api/rpcclient';
|
||||
import { ref } from 'vue';
|
||||
// import { client } from '@/api/rpcclient'; // client no longer used for auth actions
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { emailAuth, signUp, auth, googleAuth } from '@/lib/firebase';
|
||||
import { onAuthStateChanged, signOut, User as FirebaseUser } from 'firebase/auth';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@@ -15,42 +17,68 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const csrfToken = ref<string | null>(null);
|
||||
const initialized = ref(false);
|
||||
|
||||
// Check auth status on init (reads from cookie)
|
||||
// Check auth status on init using Firebase observer
|
||||
async function init() {
|
||||
if (initialized.value) return;
|
||||
|
||||
try {
|
||||
const response = await client.checkAuth();
|
||||
if (response.authenticated && response.user) {
|
||||
user.value = response.user;
|
||||
// Get CSRF token if authenticated
|
||||
try {
|
||||
const csrfResponse = await client.getCSRFToken();
|
||||
csrfToken.value = csrfResponse.csrfToken;
|
||||
} catch (e) {
|
||||
// CSRF token might not be available yet
|
||||
return new Promise<void>((resolve) => {
|
||||
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
|
||||
if (currentUser) {
|
||||
user.value = mapFirebaseUser(currentUser);
|
||||
} else {
|
||||
user.value = null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Not authenticated, that's fine
|
||||
} finally {
|
||||
initialized.value = true;
|
||||
resolve();
|
||||
// We could unsubscribe here if we only want initial load,
|
||||
// but keeping it listens for changes (token refresh etc)
|
||||
// However, 'init' usually implies just ONCE waiter.
|
||||
// For reactivity, user.value is updated.
|
||||
});
|
||||
// Note: onAuthStateChanged returns an unsubscribe function.
|
||||
// If we want to keep listening, we shouldn't unsubscribe immediately,
|
||||
// but for 'await auth.init()' we just want to wait for the first known state.
|
||||
});
|
||||
}
|
||||
|
||||
function mapFirebaseUser(fwUser: FirebaseUser): User {
|
||||
return {
|
||||
id: fwUser.uid,
|
||||
username: fwUser.email?.split('@')[0] || 'user', // fallback
|
||||
email: fwUser.email || '',
|
||||
name: fwUser.displayName || fwUser.email?.split('@')[0] || 'User'
|
||||
};
|
||||
}
|
||||
|
||||
async function login(username: string, password: string) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
return client.login(username, password).then((response) => {
|
||||
user.value = response.user;
|
||||
csrfToken.value = response.csrfToken;
|
||||
// Assuming username is email for Firebase, or we need to look it up?
|
||||
// Firebase works with Email. If input is username, this might fail.
|
||||
// For now assume email.
|
||||
return emailAuth(username, password).then((userCredential) => {
|
||||
user.value = mapFirebaseUser(userCredential.user);
|
||||
router.push('/');
|
||||
}).catch((e: any) => {
|
||||
// error.value = e.message || 'Login failed';
|
||||
error.value = 'Login failed';
|
||||
console.error(e);
|
||||
error.value = 'Login failed: ' + (e.message || 'Unknown error');
|
||||
throw e;
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function loginWithGoogle() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
return googleAuth().then((result) => {
|
||||
user.value = mapFirebaseUser(result.user);
|
||||
router.push('/');
|
||||
}).catch((e: any) => {
|
||||
console.error(e);
|
||||
error.value = 'Google Login failed';
|
||||
throw e;
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
@@ -60,13 +88,14 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
async function register(username: string, email: string, password: string) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
return client.register({ username, email, password }).then((response) => {
|
||||
user.value = response.user;
|
||||
csrfToken.value = response.csrfToken;
|
||||
return signUp(email, password).then((fwUser) => {
|
||||
// update profile with username?
|
||||
// updateProfile(fwUser, { displayName: username });
|
||||
user.value = mapFirebaseUser(fwUser);
|
||||
router.push('/');
|
||||
}).catch((e: any) => {
|
||||
// error.value = e.message || 'Registration failed';
|
||||
error.value = 'Registration failed';
|
||||
console.error(e);
|
||||
error.value = 'Registration failed: ' + (e.message || 'Unknown error');
|
||||
throw e;
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
@@ -74,18 +103,17 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
return client.logout().then(() => {
|
||||
return signOut(auth).then(() => {
|
||||
user.value = null;
|
||||
csrfToken.value = null;
|
||||
router.push('/');
|
||||
})
|
||||
}
|
||||
|
||||
return { user, loading, error, csrfToken, initialized, init, login, register, logout, $reset: () => {
|
||||
return {
|
||||
user, loading, error, initialized, init, login, loginWithGoogle, register, logout, $reset: () => {
|
||||
user.value = null;
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
csrfToken.value = null;
|
||||
initialized.value = false;
|
||||
} };
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
1
src/type.d.ts
vendored
1
src/type.d.ts
vendored
@@ -6,5 +6,6 @@ declare module "@httpClientAdapter" {
|
||||
export function httpClientAdapter(opts: {
|
||||
url: string;
|
||||
pathsForGET?: string[];
|
||||
headers?: () => Promise<{ Authorization?: undefined; } | { Authorization: string; }>
|
||||
}): TinyRpcClientAdapter;
|
||||
}
|
||||
Reference in New Issue
Block a user