Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
223 changes: 223 additions & 0 deletions backend/fastapi/src/adapter/input/controllers/webapp_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import os
from pathlib import Path
from typing import Optional, List
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse, FileResponse, Response, RedirectResponse
import mimetypes
from src.application.config.config import settings

# Resolve the repository-level "webapp" folder by walking up from this file
def _find_webapp_root() -> Path:
here = Path(__file__).resolve()
for base in here.parents:
candidate = base / "webapp"
if candidate.exists() and candidate.is_dir():
return candidate
# Fallback to fastapi parent if not found (will 404 later)
return Path(__file__).resolve().parents[4] / "webapp"

WEBAPP_ROOT = _find_webapp_root()

router = APIRouter(prefix="/webapp", tags=["webapp"])

ALLOWED_NAME_CHARS = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")


def _validate_app_name(app_name: str) -> str:
if not app_name or any(c not in ALLOWED_NAME_CHARS for c in app_name):
raise HTTPException(status_code=400, detail="Invalid app name")
return app_name


def _app_folder(app_name: str) -> Path:
folder = WEBAPP_ROOT / app_name
if not folder.exists() or not folder.is_dir():
raise HTTPException(status_code=404, detail="App not found")
return folder


def _safe_join(base: Path, relative: str) -> Path:
# Prevent directory traversal
target = (base / relative).resolve()
if not str(target).startswith(str(base.resolve())):
raise HTTPException(status_code=400, detail="Invalid path")
return target


def _guess_media_type(path: Path) -> Optional[str]:
mt, _ = mimetypes.guess_type(str(path))
return mt or "application/octet-stream"

def _find_index_file(folder: Path) -> Optional[Path]:
"""Return a suitable index file if present in folder."""
candidates: List[str] = [
"index.html",
"index.htm",
"Index.html",
"home.html",
]
for name in candidates:
p = folder / name
if p.exists() and p.is_file():
return p
return None

def _render_directory_listing(base_href: str, folder: Path, rel: str = "") -> HTMLResponse:
"""Produce a minimal HTML directory listing for the given folder.

base_href: URL path prefix like /api/v1/webapp/<app_name>/ (must end with '/')
rel: relative path inside the app ('' or 'sub/dir/') used to build links
"""
items: List[str] = []
# Parent link if not at app root
if rel not in ("", "/"):
parent = rel.rstrip("/").split("/")[:-1]
parent_rel = "/".join(parent) + ("/" if parent else "")
items.append(f"<li><a href='{base_href}{parent_rel}'>../</a></li>")
try:
for child in sorted(folder.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
name = child.name + ("/" if child.is_dir() else "")
href = f"{base_href}{rel}{name}"
items.append(f"<li><a href='{href}'>{name}</a></li>")
except Exception:
items.append("<li><em>Unable to list directory</em></li>")
html = (
"<!DOCTYPE html><html><head><meta charset='utf-8'><title>Directory listing</title>"
"<style>body{font-family:system-ui;padding:12px} a{text-decoration:none} li{margin:4px 0}</style>"
"</head><body>"
f"<h3>Index of {base_href}{rel}</h3><ul>" + "".join(items) + "</ul>"
"</body></html>"
)
return HTMLResponse(html)



DEFAULT_REDIRECT_APP = "gemini-proxy"

@router.get("/_list", response_model=list)
async def list_apps_json():
"""Return JSON list of available app names (subdirectories with index.html)."""
if not WEBAPP_ROOT.exists():
return []
apps = []
try:
for child in WEBAPP_ROOT.iterdir():
if child.is_dir():
apps.append(child.name)
except Exception:
return [DEFAULT_REDIRECT_APP]
# Ensure default redirect app is present even without a folder
if DEFAULT_REDIRECT_APP not in apps:
apps.insert(0, DEFAULT_REDIRECT_APP)
return apps

@router.get("/", response_class=HTMLResponse)
async def list_apps_root():
"""Serve the repository-level webapp/index.html if present; else render a dynamic listing."""
index = WEBAPP_ROOT / "index.html"
if index.exists():
try:
return HTMLResponse(index.read_text(encoding="utf-8"))
except Exception:
raise HTTPException(status_code=500, detail="Failed to read root webapp index.html")
# Fallback dynamic HTML
apps = []
if WEBAPP_ROOT.exists():
try:
for child in WEBAPP_ROOT.iterdir():
if child.is_dir() and (child / "index.html").exists():
apps.append(child.name)
except Exception:
apps = []
items = "".join(f'<li><a href="./{a}/">{a}</a></li>' for a in apps) or '<li><em>No apps found</em></li>'
html = f"""
<!DOCTYPE html><html><head><meta charset='utf-8'><title>Webapps</title></head>
<body><h1>Webapps</h1><ul>{items}</ul></body></html>
""".strip()
return HTMLResponse(html)


@router.get("/{app_name}")
async def serve_index(app_name: str, request: Request):
"""Serve a webapp or redirect for special default app.

Special case: 'gemini-proxy' -> redirect to FastAPI docs.
Otherwise serve index.html of the app folder.
"""
_validate_app_name(app_name)
if app_name == DEFAULT_REDIRECT_APP:
docs_url = f"{settings.API_PREFIX}/docs"
return RedirectResponse(url=docs_url, status_code=302)

# Ensure trailing slash so relative asset URLs resolve under /{app_name}/
if not request.url.path.endswith("/"):
# Preserve query string if any
q = ("?" + str(request.url.query)) if request.url.query else ""
return RedirectResponse(url=str(request.url.path) + "/" + q, status_code=307)

folder = _app_folder(app_name)
index_file = _find_index_file(folder)
if index_file is not None:
try:
content = index_file.read_text(encoding="utf-8")
except Exception:
raise HTTPException(status_code=500, detail="Failed to read index file")
return HTMLResponse(content=content)
# Otherwise, render directory listing at app root
base_href = str(request.url.path) # endswith '/'
if not base_href.endswith('/'):
base_href += '/'
return _render_directory_listing(base_href=base_href, folder=folder, rel="")


@router.get("/{app_name}/{asset_path:path}")
async def serve_asset(app_name: str, asset_path: str, request: Request):
"""Serve any asset file inside the app folder.

Example: /webapp/git-diff-viewer/main.js -> file webapp/git-diff-viewer/main.js
"""
_validate_app_name(app_name)
folder = _app_folder(app_name)
if asset_path in ("", "/"):
# Serve index.* when trailing slash is used, else render directory
folder = _app_folder(app_name)
index_file = _find_index_file(folder)
if index_file is not None:
media_type = _guess_media_type(index_file)
return FileResponse(str(index_file), media_type=media_type)
# No index here -> render root listing for this app
base_href = str(request.base_url).rstrip('/') + request.url.path
if not base_href.endswith('/'):
base_href += '/'
return _render_directory_listing(base_href=base_href, folder=folder, rel="")
target = _safe_join(folder, asset_path)
if target.is_dir():
# Ensure trailing slash for directory URLs
if not request.url.path.endswith('/'):
q = ("?" + str(request.url.query)) if request.url.query else ""
return RedirectResponse(url=str(request.url.path) + "/" + q, status_code=307)
# Serve index.* if present otherwise directory listing
idx = _find_index_file(target)
if idx is not None:
media_type = _guess_media_type(idx)
return FileResponse(str(idx), media_type=media_type)
# Render directory listing for nested folders
# Compute rel path inside app
rel = asset_path
if rel and not rel.endswith('/'):
rel += '/'
base_href = str(request.url.path)
if not base_href.endswith('/'):
base_href += '/'
# base_href should represent /webapp/<app_name>/<asset_path>/
# But for links we want prefix at app level:
# Build app_base by combining the request.base_url and the path prefix up to the app_name.
# Use str(...) and rstrip to avoid duplicate slashes.
app_base = str(request.base_url).rstrip('/') + request.url.path.split(app_name, 1)[0] + app_name + '/'
# Build listing using app_base + rel
return _render_directory_listing(base_href=app_base, folder=target, rel=rel)

if not target.exists() or not target.is_file():
raise HTTPException(status_code=404, detail="Asset not found")
media_type = _guess_media_type(target)
return FileResponse(str(target), media_type=media_type)
3 changes: 2 additions & 1 deletion backend/fastapi/src/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fastapi import FastAPI
from src.adapter.input.controllers import conversation_controller, health_controller, gemini_controller, messages_controller
from src.adapter.input.controllers import conversation_controller, health_controller, gemini_controller, messages_controller, webapp_controller
from fastapi import Request
from fastapi.responses import JSONResponse
from src.application.exceptions.exceptions import AppException
Expand All @@ -21,6 +21,7 @@
app.include_router(conversation_controller.router, prefix=settings.API_PREFIX)
app.include_router(messages_controller.router, prefix=settings.API_PREFIX)
app.include_router(health_controller.router, prefix=settings.API_PREFIX)
app.include_router(webapp_controller.router, prefix=settings.API_PREFIX)


# also expose health at root for backward compatibility (/health and /health/ready)
Expand Down
2 changes: 2 additions & 0 deletions webapp/2048-Game-main/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
42 changes: 42 additions & 0 deletions webapp/2048-Game-main/.github/workflows/static.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages

on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true

jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
# Upload entire repository
path: '.'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
21 changes: 21 additions & 0 deletions webapp/2048-Game-main/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Basim Ahmed Khan

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
66 changes: 66 additions & 0 deletions webapp/2048-Game-main/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@

# 2048 Game

2048 Game made using HTML, CSS, Javascript. 2048 is a puzzle Game in which you join tiles until you get 2048 score.




## Working Application

Check Out the Live Version -> [2048](https://basimahmedkhan.github.io/2048-Game/)

![Demo](https://github.com/BasimAhmedKhan/2048-Game/blob/main/Readme%20Resources/new.gif)


## Screenshots

New Game

![App Screenshot](https://github.com/BasimAhmedKhan/2048-Game/blob/main/Readme%20Resources/2048%20Screenshot.png)

After Some Playing

![App Screenshot](https://github.com/BasimAhmedKhan/2048-Game/blob/main/Readme%20Resources/2048%20Screenshot2.png)





## Support

If you like my work, feel free to:
- ⭐ this Repository. And we will live happily ever after xD.
- Join me on Social:
- [<img align="left" alt="BasimAhmedKhan | Facebook" width="22px" src="https://img.icons8.com/color/48/000000/facebook-circled--v1.png" />][facebook]
[<img align="left" alt="BasimAhmedKhan | Instagram" width="22px" src="https://img.icons8.com/fluency/48/000000/instagram-new.png" />][instagram]
[<img align="left" alt="BasimAhmedKhan | Snapchat" width="22px" src="https://img.icons8.com/color/48/000000/snapchat-circled-logo--v1.png" />][snapchat] [<img align="left" alt="BasimAhmedKhan | LinkedIn" width="22px" src="https://img.icons8.com/external-tal-revivo-shadow-tal-revivo/48/000000/external-linkedin-in-logo-used-for-professional-networking-logo-shadow-tal-revivo.png" />][linkedin]

Thanks a Bunch for Stopping By! <3


[facebook]: https://www.facebook.com/profile.php?id=100009322472394
[instagram]: https://www.instagram.com/basim_khann
[snapchat]: https://github.com/BasimAhmedKhan/BasimAhmedKhan/blob/main/assets/WhatsApp%20Image%202022-01-09%20at%207.23.20%20PM.jpeg
[linkedin]: https://www.linkedin.com/in/basim-khan-604a76189/

## Tech Stack
Front-End
<img src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/html/html.png" data-canonical-src="[https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/html/html.png]" width="200" />
<img src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/css/css.png" data-canonical-src="[https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/css/css.png]" width="200" />
<img src="https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/javascript/javascript.png" data-canonical-src="[https://raw.githubusercontent.com/github/explore/80688e429a7d4ef2fca1e82350fe8e3517d3494d/topics/javascript/javascript.png]" width="200" />


## Credits & References

| Resource | Description |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| [ImKennyYip][ImKennyYip] | A cool 2048 game made by [ImKennyYip], I reused the Javascript for this Project with some of my customization, |
| [2048][2048] | I refer to this original game by Gabriele Cirulli for the UI. |

[ImKennyYip]: https://github.com/ImKennyYip/2048
[2048]: https://play2048.co/

## License

[MIT](https://github.com/BasimAhmedKhan/2048-Game/blob/main/LICENSE)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added webapp/2048-Game-main/Readme Resources/new.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added webapp/2048-Game-main/Resources/chevron-down.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added webapp/2048-Game-main/Resources/chevron-left.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added webapp/2048-Game-main/Resources/chevron-right.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added webapp/2048-Game-main/Resources/chevron-up.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading