testing hehehe
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 51m31s

This commit is contained in:
2025-11-20 22:11:09 -08:00
parent 35864fe391
commit 69be398cd9
9 changed files with 362 additions and 15 deletions

View File

@@ -86,3 +86,52 @@ sudo nginx -t
sudo systemctl reload nginx sudo systemctl reload nginx
actually the proper change was to do this in the /etc/nginx/sites-available/flyer-crawler.projectium.com file actually the proper change was to do this in the /etc/nginx/sites-available/flyer-crawler.projectium.com file
## for OAuth
1. Get Google OAuth Credentials
This is a crucial step that you must do outside the codebase:
Go to the Google Cloud Console.
Create a new project (or select an existing one).
In the navigation menu, go to APIs & Services > Credentials.
Click Create Credentials > OAuth client ID.
Select Web application as the application type.
Under Authorized redirect URIs, click ADD URI and enter the URL where Google will redirect users back to your server. For local development, this will be: http://localhost:3001/api/auth/google/callback.
Click Create. You will be given a Client ID and a Client Secret.
Add these credentials to your .env file at the project root:
plaintext
GOOGLE_CLIENT_ID="your-client-id-from-google"
GOOGLE_CLIENT_SECRET="your-client-secret-from-google"
2. Get GitHub OAuth Credentials
You'll need to obtain a Client ID and Client Secret from GitHub:
Go to your GitHub profile settings.
Navigate to Developer settings > OAuth Apps.
Click New OAuth App.
Fill in the required fields:
Application name: A descriptive name for your app (e.g., "Flyer Crawler").
Homepage URL: The base URL of your application (e.g., http://localhost:5173 for local development).
Authorization callback URL: This is where GitHub will redirect users after they authorize your app. For local development, this will be: http://localhost:3001/api/auth/github/callback.
Click Register application.
You will be given a Client ID and a Client Secret.
Add these credentials to your .env file at the project root:
plaintext
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"

114
package-lock.json generated
View File

@@ -17,6 +17,8 @@
"multer": "^2.0.2", "multer": "^2.0.2",
"nodemailer": "^7.0.10", "nodemailer": "^7.0.10",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pdfjs-dist": "^5.4.394", "pdfjs-dist": "^5.4.394",
@@ -41,6 +43,8 @@
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/nodemailer": "^7.0.4", "@types/nodemailer": "^7.0.4",
"@types/passport": "^1.0.17", "@types/passport": "^1.0.17",
"@types/passport-github2": "^1.2.9",
"@types/passport-google-oauth20": "^2.0.17",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/pg": "^8.15.6", "@types/pg": "^8.15.6",
@@ -4199,6 +4203,16 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/oauth": {
"version": "0.9.6",
"resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz",
"integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/passport": { "node_modules/@types/passport": {
"version": "1.0.17", "version": "1.0.17",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
@@ -4209,6 +4223,30 @@
"@types/express": "*" "@types/express": "*"
} }
}, },
"node_modules/@types/passport-github2": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@types/passport-github2/-/passport-github2-1.2.9.tgz",
"integrity": "sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-oauth2": "*"
}
},
"node_modules/@types/passport-google-oauth20": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.17.tgz",
"integrity": "sha512-MHNOd2l7gOTCn3iS+wInPQMiukliAUvMpODO3VlXxOiwNEMSyzV7UNvAdqxSN872o8OXx1SqPDVT6tLW74AtqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-oauth2": "*"
}
},
"node_modules/@types/passport-jwt": { "node_modules/@types/passport-jwt": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
@@ -4232,6 +4270,18 @@
"@types/passport-strategy": "*" "@types/passport-strategy": "*"
} }
}, },
"node_modules/@types/passport-oauth2": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
"integrity": "sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/oauth": "*",
"@types/passport": "*"
}
},
"node_modules/@types/passport-strategy": { "node_modules/@types/passport-strategy": {
"version": "0.2.38", "version": "0.2.38",
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
@@ -5457,6 +5507,15 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.25", "version": "2.8.25",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz",
@@ -10132,6 +10191,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/oauth": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
"integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
"license": "MIT"
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -10386,6 +10451,29 @@
"url": "https://github.com/sponsors/jaredhanson" "url": "https://github.com/sponsors/jaredhanson"
} }
}, },
"node_modules/passport-github2": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz",
"integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==",
"dependencies": {
"passport-oauth2": "1.x.x"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/passport-google-oauth20": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
"integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==",
"license": "MIT",
"dependencies": {
"passport-oauth2": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/passport-jwt": { "node_modules/passport-jwt": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
@@ -10407,6 +10495,26 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/passport-oauth2": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
"license": "MIT",
"dependencies": {
"base64url": "3.x.x",
"oauth": "0.10.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-strategy": { "node_modules/passport-strategy": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
@@ -12793,6 +12901,12 @@
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <6.0.0"
} }
}, },
"node_modules/uid2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
"license": "MIT"
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",

View File

@@ -21,6 +21,8 @@
"multer": "^2.0.2", "multer": "^2.0.2",
"nodemailer": "^7.0.10", "nodemailer": "^7.0.10",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pdfjs-dist": "^5.4.394", "pdfjs-dist": "^5.4.394",
@@ -45,6 +47,8 @@
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/nodemailer": "^7.0.4", "@types/nodemailer": "^7.0.4",
"@types/passport": "^1.0.17", "@types/passport": "^1.0.17",
"@types/passport-github2": "^1.2.9",
"@types/passport-google-oauth20": "^2.0.17",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/pg": "^8.15.6", "@types/pg": "^8.15.6",

110
server.ts
View File

@@ -1,6 +1,8 @@
import express, { Request, Response, NextFunction } from 'express'; import express, { Request, Response, NextFunction } from 'express';
import passport from 'passport'; import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local'; import { Strategy as LocalStrategy } from 'passport-local';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { Strategy as GitHubStrategy } from 'passport-github2';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'; import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import zxcvbn from 'zxcvbn'; import zxcvbn from 'zxcvbn';
@@ -14,7 +16,7 @@ import multer from 'multer';
import * as db from './src/services/db'; import * as db from './src/services/db';
import { logger } from './src/services/logger'; // This import is correct import { logger } from './src/services/logger'; // This import is correct
import * as aiService from './src/services/aiService.server'; // Import the new server-side AI service import * as aiService from './src/services/aiService.server'; // Import the new server-side AI service
import { sendPasswordResetEmail } from './src/services/emailService'; import { sendPasswordResetEmail, sendWelcomeEmail } from './src/services/emailService';
import { UserProfile, ShoppingListItem } from './src/types'; import { UserProfile, ShoppingListItem } from './src/types';
// Load environment variables from a .env file at the root of your project // Load environment variables from a .env file at the root of your project
@@ -132,6 +134,112 @@ passport.use(new LocalStrategy(
} }
)); ));
// --- Passport Google OAuth 2.0 Strategy ---
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: '/api/auth/google/callback', // Must match the one in Google Cloud Console
scope: ['profile', 'email']
},
async (accessToken, refreshToken, profile, done) => {
try {
const email = profile.emails?.[0]?.value;
if (!email) {
return done(new Error("No email found in Google profile."), false);
}
// Check if user already exists in our database
let user = await db.findUserByEmail(email);
if (user) {
// User exists, proceed to log them in.
logger.info(`Google OAuth successful for existing user: ${email}`);
const { password_hash, ...userWithoutHash } = user;
return done(null, userWithoutHash);
} else {
// User does not exist, create a new account for them.
logger.info(`Google OAuth: creating new user for email: ${email}`);
// Since this is an OAuth user, they don't have a password.
// We pass `null` for the password hash.
const newUser = await db.createUser(email, null, {
full_name: profile.displayName,
avatar_url: profile.photos?.[0]?.value
});
// Send a welcome email to the new user
try {
await sendWelcomeEmail(email, profile.displayName);
} catch (emailError) {
logger.error(`Failed to send welcome email to new Google user ${email}`, { error: emailError });
// Don't block the login flow if email fails.
}
// The `createUser` function returns the user object without the password hash.
return done(null, newUser);
}
} catch (err) {
logger.error('Error during Google authentication strategy:', { error: err });
return done(err, false);
}
}
));
// --- Passport GitHub OAuth 2.0 Strategy ---
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
callbackURL: '/api/auth/github/callback', // Must match the one in GitHub OAuth App settings
scope: ['user:email'] // Request email access
},
async (accessToken, refreshToken, profile, done) => {
try {
const email = profile.emails?.[0]?.value;
if (!email) {
return done(new Error("No public email found in GitHub profile. Please ensure your primary email is public or add one."), false);
}
// Check if user already exists in our database
let user = await db.findUserByEmail(email);
if (user) {
// User exists, proceed to log them in.
logger.info(`GitHub OAuth successful for existing user: ${email}`);
const { password_hash, ...userWithoutHash } = user;
return done(null, userWithoutHash);
} else {
// User does not exist, create a new account for them.
logger.info(`GitHub OAuth: creating new user for email: ${email}`);
// Since this is an OAuth user, they don't have a password.
// We pass `null` for the password hash.
const newUser = await db.createUser(email, null, {
full_name: profile.displayName || profile.username, // GitHub profile might not have displayName
avatar_url: profile.photos?.[0]?.value
});
// Send a welcome email to the new user
try {
await sendWelcomeEmail(email, profile.displayName || profile.username);
} catch (emailError) {
logger.error(`Failed to send welcome email to new GitHub user ${email}`, { error: emailError });
// Don't block the login flow if email fails.
}
// The `createUser` function returns the user object without the password hash.
return done(null, newUser);
}
} catch (err) {
logger.error('Error during GitHub authentication strategy:', { error: err });
return done(err, false);
}
}
));
// Serialize and deserialize user are not needed for JWT-based sessions
// passport.serializeUser((user, done) => done(null, user));
// passport.deserializeUser((user, done) => done(null, user as any));
// --- Passport JWT Strategy (for protecting API routes) --- // --- Passport JWT Strategy (for protecting API routes) ---
const jwtOptions = { const jwtOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Expect JWT in 'Authorization: Bearer <token>' header jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Expect JWT in 'Authorization: Bearer <token>' header

View File

@@ -32,7 +32,7 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- For generating UUIDs
CREATE TABLE IF NOT EXISTS public.users ( CREATE TABLE IF NOT EXISTS public.users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL, password_hash TEXT,
refresh_token TEXT, refresh_token TEXT,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL updated_at TIMESTAMPTZ DEFAULT now() NOT NULL

View File

@@ -248,6 +248,46 @@ function App() {
checkAuthToken(); checkAuthToken();
}, [fetchWatchedItems, fetchShoppingLists]); // Add callbacks to dependency array. }, [fetchWatchedItems, fetchShoppingLists]); // Add callbacks to dependency array.
// Effect to handle the token from Google OAuth redirect
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const googleToken = urlParams.get('googleAuthToken');
if (googleToken) {
logger.info('Received Google Auth token from URL. Authenticating...');
// The token is already a valid access token from our server.
// We can use it to fetch the user profile and complete the login flow.
localStorage.setItem('authToken', googleToken);
getAuthenticatedUserProfile()
.then(userProfile => {
handleLoginSuccess(userProfile.user, googleToken);
})
.catch(err => logger.error('Failed to log in with Google token', { error: err }));
// Clean the token from the URL
window.history.replaceState({}, document.title, "/");
}
const githubToken = urlParams.get('githubAuthToken');
if (githubToken) {
logger.info('Received GitHub Auth token from URL. Authenticating...');
// The token is already a valid access token from our server.
// We can use it to fetch the user profile and complete the login flow.
localStorage.setItem('authToken', githubToken);
getAuthenticatedUserProfile()
.then(userProfile => {
handleLoginSuccess(userProfile.user, githubToken);
})
.catch(err => {
logger.error('Failed to log in with GitHub token', { error: err });
// Optionally, redirect to a page with an error message
// navigate('/login?error=github_auth_failed');
});
// Clean the token from the URL
window.history.replaceState({}, document.title, "/");
}
}, [handleLoginSuccess]);
useEffect(() => { useEffect(() => {
if (isReady) { if (isReady) {

View File

@@ -5,8 +5,10 @@ import { notifySuccess, notifyError } from '../services/notificationService';
import { logger } from '../services/logger'; import { logger } from '../services/logger';
import { LoadingSpinner } from './LoadingSpinner'; import { LoadingSpinner } from './LoadingSpinner';
import { XMarkIcon } from './icons/XMarkIcon'; import { XMarkIcon } from './icons/XMarkIcon';
import { GoogleIcon } from './icons/GoogleIcon';
import { GithubIcon } from './icons/GithubIcon';
import { ConfirmationModal } from './ConfirmationModal'; import { ConfirmationModal } from './ConfirmationModal';
import { User } from '../types'; // Import User type for props import { User } from '../types';
import { PasswordInput } from './PasswordInput'; import { PasswordInput } from './PasswordInput';
type AuthStatus = 'SIGNED_OUT' | 'ANONYMOUS' | 'AUTHENTICATED'; type AuthStatus = 'SIGNED_OUT' | 'ANONYMOUS' | 'AUTHENTICATED';
@@ -265,19 +267,16 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
} }
}; };
const handleOAuthSignIn = async (provider: 'google' | 'github') => { const handleOAuthSignIn = (provider: 'google' | 'github') => {
// Redirect to the backend OAuth initiation route
// The backend will then handle the redirect to the OAuth provider
// and eventually redirect back to the frontend with a token.
window.location.href = `/api/auth/${provider}`;
// Set authLoading to true to show a spinner while redirecting
setAuthLoading(true); setAuthLoading(true);
const message = `Sign in with ${provider} is not yet implemented.`;
notifyError(message);
logger.warn(`OAuth sign-in attempt with ${provider} failed: not implemented.`);
// We set a timeout to turn off the loading indicator and clear the error
// so the user can try another method.
setTimeout(() => {
setAuthLoading(false);
}, 3000); // This was a closing parenthesis for the setTimeout
}; };
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
@@ -383,6 +382,24 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
{isRegistering ? 'Already have an account? Sign In' : "Don't have an account? Register"} {isRegistering ? 'Already have an account? Sign In' : "Don't have an account? Register"}
</button> </button>
</div> </div>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300 dark:border-gray-600" />
</div>
<div className="relative flex justify-center">
<span className="bg-white dark:bg-gray-800 px-2 text-sm text-gray-500 dark:text-gray-400">Or continue with</span>
</div>
</div>
<div className="space-y-3">
<button type="button" onClick={() => handleOAuthSignIn('google')} disabled={authLoading} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed">
{authLoading ? <div className="w-5 h-5 mr-3"><LoadingSpinner /></div> : <GoogleIcon className="w-5 h-5 mr-3" />}
Sign In with Google
</button>
<button type="button" onClick={() => handleOAuthSignIn('github')} disabled={authLoading} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed">
{authLoading ? <div className="w-5 h-5 mr-3"><LoadingSpinner /></div> : <GithubIcon className="w-5 h-5 mr-3" />}
Sign In with GitHub
</button>
</div>
</div> </div>
) )
) : ( ) : (
@@ -458,6 +475,20 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
</button> </button>
</div> </div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<h3 className="text-md font-semibold text-gray-800 dark:text-white">Link Social Accounts</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1 mb-3">Connect other accounts for easier sign-in.</p>
<div className="space-y-3">
<button type="button" onClick={() => handleOAuthLink('google')} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
<GoogleIcon className="w-5 h-5 mr-3" />
Link Google Account
</button>
<button type="button" onClick={() => handleOAuthLink('github')} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
<GithubIcon className="w-5 h-5 mr-3" />
Link GitHub Account
</button>
</div>
</div>
</form> </form>
)} )}

View File

@@ -3,7 +3,8 @@ import { generateSpeechFromText, startVoiceSession } from '../services/aiApiClie
import { logger } from '../services/logger'; import { logger } from '../services/logger';
import { notifyError } from '../services/notificationService'; import { notifyError } from '../services/notificationService';
import { LoadingSpinner } from '../components/LoadingSpinner'; import { LoadingSpinner } from '../components/LoadingSpinner';
import { SpeakerWaveIcon, MicrophoneIcon } from '../components/icons/HeroIcons'; import { SpeakerWaveIcon } from '../components/icons/SpeakerWaveIcon';
import { MicrophoneIcon } from '../components/icons/MicrophoneIcon';
export const VoiceLabPage: React.FC = () => { export const VoiceLabPage: React.FC = () => {
const [textToSpeak, setTextToSpeak] = useState('Hello! This is a test of the text-to-speech generation.'); const [textToSpeak, setTextToSpeak] = useState('Hello! This is a test of the text-to-speech generation.');

View File

@@ -54,7 +54,7 @@ export async function findUserByEmail(email: string): Promise<DbUser | undefined
*/ */
export async function createUser( export async function createUser(
email: string, email: string,
passwordHash: string, passwordHash: string | null,
profileData: { full_name?: string; avatar_url?: string } profileData: { full_name?: string; avatar_url?: string }
): Promise<{ id: string; email: string }> { ): Promise<{ id: string; email: string }> {
// Use a client from the pool to run multiple queries in a transaction // Use a client from the pool to run multiple queries in a transaction