diff --git a/.gitignore b/.gitignore index c6bb3ff..eb9f0c2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__ data .env .DS_Store +iac/kb-*.txt \ No newline at end of file diff --git a/README.md b/README.md index 892da54..b4e5b76 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,21 @@ This implementation is an evolution of the [AI Chat Accelerator implementation]( - Implements Agentic RAG - Easily add additional tools for the agent to use - See conversation history and select to see past converations +- Optional ALB+Cognito authentication (disabled by default, can be enabled for production) - Built-in auto scaling architecture (see docs below) - End to end observability with AgentCore GenAI observability and OpenTelemetry (OTEL) ## Usage -Follow the 6 step process below for deploying this solution into your AWS account. +Follow the 7 step process below for deploying this solution into your AWS account. 1. Setup/Install prerequisites 2. Deploy cloud infrastructure 3. Deploy application code 4. Upload your documents to the generated S3 bucket 5. Trigger the Bedrock Knowledge Base sync -6. Start chatting with your documents in the app +6. Create a Cognito user for authentication (if authentication is enabled) +7. Start chatting with your documents in the app ### 1. Setup/Install prerequisites @@ -107,6 +109,43 @@ tags = { EOF ``` +#### Optional: Restrict web interface access by IP + +By default, the web interface is accessible from any IP address. To restrict access to specific IP addresses or CIDR blocks, add the `allowed_ips` parameter to your `terraform.tfvars`: + +```sh +cat << EOF >> terraform.tfvars +allowed_ips = [ + "203.0.113.0/24", # Your office network + "198.51.100.5/32", # Your home IP + "192.0.2.0/24" # Additional network +] +EOF +``` + +Replace the example IP addresses with your actual IP addresses or CIDR blocks. You can find your current IP address by running `curl ifconfig.me`. + +#### Optional: Enable authentication + +By default, authentication is disabled for easier development and testing. To enable ALB+Cognito authentication (recommended for production), add the `enable_authentication` parameter to your `terraform.tfvars`: + +```sh +cat << EOF >> terraform.tfvars +enable_authentication = true +EOF +``` + +**Important Security Requirement**: ALB authentication with Cognito requires HTTPS. You must provide an SSL/TLS certificate via the `acm_certificate_arn` parameter when enabling authentication. This ensures that sensitive authentication information (credentials, tokens) is encrypted during transit. + +```sh +cat << EOF >> terraform.tfvars +enable_authentication = true +acm_certificate_arn = "arn:aws:acm:region:account:certificate/certificate-id" +EOF +``` + +When authentication is enabled, users must log in via Cognito to access the application. + Deploy using terraform. ```sh @@ -201,12 +240,56 @@ make sync Note that this script calls the `bedrock-agent start-ingestion-job` API. This job will need to successfully complete before the agent will be able to answer questions about your documents. -### 6. Start chatting with your documents in the app +### 6. Create a Cognito user for authentication (if authentication is enabled) + +If you have authentication enabled, create a Cognito user: + +```sh +cd iac +./create-user.sh +``` + +Follow the prompts to create a user with your email address and a temporary password. + +### 7. Start chatting with your documents in the app ```sh open $(terraform output -raw endpoint) ``` +If authentication is enabled, you'll be redirected to the Cognito login page. Use the email and temporary password you created in step 6. You'll be prompted to set a new password on first login. + +If authentication is disabled, you can access the application directly without any login. + +## Security + +### SSL/TLS and Authentication Requirements + +For production deployments, SSL/TLS encryption is strongly recommended. When enabling Cognito authentication, SSL/TLS is **required** - you cannot enable authentication without providing an ACM certificate. + +- **SSL without authentication**: Supported - provides encrypted communication +- **Authentication without SSL**: **Not supported** - ALB authentication requires HTTPS for security +- **Both SSL and authentication**: Recommended for production + +To configure SSL/TLS, provide an ACM certificate ARN: +```hcl +acm_certificate_arn = "arn:aws:acm:region:account:certificate/certificate-id" +``` + +### IP Access Restrictions + +The web interface can be restricted to specific IP addresses by setting the `allowed_ips` variable in your `terraform.tfvars` file. This creates security group rules that only allow HTTP traffic from the specified IP addresses or CIDR blocks. + +Example: +```hcl +allowed_ips = [ + "203.0.113.0/24", # Office network + "198.51.100.5/32", # Specific IP address +] +``` + +If not specified, the default value allows access from anywhere (`0.0.0.0/0`). + ## Scaling This architecture can be scaled using two primary levers: diff --git a/agent/client.py b/agent/client.py index d31c908..1d11a3e 100644 --- a/agent/client.py +++ b/agent/client.py @@ -2,6 +2,7 @@ import json import argparse import uuid +import base64 parser = argparse.ArgumentParser(description="Create an agent runtime") parser.add_argument("--agent_runtime_arn", required=True, help="Agent runtime arn") @@ -17,9 +18,12 @@ agent_core_client = boto3.client("bedrock-agentcore") +# Encode user_id with base64 for AgentCore compatibility +user_id = base64.b64encode("test-user".encode()).decode().rstrip('=') + payload = json.dumps({ "input": { - "user_id": "6886c5c5ced611f1af8885b941a07a61", + "user_id": user_id, "prompt": "Who are you and what can you do?" } }) @@ -30,6 +34,7 @@ response = agent_core_client.invoke_agent_runtime( agentRuntimeArn=agent_runtime_arn, runtimeSessionId=session_id, + runtimeUserId=user_id, payload=payload, ) diff --git a/deploy.sh b/deploy.sh index 7d27ac1..63e01be 100755 --- a/deploy.sh +++ b/deploy.sh @@ -5,6 +5,11 @@ set -e export APP=$1 export ARCH=$2 +# Validate required variables +[ -z "$APP" ] && { echo "Error: APP variable is empty"; exit 1; } +[ -z "$ARCH" ] && { echo "Error: ARCH variable is empty"; exit 1; } +[ -z "$AWS_REGION" ] && { echo "Error: AWS_REGION variable is empty"; exit 1; } + export VERSION=$(cat /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 50 | head -n 1) # login to ECR diff --git a/iac/cognito.tf b/iac/cognito.tf new file mode 100644 index 0000000..08eeebf --- /dev/null +++ b/iac/cognito.tf @@ -0,0 +1,64 @@ +resource "aws_cognito_user_pool" "main" { + count = var.enable_authentication ? 1 : 0 + name = var.name + + auto_verified_attributes = ["email"] + admin_create_user_config { + allow_admin_create_user_only = true + } + + password_policy { + minimum_length = 8 + require_lowercase = true + require_numbers = true + require_symbols = true + require_uppercase = true + } + + schema { + attribute_data_type = "String" + name = "email" + required = true + mutable = true + } + + tags = var.tags +} + +resource "aws_cognito_user_pool_client" "main" { + count = var.enable_authentication ? 1 : 0 + name = var.name + user_pool_id = aws_cognito_user_pool.main[0].id + + generate_secret = true + allowed_oauth_flows_user_pool_client = true + allowed_oauth_flows = ["code"] + allowed_oauth_scopes = ["email", "openid", "profile"] + + callback_urls = var.acm_certificate_arn != "" ? [ + "https://${module.alb.dns_name}/oauth2/idpresponse" + ] : [ + "http://${module.alb.dns_name}/oauth2/idpresponse" + ] + + logout_urls = var.acm_certificate_arn != "" ? [ + "https://${module.alb.dns_name}" + ] : [ + "http://${module.alb.dns_name}" + ] + + supported_identity_providers = ["COGNITO"] +} + +resource "aws_cognito_user_pool_domain" "main" { + count = var.enable_authentication ? 1 : 0 + domain = "${var.name}-${random_string.cognito_domain[0].result}" + user_pool_id = aws_cognito_user_pool.main[0].id +} + +resource "random_string" "cognito_domain" { + count = var.enable_authentication ? 1 : 0 + length = 8 + special = false + upper = false +} \ No newline at end of file diff --git a/iac/create-user.sh b/iac/create-user.sh new file mode 100644 index 0000000..ccd11df --- /dev/null +++ b/iac/create-user.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Script to create a Cognito user for the AI Agent application + +set -e + +# Get Cognito User Pool ID from Terraform output +USER_POOL_ID=$(terraform output -raw cognito_user_pool_id) + +if [ -z "$USER_POOL_ID" ]; then + echo "Error: Could not get Cognito User Pool ID from Terraform output" + exit 1 +fi + +# Prompt for user email +read -p "Enter email address for the new user: " EMAIL + +if [ -z "$EMAIL" ]; then + echo "Error: Email address is required" + exit 1 +fi + +# Prompt for temporary password +read -s -p "Enter temporary password (min 8 chars, must include uppercase, lowercase, number, symbol): " TEMP_PASSWORD +echo + +if [ -z "$TEMP_PASSWORD" ]; then + echo "Error: Password is required" + exit 1 +fi + +echo "Creating user in Cognito User Pool: $USER_POOL_ID" + +# Create the user +aws cognito-idp admin-create-user \ + --user-pool-id "$USER_POOL_ID" \ + --username "$EMAIL" \ + --user-attributes Name=email,Value="$EMAIL" Name=email_verified,Value=true \ + --temporary-password "$TEMP_PASSWORD" \ + --message-action SUPPRESS + +echo "User created successfully!" +echo "Email: $EMAIL" +echo "Temporary password: $TEMP_PASSWORD" +echo "" +echo "The user will need to change their password on first login." +echo "Access the application at: $(terraform output -raw endpoint)" \ No newline at end of file diff --git a/iac/ecs.tf b/iac/ecs.tf index f850465..206b544 100644 --- a/iac/ecs.tf +++ b/iac/ecs.tf @@ -63,6 +63,14 @@ module "ecs_service" { "name" : "MEMORY_ID", "value" : var.agentcore_memory_id }, + { + "name" : "ENABLE_AUTHENTICATION", + "value" : tostring(var.enable_authentication) + }, + { + "name" : "COGNITO_LOGOUT_URL", + "value" : var.enable_authentication ? coalesce(var.cognito_logout_url, "https://${aws_cognito_user_pool_domain.main[0].domain}.auth.${data.aws_region.current.name}.amazoncognito.com/logout?client_id=${aws_cognito_user_pool_client.main[0].id}&logout_uri=${var.acm_certificate_arn != "" ? "https" : "http"}://${module.alb.dns_name}") : "" + }, ] readonly_root_filesystem = false @@ -157,33 +165,112 @@ module "alb" { vpc_id = module.vpc.vpc_id subnets = module.vpc.public_subnets - security_group_ingress_rules = { - all_http = { - from_port = 80 - to_port = 80 - ip_protocol = "tcp" - description = "HTTP web traffic" - cidr_ipv4 = "0.0.0.0/0" - } - } - - security_group_egress_rules = { for cidr_block in module.vpc.private_subnets_cidr_blocks : - (cidr_block) => { - ip_protocol = "-1" - cidr_ipv4 = cidr_block - } - } + access_logs = var.alb_access_logs_enabled ? { + bucket = aws_s3_bucket.alb_logs.id + enabled = true + } : {} - listeners = { - http = { - port = "80" - protocol = "HTTP" + connection_logs = var.alb_connection_logs_enabled ? { + bucket = aws_s3_bucket.alb_logs.id + enabled = true + } : {} - forward = { - target_group_key = "ecs-task" + security_group_ingress_rules = merge( + { for idx, ip in var.allowed_ips : + "http_${idx}" => merge( + { + from_port = 80 + to_port = 80 + ip_protocol = "tcp" + description = "HTTP web traffic from ${ip}" + }, + strcontains(ip, ":") ? { cidr_ipv6 = ip } : { cidr_ipv4 = ip } + ) + }, + var.acm_certificate_arn != "" ? { for idx, ip in var.allowed_ips : + "https_${idx}" => merge( + { + from_port = 443 + to_port = 443 + ip_protocol = "tcp" + description = "HTTPS web traffic from ${ip}" + }, + strcontains(ip, ":") ? { cidr_ipv6 = ip } : { cidr_ipv4 = ip } + ) + } : {} + ) + + security_group_egress_rules = merge( + { for cidr_block in module.vpc.private_subnets_cidr_blocks : + (cidr_block) => { + ip_protocol = "-1" + cidr_ipv4 = cidr_block + } + }, + { + https_outbound = { + from_port = 443 + to_port = 443 + ip_protocol = "tcp" + cidr_ipv4 = "0.0.0.0/0" + description = "HTTPS outbound for Cognito authentication" } } - } + ) + + listeners = merge( + { + http = merge( + { + port = "80" + protocol = "HTTP" + }, + var.acm_certificate_arn != "" ? { + redirect = { + port = "443" + protocol = "HTTPS" + status_code = "HTTP_301" + } + } : var.enable_authentication ? { + authenticate_cognito = { + user_pool_arn = aws_cognito_user_pool.main[0].arn + user_pool_client_id = aws_cognito_user_pool_client.main[0].id + user_pool_domain = aws_cognito_user_pool_domain.main[0].domain + } + forward = { + target_group_key = "ecs-task" + } + } : { + forward = { + target_group_key = "ecs-task" + } + } + ) + }, + var.acm_certificate_arn != "" ? { + https = merge( + { + port = "443" + protocol = "HTTPS" + certificate_arn = var.acm_certificate_arn + }, + var.enable_authentication ? { + authenticate_cognito = { + user_pool_arn = aws_cognito_user_pool.main[0].arn + user_pool_client_id = aws_cognito_user_pool_client.main[0].id + user_pool_domain = aws_cognito_user_pool_domain.main[0].domain + } + forward = { + target_group_key = "ecs-task" + } + } : { + forward = { + target_group_key = "ecs-task" + } + } + ) + } : {} + ) target_groups = { diff --git a/iac/outputs.tf b/iac/outputs.tf index c75ffb1..930ac16 100644 --- a/iac/outputs.tf +++ b/iac/outputs.tf @@ -52,3 +52,23 @@ output "s3_bucket_name" { description = "The name of the s3 bucket that was created" value = aws_s3_bucket.main.bucket } + +output "cognito_user_pool_id" { + description = "Cognito User Pool ID" + value = var.enable_authentication ? aws_cognito_user_pool.main[0].id : null +} + +output "cognito_user_pool_client_id" { + description = "Cognito User Pool Client ID" + value = var.enable_authentication ? aws_cognito_user_pool_client.main[0].id : null +} + +output "cognito_domain" { + description = "Cognito Domain for authentication" + value = var.enable_authentication ? "https://${aws_cognito_user_pool_domain.main[0].domain}.auth.${data.aws_region.current.name}.amazoncognito.com" : null +} + +output "alb_logs_bucket_name" { + description = "The name of the S3 bucket for ALB access logs" + value = aws_s3_bucket.alb_logs.bucket +} \ No newline at end of file diff --git a/iac/s3.tf b/iac/s3.tf index 17546b9..56166f6 100644 --- a/iac/s3.tf +++ b/iac/s3.tf @@ -17,3 +17,59 @@ resource "aws_s3_bucket" "llm_logs" { bucket = "${var.name}-llm-logs-${local.account_id}" force_destroy = true } + +# bucket for storing ALB access logs +resource "aws_s3_bucket" "alb_logs" { + bucket = "${var.name}-alb-logs-${local.account_id}" + force_destroy = true +} + +resource "aws_s3_bucket_public_access_block" "alb_logs" { + bucket = aws_s3_bucket.alb_logs.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +data "aws_elb_service_account" "main" {} + +resource "aws_s3_bucket_policy" "alb_logs" { + bucket = aws_s3_bucket.alb_logs.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + AWS = data.aws_elb_service_account.main.arn + } + Action = "s3:PutObject" + Resource = "${aws_s3_bucket.alb_logs.arn}/*" + }, + { + Effect = "Allow" + Principal = { + Service = "delivery.logs.amazonaws.com" + } + Action = "s3:PutObject" + Resource = "${aws_s3_bucket.alb_logs.arn}/*" + Condition = { + StringEquals = { + "s3:x-amz-acl" = "bucket-owner-full-control" + } + } + }, + { + Effect = "Allow" + Principal = { + Service = "delivery.logs.amazonaws.com" + } + Action = "s3:GetBucketAcl" + Resource = aws_s3_bucket.alb_logs.arn + } + ] + }) +} diff --git a/iac/terraform.tfvars.example b/iac/terraform.tfvars.example new file mode 100644 index 0000000..0d5e850 --- /dev/null +++ b/iac/terraform.tfvars.example @@ -0,0 +1,31 @@ +name = "agent" +tags = { + app = "agent" +} + +# Restrict web interface access to specific IP addresses +# Replace with your actual IP addresses/CIDR blocks +allowed_ips = [ + "203.0.113.0/24", # Example office network + "198.51.100.5/32", # Example specific IP + "192.0.2.0/24" # Example home network +] + +# These will be populated after agent deployment +# agentcore_runtime_arn = "arn:aws:bedrock-agentcore:::runtime/" +# agentcore_memory_id = "agent-suffix" + +# Optional: ACM certificate ARN for HTTPS support +acm_certificate_arn = "arn:aws:acm:::certificate/" + +# Enable ALB+Cognito authentication (default: false) +# Uncomment to enable authentication for production deployments +# enable_authentication = true + +# Custom Cognito logout URL (for custom domains) +# If not specified, will use default: https://.auth..amazoncognito.com/logout?client_id=&logout_uri= +# cognito_logout_url = "https://auth.yourdomain.com/logout?client_id=your-client-id&logout_uri=" + +# ALB Logging Configuration +alb_access_logs_enabled = true +alb_connection_logs_enabled = true \ No newline at end of file diff --git a/iac/variables.tf b/iac/variables.tf index faed20d..dfd2cb1 100644 --- a/iac/variables.tf +++ b/iac/variables.tf @@ -50,3 +50,44 @@ variable "agentcore_memory_id" { type = string default = "" } + +variable "allowed_ips" { + description = "List of IP addresses/CIDR blocks allowed to access the web interface" + type = list(string) + default = ["0.0.0.0/0"] +} + +variable "acm_certificate_arn" { + description = "ARN of the ACM certificate for HTTPS listener" + type = string + default = "" +} + +variable "alb_access_logs_enabled" { + description = "Enable ALB access logs" + type = bool + default = false +} + +variable "alb_connection_logs_enabled" { + description = "Enable ALB connection logs" + type = bool + default = false +} + +variable "enable_authentication" { + description = "Enable ALB+Cognito authentication" + type = bool + default = false + + validation { + condition = !var.enable_authentication || var.acm_certificate_arn != "" + error_message = "SSL/TLS certificate (acm_certificate_arn) is required when authentication is enabled. ALB authentication requires HTTPS for security." + } +} + +variable "cognito_logout_url" { + description = "Custom Cognito logout URL (overrides default constructed URL)" + type = string + default = null +} diff --git a/main.py b/main.py index 06268e4..deee3ff 100644 --- a/main.py +++ b/main.py @@ -2,8 +2,9 @@ import log import sys import signal +import os from datetime import datetime, timezone -from flask import Flask, request, render_template, abort +from flask import Flask, request, render_template, abort, redirect, url_for from markupsafe import Markup import mistune import uuid @@ -26,6 +27,7 @@ def signal_handler(signal, frame): signal.signal(signal.SIGTERM, signal_handler) app = Flask(__name__) +app.secret_key = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production') # Setup OpenTelemetry tracer_provider = TracerProvider() @@ -54,6 +56,9 @@ def after_request(response): # initialize database client db = database.Database() +# Check if authentication is enabled +auth_enabled = os.environ.get('ENABLE_AUTHENTICATION', 'false').lower() == 'true' + @app.template_filter('markdown') def render_markdown(text): @@ -73,9 +78,61 @@ def health_check(): def get_current_user_id(): - """get the currently logged in user""" - # TODO: get current user id from auth - return "user-1" + """get the currently logged in user from Cognito headers or fallback for non-authenticated mode""" + import base64 + import json + + if auth_enabled: + # ALB forwards Cognito user info in headers + email = request.headers.get('x-amzn-oidc-data') + if email: + # Decode the JWT payload (middle part) + try: + payload = email.split('.')[1] + # Add padding if needed + payload += '=' * (4 - len(payload) % 4) + decoded = base64.b64decode(payload) + user_data = json.loads(decoded) + email_address = user_data.get('email', 'unknown-user') + # Encode email with base64 to comply with actorId pattern [a-zA-Z0-9][a-zA-Z0-9-_/]* + return base64.b64encode(email_address.encode()).decode().rstrip('=') + except Exception as e: + logging.warning(f"Failed to decode user data: {e}") + + # Fallback for development/testing - also encode to maintain consistency + fallback_user = request.headers.get('x-amzn-oidc-identity', 'user-1') + return base64.b64encode(fallback_user.encode()).decode().rstrip('=') + else: + # Non-authenticated mode - use a default user + default_user = 'anonymous-user' + return base64.b64encode(default_user.encode()).decode().rstrip('=') + + +def get_current_user_email(): + """get the currently logged in user's email for display""" + import base64 + import json + + if auth_enabled: + # ALB forwards Cognito user info in headers + email = request.headers.get('x-amzn-oidc-data') + if email: + # Decode the JWT payload (middle part) + try: + payload = email.split('.')[1] + # Add padding if needed + payload += '=' * (4 - len(payload) % 4) + decoded = base64.b64decode(payload) + user_data = json.loads(decoded) + return user_data.get('email', 'Unknown User') + except Exception as e: + logging.warning(f"Failed to decode user data: {e}") + + # Fallback for development/testing + return request.headers.get('x-amzn-oidc-identity', 'Test User') + else: + # Non-authenticated mode + return 'Anonymous User' def get_chat_history(user_id): @@ -91,7 +148,38 @@ def get_chat_history(user_id): @app.route("/") def index(): """home page""" - return render_template("index.html", conversation={}) + user_email = get_current_user_email() if auth_enabled else None + return render_template("index.html", conversation={}, user_email=user_email, auth_enabled=auth_enabled) + + +@app.route("/logout") +def logout(): + """logout route - expires ALB auth cookies and redirects to Cognito logout""" + if not auth_enabled: + return redirect(url_for('index')) + + # Get Cognito logout URL + cognito_logout_url = os.environ.get('COGNITO_LOGOUT_URL') + if not cognito_logout_url: + return redirect('/') + + # Create response that redirects to Cognito logout + response = redirect(cognito_logout_url) + + # Expire ALB authentication session cookies + # ALB can create up to 4 cookie shards (AWSELBAuthSessionCookie-0 to AWSELBAuthSessionCookie-3) + for i in range(4): + cookie_name = f"AWSELBAuthSessionCookie-{i}" + response.set_cookie( + cookie_name, + value='', + expires=0, # Expire immediately + path='/', + secure=True, + httponly=True + ) + + return response @app.route("/new", methods=["POST"]) @@ -104,7 +192,8 @@ def new(): def conversations(): """GET /conversations returns just the conversation history""" user_id = get_current_user_id() - return render_template("conversations.html", chat_history=get_chat_history(user_id)) + user_email = get_current_user_email() if auth_enabled else None + return render_template("conversations.html", chat_history=get_chat_history(user_id), user_email=user_email, auth_enabled=auth_enabled) @app.route("/ask", methods=["POST"]) @@ -222,6 +311,8 @@ def ask_api_new(): abort(400, m) question = body["question"] + user_id = get_current_user_id() + conversation = { "conversationId": str(uuid.uuid4()), "userId": user_id, diff --git a/static/style.css b/static/style.css index 6dfbaaa..7401b54 100644 --- a/static/style.css +++ b/static/style.css @@ -64,6 +64,38 @@ body { transition: transform 0.2s ease; } +/* User Info Styles */ +.user-info { + align-items: center; + gap: 1rem; +} + +.user-email { + color: rgba(255, 255, 255, 0.9); + font-size: 0.9rem; + font-weight: 500; + background: rgba(255, 255, 255, 0.1); + padding: 0.5rem 1rem; + border-radius: 20px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.user-info .btn-outline-light { + border-color: rgba(255, 255, 255, 0.3); + color: white; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + transition: all 0.3s ease; +} + +.user-info .btn-outline-light:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.5); + color: white; + transform: translateY(-1px); +} + /* Main Container */ .main-container { background: var(--dark-bg); diff --git a/templates/index.html b/templates/index.html index 95f47be..c13add5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -79,10 +79,18 @@
-
+
+ {% if auth_enabled and user_email %} +
+ +
+ {% endif %}