Add google authentication
This commit is contained in:
parent
ba7d0c9121
commit
1da5dc0a30
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
my-recipes/
|
||||
my-recipes-chart/
|
||||
@ -16,4 +16,10 @@ SMTP_FROM=dvirlabs@gmail.com
|
||||
GOOGLE_CLIENT_ID=143092846986-hsi59m0on2c9rb5qrdoejfceieao2ioc.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
GOOGLE_REDIRECT_URI=http://localhost:8001/auth/google/callback
|
||||
FRONTEND_URL=http://localhost:5174
|
||||
FRONTEND_URL=http://localhost:5174
|
||||
|
||||
# Microsoft Entra ID (Azure AD) OAuth
|
||||
AZURE_CLIENT_ID=db244cf5-eb11-4738-a2ea-5b0716c9ec0a
|
||||
AZURE_CLIENT_SECRET=Zad8Q~qRBxaQq8up0lLXAq4pHzrVM2JFGFJhHaDp
|
||||
AZURE_TENANT_ID=consumers
|
||||
AZURE_REDIRECT_URI=http://localhost:8001/auth/azure/callback
|
||||
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -205,6 +205,8 @@ app.add_middleware(
|
||||
SessionMiddleware,
|
||||
secret_key=os.getenv("SECRET_KEY", "your-secret-key-change-in-production-min-32-chars"),
|
||||
max_age=3600, # 1 hour
|
||||
same_site="lax", # Prevent session issues on frequent refreshes
|
||||
https_only=False, # Set to True in production with HTTPS
|
||||
)
|
||||
|
||||
# Allow CORS from frontend domains
|
||||
@ -638,6 +640,89 @@ async def google_callback(request: Request):
|
||||
raise HTTPException(status_code=400, detail=f"Google authentication failed: {str(e)}")
|
||||
|
||||
|
||||
# ============= Microsoft Entra ID (Azure AD) OAuth Endpoints =============
|
||||
|
||||
@app.get("/auth/azure/login")
|
||||
async def azure_login(request: Request):
|
||||
"""Redirect to Microsoft Entra ID OAuth login"""
|
||||
redirect_uri = os.getenv("AZURE_REDIRECT_URI", "http://localhost:8001/auth/azure/callback")
|
||||
return await oauth.azure.authorize_redirect(request, redirect_uri)
|
||||
|
||||
|
||||
@app.get("/auth/azure/callback")
|
||||
async def azure_callback(request: Request):
|
||||
"""Handle Microsoft Entra ID OAuth callback"""
|
||||
try:
|
||||
# Get token from Azure
|
||||
token = await oauth.azure.authorize_access_token(request)
|
||||
|
||||
# Get user info from Azure
|
||||
user_info = token.get('userinfo')
|
||||
if not user_info:
|
||||
raise HTTPException(status_code=400, detail="Failed to get user info from Microsoft")
|
||||
|
||||
email = user_info.get('email') or user_info.get('preferred_username')
|
||||
azure_id = user_info.get('oid') or user_info.get('sub')
|
||||
name = user_info.get('name', '')
|
||||
given_name = user_info.get('given_name', '')
|
||||
family_name = user_info.get('family_name', '')
|
||||
|
||||
if not email:
|
||||
raise HTTPException(status_code=400, detail="Email not provided by Microsoft")
|
||||
|
||||
# Check if user exists
|
||||
existing_user = get_user_by_email(email)
|
||||
|
||||
if existing_user:
|
||||
# User exists, log them in
|
||||
user_id = existing_user["id"]
|
||||
username = existing_user["username"]
|
||||
else:
|
||||
# Create new user
|
||||
# Generate username from email or name
|
||||
username = email.split('@')[0]
|
||||
|
||||
# Check if username exists, add number if needed
|
||||
base_username = username
|
||||
counter = 1
|
||||
while get_user_by_username(username):
|
||||
username = f"{base_username}{counter}"
|
||||
counter += 1
|
||||
|
||||
# Create user with random password (they'll use Azure login)
|
||||
import secrets
|
||||
random_password = secrets.token_urlsafe(32)
|
||||
password_hash = hash_password(random_password)
|
||||
|
||||
new_user = create_user(
|
||||
username=username,
|
||||
email=email,
|
||||
password_hash=password_hash,
|
||||
first_name=given_name if given_name else None,
|
||||
last_name=family_name if family_name else None,
|
||||
display_name=name if name else username,
|
||||
is_admin=False
|
||||
)
|
||||
user_id = new_user["id"]
|
||||
|
||||
# Create JWT token
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": str(user_id), "username": username},
|
||||
expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
# Redirect to frontend with token
|
||||
frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5174")
|
||||
return Response(
|
||||
status_code=302,
|
||||
headers={"Location": f"{frontend_url}?token={access_token}"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Microsoft authentication failed: {str(e)}")
|
||||
|
||||
|
||||
# ============= Grocery Lists Endpoints =============
|
||||
|
||||
@app.get("/grocery-lists", response_model=List[GroceryList])
|
||||
|
||||
@ -18,3 +18,16 @@ oauth.register(
|
||||
'scope': 'openid email profile'
|
||||
}
|
||||
)
|
||||
|
||||
# Register Microsoft Entra ID (Azure AD) OAuth
|
||||
# Use 'common' for multi-tenant + personal accounts, or 'consumers' for personal accounts only
|
||||
tenant_id = os.getenv('AZURE_TENANT_ID', 'common')
|
||||
oauth.register(
|
||||
name='azure',
|
||||
client_id=os.getenv('AZURE_CLIENT_ID'),
|
||||
client_secret=os.getenv('AZURE_CLIENT_SECRET'),
|
||||
server_metadata_url=f'https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration',
|
||||
client_kwargs={
|
||||
'scope': 'openid email profile'
|
||||
}
|
||||
)
|
||||
|
||||
@ -72,9 +72,16 @@ function App() {
|
||||
setUser(userData);
|
||||
setIsAuthenticated(true);
|
||||
} catch (err) {
|
||||
// Token invalid or expired
|
||||
removeToken();
|
||||
setIsAuthenticated(false);
|
||||
// Only remove token on authentication errors (401), not network errors
|
||||
if (err.status === 401) {
|
||||
console.log("Token invalid or expired, logging out");
|
||||
removeToken();
|
||||
setIsAuthenticated(false);
|
||||
} else {
|
||||
// Network error or server error - keep user logged in
|
||||
console.warn("Auth check failed but keeping session:", err.message);
|
||||
setIsAuthenticated(true); // Assume still authenticated
|
||||
}
|
||||
}
|
||||
}
|
||||
setLoadingAuth(false);
|
||||
|
||||
@ -48,7 +48,9 @@ export async function getMe(token) {
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to get user info");
|
||||
const error = new Error("Failed to get user info");
|
||||
error.status = res.status;
|
||||
throw error;
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
@ -40,6 +40,11 @@ function Login({ onSuccess, onSwitchToRegister }) {
|
||||
window.location.href = `${apiBase}/auth/google/login`;
|
||||
};
|
||||
|
||||
const handleAzureLogin = () => {
|
||||
const apiBase = window.__ENV__?.API_BASE || "http://localhost:8001";
|
||||
window.location.href = `${apiBase}/auth/azure/login`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auth-container">
|
||||
<div className="auth-card">
|
||||
@ -121,6 +126,28 @@ function Login({ onSuccess, onSwitchToRegister }) {
|
||||
המשך עם Google
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAzureLogin}
|
||||
className="btn ghost full-width"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "0.5rem",
|
||||
border: "1px solid var(--border-subtle)",
|
||||
marginTop: "0.5rem"
|
||||
}}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 23 23">
|
||||
<path fill="#f25022" d="M1 1h10v10H1z"/>
|
||||
<path fill="#00a4ef" d="M12 1h10v10H12z"/>
|
||||
<path fill="#7fba00" d="M1 12h10v10H1z"/>
|
||||
<path fill="#ffb900" d="M12 12h10v10H12z"/>
|
||||
</svg>
|
||||
המשך עם Microsoft
|
||||
</button>
|
||||
|
||||
<div className="auth-footer">
|
||||
<p>
|
||||
עדיין אין לך חשבון?{" "}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user