Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ METRICS_TOKEN=
# Accepts DEBUG/INFO/WARNING/ERROR/CRITICAL (case-insensitive) or an integer.
# Defaults to INFO when unset or invalid.
LOG_LEVEL=INFO

# Set to 1 ONLY for local development โ€” NEVER in production
OAUTH_DISABLE_SSL_VERIFY=0
81 changes: 10 additions & 71 deletions Frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,75 +1,14 @@
# =============================================================================
# Multi-stage Dockerfile for HELPDESK.AI Frontend (Vite + nginx)
# Stage 1 (builder): Build the Vite/React application
# Stage 2 (production): Serve via nginx:alpine with non-root user
# Stage 3 (dev): Vite dev server with hot module replacement
# =============================================================================

# ---------------------------------------------------------------------------
# Stage 1: builder - install deps and compile Vite production bundle
# ---------------------------------------------------------------------------
FROM node:20-alpine AS builder

WORKDIR /build

ARG VITE_API_URL=http://localhost:7860
ARG VITE_WS_URL=ws://localhost:7860/ws/metrics
ARG VITE_SUPABASE_URL
ARG VITE_SUPABASE_ANON_KEY

ENV VITE_API_URL=$VITE_API_URL
ENV VITE_WS_URL=$VITE_WS_URL
ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL
ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY

COPY package.json package-lock.json* ./
RUN npm ci --prefer-offline

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# ---------------------------------------------------------------------------
# Stage 2: production - serve static files via nginx with non-root user
# ---------------------------------------------------------------------------
FROM nginx:stable-alpine AS production

LABEL maintainer="HELPDESK.AI Team" \
version="3.0.0" \
description="AI Helpdesk frontend production image (optimized)"

RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup

COPY --from=builder /build/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

RUN chown -R appuser:appgroup /usr/share/nginx/html && \
chown -R appuser:appgroup /var/cache/nginx && \
chown -R appuser:appgroup /var/log/nginx && \
touch /var/run/nginx.pid && \
chown appuser:appgroup /var/run/nginx.pid

USER appuser

EXPOSE 80

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --spider -q http://localhost:80/ || exit 1

CMD ["nginx", "-g", "daemon off;"]

# ---------------------------------------------------------------------------
# Stage 3: dev - Vite dev server with hot module replacement
# ---------------------------------------------------------------------------
FROM node:20-alpine AS dev

FROM node:18-alpine AS production
WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm ci

COPY . .

EXPOSE 5173

CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
EXPOSE 3000
CMD ["node", "dist/main.js"]
101 changes: 65 additions & 36 deletions backend/auth/oauth_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@
import urllib.parse
import json
import ssl
import os
import logging

logger = logging.getLogger(__name__)


def _create_ssl_context() -> ssl.SSLContext:
"""
Creates a secure SSL context with full certificate verification enabled.

SSL verification is ALWAYS enforced in production.
Set OAUTH_DISABLE_SSL_VERIFY=1 ONLY for local development/testing โ€” never in production.
"""
ctx = ssl.create_default_context()

# Safety valve for local dev only โ€” never set in production
if os.getenv("OAUTH_DISABLE_SSL_VERIFY", "0") == "1":
logger.warning(
"[OAuth] SSL verification is DISABLED via OAUTH_DISABLE_SSL_VERIFY=1. "
"This must NEVER be set in production."
)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
else:
# Enforce strict SSL โ€” default secure behavior
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED

return ctx


def get_authorization_url(provider: str, client_id: str, redirect_uri: str, state: str) -> str:
"""
Expand All @@ -13,7 +43,7 @@ def get_authorization_url(provider: str, client_id: str, redirect_uri: str, stat
"response_type": "code",
"state": state
}

if provider == "google":
url = "https://accounts.google.com/o/oauth2/v2/auth"
params.update({
Expand All @@ -33,12 +63,20 @@ def get_authorization_url(provider: str, client_id: str, redirect_uri: str, stat
})
else:
raise ValueError(f"Unsupported OAuth provider: {provider}")

return f"{url}?{urllib.parse.urlencode(params)}"

def exchange_code_for_tokens(provider: str, code: str, client_id: str, client_secret: str, redirect_uri: str) -> dict:

def exchange_code_for_tokens(
provider: str,
code: str,
client_id: str,
client_secret: str,
redirect_uri: str
) -> dict:
"""
Exchanges the authorization code for an access token.
SSL verification is always enforced unless OAUTH_DISABLE_SSL_VERIFY=1 (dev only).
"""
if provider == "google":
url = "https://oauth2.googleapis.com/token"
Expand All @@ -48,90 +86,83 @@ def exchange_code_for_tokens(provider: str, code: str, client_id: str, client_se
url = "https://github.com/login/oauth/access_token"
else:
raise ValueError(f"Unsupported OAuth provider: {provider}")

payload = {
"client_id": client_id,
"client_secret": client_secret,
"code": code,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code"
}

data = urllib.parse.urlencode(payload).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
headers={"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}
headers={
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded"
}
)

# Ignore SSL verification for local dev fallback robustness if needed
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE


ctx = _create_ssl_context()

try:
with urllib.request.urlopen(req, context=ctx) as response:
res_body = response.read().decode("utf-8")
return json.loads(res_body)
except Exception as e:
print(f"[OAuth exchange error] {provider} exchange failed: {e}")
logger.error(f"[OAuth] {provider} token exchange failed: {e}")
return {"error": str(e)}


def get_user_profile(provider: str, access_token: str) -> dict:
"""
Fetches the user's email, name, avatar, and group memberships from the provider API.
SSL verification is always enforced unless OAUTH_DISABLE_SSL_VERIFY=1 (dev only).
"""
# Create SSL Context to avoid certificate validation issues in local test runners
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx = _create_ssl_context()

headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json"
}

# Stub response handler helper

def fetch_api(url, custom_headers=None):
r_headers = custom_headers or headers
req = urllib.request.Request(url, headers=r_headers)
try:
with urllib.request.urlopen(req, context=ctx) as response:
return json.loads(response.read().decode("utf-8"))
except Exception as e:
print(f"[OAuth API error] Failed to fetch {url}: {e}")
logger.error(f"[OAuth] Failed to fetch {url}: {e}")
return None

email = None
full_name = None
avatar_url = None
groups = []

if provider == "google":
# Get standard profile info
profile = fetch_api("https://www.googleapis.com/oauth2/v3/userinfo")
if profile:
email = profile.get("email")
full_name = profile.get("name")
avatar_url = profile.get("picture")

elif provider == "microsoft":
# Get Microsoft Graph profile info
profile = fetch_api("https://graph.microsoft.com/v1.0/me")
if profile:
email = profile.get("mail") or profile.get("userPrincipalName")
full_name = profile.get("displayName")

# Fetch Microsoft Graph groups

groups_data = fetch_api("https://graph.microsoft.com/v1.0/me/transitiveMemberOf")
if groups_data and "value" in groups_data:
for grp in groups_data["value"]:
# Look for group displayName
if grp.get("@odata.type") == "#microsoft.graph.group":
groups.append(grp.get("displayName"))

elif provider == "github":
# Get GitHub user profile
gh_headers = {
"Authorization": f"token {access_token}",
"Accept": "application/json",
Expand All @@ -141,28 +172,26 @@ def fetch_api(url, custom_headers=None):
if profile:
full_name = profile.get("name") or profile.get("login")
avatar_url = profile.get("avatar_url")

# Fetch emails (as user:email scope gives private emails)

emails = fetch_api("https://api.github.com/user/emails", custom_headers=gh_headers)
if emails:
primary = next((e for e in emails if e.get("primary")), None)
email = primary.get("email") if primary else emails[0].get("email")
else:
email = profile.get("email")

# Fetch GitHub Orgs/Teams as Groups

orgs = fetch_api("https://api.github.com/user/orgs", custom_headers=gh_headers)
if orgs:
for org in orgs:
groups.append(org.get("login"))

if not email:
return {"status": "error", "message": "Failed to retrieve user email from OAuth provider."}

return {
"status": "success",
"email": email,
"full_name": full_name or email.split("@")[0].title(),
"avatar_url": avatar_url,
"groups": groups
}
}
14 changes: 0 additions & 14 deletions frontend/Dockerfile

This file was deleted.

Loading