init
This commit is contained in:
2
darkbazaar/.gitignore
vendored
Executable file
2
darkbazaar/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
.venv
|
||||
__pycache__/
|
||||
13
darkbazaar/Dockerfile
Executable file
13
darkbazaar/Dockerfile
Executable file
@@ -0,0 +1,13 @@
|
||||
FROM python:3.10-slim-bookworm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install uuid
|
||||
|
||||
COPY src/ ./src/
|
||||
COPY src/static/ ./src/static/
|
||||
COPY src/templates/ ./src/templates/
|
||||
|
||||
EXPOSE 8000
|
||||
16
darkbazaar/Dockerfile.cleaner
Executable file
16
darkbazaar/Dockerfile.cleaner
Executable file
@@ -0,0 +1,16 @@
|
||||
FROM python:3.10-slim-bookworm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY src/ ./src/
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Install cron
|
||||
RUN apt-get update && apt-get install -y cron procps dash
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["cron", "-f"]
|
||||
45
darkbazaar/docker-compose.yml
Executable file
45
darkbazaar/docker-compose.yml
Executable file
@@ -0,0 +1,45 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://postgres:mysecretpassword@db:5432/fastapi_db
|
||||
command: sh -c "SESSION_SECRET_KEY=$(python -c 'import uuid; print(uuid.uuid4().hex)') uvicorn src.main:app --loop uvloop --workers 4 --host 0.0.0.0 --port 8000"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
cleaner:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.cleaner
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://postgres:mysecretpassword@db:5432/fastapi_db
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: fastapi_db
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: mysecretpassword
|
||||
command: ["postgres", "-c", "max_connections=1000"]
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d fastapi_db"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
14
darkbazaar/docker-entrypoint.sh
Executable file
14
darkbazaar/docker-entrypoint.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
echo "Error: DATABASE_URL is not set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "* * * * * root PYTHONPATH=/app DATABASE_URL=\"$DATABASE_URL\" /usr/local/bin/python3 -m src.cleaner > /proc/1/fd/1 2>&1" > /etc/cron.d/cleaner_job
|
||||
chmod 0644 /etc/cron.d/cleaner_job
|
||||
|
||||
touch /var/log/cron.log
|
||||
|
||||
exec "$@"
|
||||
BIN
darkbazaar/requirements.txt
Executable file
BIN
darkbazaar/requirements.txt
Executable file
Binary file not shown.
0
darkbazaar/src/__init__.py
Executable file
0
darkbazaar/src/__init__.py
Executable file
9
darkbazaar/src/auth.py
Executable file
9
darkbazaar/src/auth.py
Executable file
@@ -0,0 +1,9 @@
|
||||
from passlib.context import CryptContext
|
||||
|
||||
ctx = CryptContext(schemes=["pbkdf2_sha256"], pbkdf2_sha256__rounds=1)
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return ctx.hash(password)
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return ctx.verify(plain_password, hashed_password)
|
||||
44
darkbazaar/src/cleaner.py
Executable file
44
darkbazaar/src/cleaner.py
Executable file
@@ -0,0 +1,44 @@
|
||||
import asyncio
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from src.db import Base, SeekerRequestOrm, GhostlinkInsightOrm, FinderInsightRequestOrm, UserOrm
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL")
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
|
||||
|
||||
async def clean_database():
|
||||
print(f"Cleaner started at {datetime.now()}")
|
||||
async with SessionLocal() as db:
|
||||
six_minutes_ago = datetime.now() - timedelta(minutes=6)
|
||||
|
||||
tables_to_clean_order = [
|
||||
("seeker_requests", SeekerRequestOrm),
|
||||
("ghostlink_insights", GhostlinkInsightOrm),
|
||||
("finder_insight_requests", FinderInsightRequestOrm),
|
||||
("users", UserOrm),
|
||||
]
|
||||
|
||||
for table_name, orm_model in tables_to_clean_order:
|
||||
initial_count_result = await db.execute(select(func.count()).select_from(orm_model))
|
||||
initial_count = initial_count_result.scalar_one()
|
||||
|
||||
delete_statement = delete(orm_model).where(orm_model.created_at < six_minutes_ago)
|
||||
result = await db.execute(delete_statement)
|
||||
cleaned_count = result.rowcount
|
||||
|
||||
final_count_result = await db.execute(select(func.count()).select_from(orm_model))
|
||||
final_count = final_count_result.scalar_one()
|
||||
|
||||
print(f"Table '{table_name}': Cleaned {cleaned_count} records. Current records: {final_count}")
|
||||
|
||||
await db.commit()
|
||||
print("Cleaner finished.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(clean_database())
|
||||
324
darkbazaar/src/db.py
Executable file
324
darkbazaar/src/db.py
Executable file
@@ -0,0 +1,324 @@
|
||||
import uuid
|
||||
import random
|
||||
import os
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
CheckConstraint,
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
delete,
|
||||
func,
|
||||
select,
|
||||
ForeignKey,
|
||||
DateTime
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import ARRAY, UUID
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.ext.mutable import MutableList
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker, relationship, joinedload
|
||||
|
||||
from .auth import get_password_hash, verify_password
|
||||
from .models import UserCreate, SeekerRequestBase, GhostlinkInsightBase
|
||||
from typing import List, Optional
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL")
|
||||
|
||||
engine = create_async_engine(
|
||||
DATABASE_URL,
|
||||
pool_size=50,
|
||||
max_overflow=100,
|
||||
pool_recycle=3600,
|
||||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class UserOrm(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String, unique=True, index=True, nullable=False)
|
||||
role = Column(String, default="seeker", nullable=False)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
seeker_requests = relationship("SeekerRequestOrm", back_populates="user", cascade="all, delete-orphan", primaryjoin="UserOrm.username==SeekerRequestOrm.seeker_username")
|
||||
ghostlink_insights = relationship("GhostlinkInsightOrm", back_populates="user", cascade="all, delete-orphan", primaryjoin="UserOrm.username==GhostlinkInsightOrm.ghostlink_username")
|
||||
finder_insight_requests_as_ghostlink = relationship("FinderInsightRequestOrm", back_populates="ghostlink_user", primaryjoin="UserOrm.username==FinderInsightRequestOrm.ghostlink_username")
|
||||
finder_insight_request_as_finder = relationship("FinderInsightRequestOrm", back_populates="finder_user", uselist=False, primaryjoin="UserOrm.username==FinderInsightRequestOrm.finder_username")
|
||||
|
||||
|
||||
class SeekerRequestOrm(Base):
|
||||
__tablename__ = "seeker_requests"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
seeker_username = Column(String, ForeignKey("users.username", ondelete="CASCADE"), nullable=False, index=True)
|
||||
uuid = Column(UUID(as_uuid=True), unique=True, nullable=False, index=True)
|
||||
description = Column(String, nullable=False)
|
||||
contact_info = Column(String, nullable=False)
|
||||
fulfilled_by_finder_username = Column(MutableList.as_mutable(ARRAY(String)), default=[], nullable=False, index=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
user = relationship("UserOrm", back_populates="seeker_requests", primaryjoin="UserOrm.username==SeekerRequestOrm.seeker_username")
|
||||
|
||||
|
||||
class GhostlinkInsightOrm(Base):
|
||||
__tablename__ = "ghostlink_insights"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
ghostlink_username = Column(String, ForeignKey("users.username", ondelete="CASCADE"), nullable=False, index=True)
|
||||
uuid = Column(UUID(as_uuid=True), nullable=False)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
user = relationship("UserOrm", back_populates="ghostlink_insights", primaryjoin="UserOrm.username==GhostlinkInsightOrm.ghostlink_username")
|
||||
|
||||
|
||||
class FinderInsightRequestOrm(Base):
|
||||
__tablename__ = "finder_insight_requests"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
finder_username = Column(String, ForeignKey("users.username", ondelete="CASCADE"), nullable=False, index=True, unique=True)
|
||||
ghostlink_username = Column(String, ForeignKey("users.username", ondelete="CASCADE"), nullable=False)
|
||||
status = Column(String, default="pending", nullable=False)
|
||||
suggested_insight_uuid = Column(UUID(as_uuid=True), nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
finder_user = relationship("UserOrm", back_populates="finder_insight_request_as_finder", primaryjoin="UserOrm.username==FinderInsightRequestOrm.finder_username")
|
||||
ghostlink_user = relationship("UserOrm", back_populates="finder_insight_requests_as_ghostlink", primaryjoin="UserOrm.username==FinderInsightRequestOrm.ghostlink_username")
|
||||
|
||||
|
||||
async def get_db():
|
||||
async with SessionLocal() as db:
|
||||
yield db
|
||||
|
||||
|
||||
async def init_db():
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
async def get_user(db: AsyncSession, username: str) -> UserOrm | None:
|
||||
result = await db.execute(
|
||||
select(UserOrm)
|
||||
.options(joinedload(UserOrm.seeker_requests))
|
||||
.options(joinedload(UserOrm.ghostlink_insights))
|
||||
.options(joinedload(UserOrm.finder_insight_request_as_finder))
|
||||
.options(joinedload(UserOrm.finder_insight_requests_as_ghostlink))
|
||||
.where(UserOrm.username == username)
|
||||
)
|
||||
user_orm = result.scalars().unique().one_or_none()
|
||||
return user_orm
|
||||
|
||||
|
||||
async def authenticate_user(db: AsyncSession, username: str, password: str):
|
||||
user = await get_user(db, username)
|
||||
if (not user) or (not verify_password(password, user.hashed_password)):
|
||||
return None
|
||||
return user
|
||||
|
||||
|
||||
async def create_user(db: AsyncSession, user: UserCreate):
|
||||
if await get_user(db, user.username):
|
||||
return None
|
||||
hashed_password = get_password_hash(user.password)
|
||||
new_user = UserOrm(username=user.username, hashed_password=hashed_password, role=user.role)
|
||||
db.add(new_user)
|
||||
await db.commit()
|
||||
await db.refresh(new_user)
|
||||
return new_user
|
||||
|
||||
|
||||
# Seeker functions
|
||||
async def create_seeker_request(db: AsyncSession, seeker_user: UserOrm, seeker_request: SeekerRequestBase) -> str | None:
|
||||
if seeker_user.role != "seeker":
|
||||
return "Access denied: only 'seeker' role can create requests."
|
||||
|
||||
if len(seeker_user.seeker_requests) >= 3:
|
||||
return "You can only have up to 3 active requests at a time."
|
||||
|
||||
existing_request = await db.scalar(
|
||||
select(SeekerRequestOrm)
|
||||
.where(SeekerRequestOrm.uuid == seeker_request.uuid)
|
||||
)
|
||||
|
||||
if existing_request:
|
||||
return "A request for this UUID is already active."
|
||||
|
||||
new_request = SeekerRequestOrm(
|
||||
seeker_username=seeker_user.username,
|
||||
uuid=seeker_request.uuid,
|
||||
description=seeker_request.description,
|
||||
contact_info=seeker_request.contact_info,
|
||||
)
|
||||
db.add(new_request)
|
||||
await db.commit()
|
||||
await db.refresh(new_request)
|
||||
return None
|
||||
|
||||
|
||||
async def get_seeker_request_by_uuid(db: AsyncSession, item_uuid: uuid.UUID) -> SeekerRequestOrm | None:
|
||||
result = await db.execute(
|
||||
select(SeekerRequestOrm)
|
||||
.where(SeekerRequestOrm.uuid == item_uuid)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def fulfill_seeker_request(db: AsyncSession, item_uuid: uuid.UUID, finder_username: str) -> SeekerRequestOrm | None:
|
||||
seeker_request = await get_seeker_request_by_uuid(db, item_uuid)
|
||||
if not seeker_request:
|
||||
return None
|
||||
|
||||
if finder_username not in seeker_request.fulfilled_by_finder_username:
|
||||
seeker_request.fulfilled_by_finder_username.append(finder_username)
|
||||
await db.commit()
|
||||
await db.refresh(seeker_request)
|
||||
return seeker_request
|
||||
|
||||
|
||||
async def get_all_fulfilled_seeker_requests(db: AsyncSession, username: str) -> List[SeekerRequestOrm]:
|
||||
|
||||
query = select(SeekerRequestOrm).where(func.cardinality(SeekerRequestOrm.fulfilled_by_finder_username) > 0)
|
||||
|
||||
if username:
|
||||
query = query.where(SeekerRequestOrm.fulfilled_by_finder_username.contains([username]))
|
||||
|
||||
result = await db.execute(query)
|
||||
requests = result.scalars().all()
|
||||
return requests
|
||||
|
||||
|
||||
# Ghostlink functions
|
||||
async def create_ghostlink_insight(db: AsyncSession, ghostlink_user: UserOrm, insight_uuid: uuid.UUID) -> str | None:
|
||||
if ghostlink_user.role != "ghostlink":
|
||||
return "Access denied: only 'ghostlink' role can create insights."
|
||||
|
||||
if len(ghostlink_user.ghostlink_insights) >= 3:
|
||||
return "You can only have up to 3 insights at a time."
|
||||
|
||||
new_insight = GhostlinkInsightOrm(
|
||||
ghostlink_username=ghostlink_user.username,
|
||||
uuid=insight_uuid
|
||||
)
|
||||
db.add(new_insight)
|
||||
await db.commit()
|
||||
await db.refresh(new_insight)
|
||||
return None
|
||||
|
||||
|
||||
async def get_ghostlink_insights(db: AsyncSession, ghostlink_username: str) -> List[GhostlinkInsightOrm]:
|
||||
result = await db.execute(
|
||||
select(GhostlinkInsightOrm)
|
||||
.where(GhostlinkInsightOrm.ghostlink_username == ghostlink_username)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def get_random_ghostlink_insight(db: AsyncSession, ghostlink_username: str) -> GhostlinkInsightOrm | None:
|
||||
ghostlink_user = await get_user(db, ghostlink_username)
|
||||
if not ghostlink_user or not ghostlink_user.ghostlink_insights:
|
||||
return None
|
||||
|
||||
return random.choice(ghostlink_user.ghostlink_insights)
|
||||
|
||||
|
||||
# Finder-Ghostlink Request functions
|
||||
async def create_finder_insight_request(db: AsyncSession, finder_user: UserOrm, ghostlink_username: str) -> str | None:
|
||||
if finder_user.role != "finder":
|
||||
return "Access denied: only 'finder' role can request insights."
|
||||
|
||||
ghostlink_user = await get_user(db, ghostlink_username)
|
||||
if not ghostlink_user:
|
||||
return "Ghostlink not found."
|
||||
|
||||
existing_request = await db.scalar(
|
||||
select(FinderInsightRequestOrm)
|
||||
.where(FinderInsightRequestOrm.finder_username == finder_user.username)
|
||||
)
|
||||
if existing_request:
|
||||
return "You already have a request to a Ghostlink."
|
||||
|
||||
if finder_user.username == ghostlink_username:
|
||||
return "You cannot request an insight from yourself."
|
||||
|
||||
new_request = FinderInsightRequestOrm(
|
||||
finder_username=finder_user.username,
|
||||
ghostlink_username=ghostlink_username,
|
||||
status="pending"
|
||||
)
|
||||
db.add(new_request)
|
||||
await db.commit()
|
||||
await db.refresh(new_request)
|
||||
return None
|
||||
|
||||
|
||||
async def get_finder_insight_request(db: AsyncSession, finder_username: str) -> FinderInsightRequestOrm | None:
|
||||
result = await db.execute(
|
||||
select(FinderInsightRequestOrm)
|
||||
.where(FinderInsightRequestOrm.finder_username == finder_username)
|
||||
.order_by(FinderInsightRequestOrm.id.desc())
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_all_ghostlink_requests(db: AsyncSession, ghostlink_username: str) -> List[FinderInsightRequestOrm]:
|
||||
result = await db.execute(
|
||||
select(FinderInsightRequestOrm)
|
||||
.where(FinderInsightRequestOrm.ghostlink_username == ghostlink_username, FinderInsightRequestOrm.status == "pending")
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def accept_finder_insight_request(db: AsyncSession, ghostlink_user: UserOrm, finder_username: str, username: Optional[str] = None) -> str | GhostlinkInsightBase | None:
|
||||
|
||||
if ghostlink_user.role == "seeker":
|
||||
return "Access denied: only 'ghostlink' role can accept requests."
|
||||
|
||||
user = await get_user(db, username)
|
||||
if username and user.role != ghostlink_user.role:
|
||||
ghost_username = user.username
|
||||
else:
|
||||
ghost_username = ghostlink_user.username
|
||||
|
||||
request_to_fulfill = await db.scalar(
|
||||
select(FinderInsightRequestOrm)
|
||||
.where(FinderInsightRequestOrm.finder_username == finder_username, FinderInsightRequestOrm.ghostlink_username == ghost_username, FinderInsightRequestOrm.status == "pending")
|
||||
)
|
||||
|
||||
if not request_to_fulfill:
|
||||
return "Pending request not found."
|
||||
|
||||
random_insight = await get_random_ghostlink_insight(db, ghost_username)
|
||||
if not random_insight:
|
||||
return "Ghostlink has no insights to share."
|
||||
|
||||
insight_uuid_to_return = random_insight.uuid
|
||||
|
||||
request_to_fulfill.status = "accepted"
|
||||
request_to_fulfill.suggested_insight_uuid = insight_uuid_to_return
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(request_to_fulfill)
|
||||
|
||||
return GhostlinkInsightBase(uuid=insight_uuid_to_return)
|
||||
|
||||
|
||||
async def reject_finder_insight_request(db: AsyncSession, ghostlink_user: UserOrm, finder_username: str) -> str | None:
|
||||
if ghostlink_user.role != "ghostlink":
|
||||
return "Access denied: only 'ghostlink' role can reject requests."
|
||||
|
||||
request_to_reject = await db.scalar(
|
||||
select(FinderInsightRequestOrm)
|
||||
.where(FinderInsightRequestOrm.finder_username == finder_username, FinderInsightRequestOrm.ghostlink_username == ghostlink_user.username, FinderInsightRequestOrm.status == "pending")
|
||||
)
|
||||
|
||||
if not request_to_reject:
|
||||
return "Pending request not found."
|
||||
|
||||
request_to_reject.status = "rejected"
|
||||
await db.commit()
|
||||
await db.refresh(request_to_reject)
|
||||
return None
|
||||
330
darkbazaar/src/main.py
Executable file
330
darkbazaar/src/main.py
Executable file
@@ -0,0 +1,330 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from typing import Annotated, Optional, List
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, Depends, HTTPException, status, Request, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from .db import (
|
||||
authenticate_user,
|
||||
create_user,
|
||||
get_db,
|
||||
get_user,
|
||||
init_db,
|
||||
create_seeker_request,
|
||||
get_seeker_request_by_uuid,
|
||||
fulfill_seeker_request,
|
||||
create_ghostlink_insight,
|
||||
get_ghostlink_insights,
|
||||
get_random_ghostlink_insight,
|
||||
create_finder_insight_request,
|
||||
get_finder_insight_request,
|
||||
get_all_ghostlink_requests,
|
||||
accept_finder_insight_request,
|
||||
reject_finder_insight_request,
|
||||
SeekerRequestOrm,
|
||||
GhostlinkInsightOrm,
|
||||
FinderInsightRequestOrm,
|
||||
get_all_fulfilled_seeker_requests
|
||||
)
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from .models import User, UserCreate, SeekerRequestBase, GhostlinkInsightBase, FinderItemBase
|
||||
import os
|
||||
|
||||
|
||||
templates = Jinja2Templates(directory="src/templates")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await init_db()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan, title="DarkBazaar")
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
app.add_middleware(SessionMiddleware, secret_key=os.getenv("SESSION_SECRET_KEY"))
|
||||
app.mount("/static", StaticFiles(directory="src/static"), name="static")
|
||||
|
||||
|
||||
async def get_current_user(request: Request, db: Annotated[AsyncSession, Depends(get_db)]):
|
||||
username = request.session.get('username')
|
||||
if not username:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
||||
user_orm = await get_user(db, username)
|
||||
if not user_orm:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
||||
return User.model_validate(user_orm)
|
||||
|
||||
|
||||
def render_profile_template(
|
||||
request: Request,
|
||||
user: User,
|
||||
found_item: Optional[FinderItemBase] = None,
|
||||
error: Optional[str] = None,
|
||||
success_message: Optional[str] = None
|
||||
) -> HTMLResponse:
|
||||
context = {
|
||||
"request": request,
|
||||
"role": user.role,
|
||||
"username": user.username,
|
||||
"seeker_requests": [SeekerRequestBase.model_validate(req) for req in user.seeker_requests],
|
||||
"ghostlink_insights": [GhostlinkInsightBase.model_validate(ins) for ins in user.ghostlink_insights],
|
||||
"finder_insight_request_as_finder": user.finder_insight_request_as_finder,
|
||||
"found_item": found_item,
|
||||
"error": error,
|
||||
"success_message": success_message,
|
||||
}
|
||||
return templates.TemplateResponse("profile.html", context)
|
||||
|
||||
|
||||
def render_relationships_template(
|
||||
request: Request,
|
||||
user: User,
|
||||
ghostlink_requests: Optional[List[FinderInsightRequestOrm]] = None,
|
||||
error: Optional[str] = None
|
||||
) -> HTMLResponse:
|
||||
context = {
|
||||
"request": request,
|
||||
"user": user,
|
||||
"ghostlink_requests": ghostlink_requests,
|
||||
"error": error,
|
||||
}
|
||||
return templates.TemplateResponse("relationships.html", context)
|
||||
|
||||
|
||||
@app.post("/users/register")
|
||||
async def register_new_user(
|
||||
request: Request,
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
role: str = Form("seeker"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
user_create = UserCreate(username=username, password=password, role=role)
|
||||
new_user = await create_user(db, user_create)
|
||||
if not new_user:
|
||||
return templates.TemplateResponse("register.html", {"request": request, "error": "User with this name already exists"})
|
||||
return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.post("/users/login")
|
||||
async def login_user(
|
||||
request: Request,
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
user = await authenticate_user(db, username, password)
|
||||
if not user:
|
||||
return templates.TemplateResponse("login.html", {"request": request, "error": "User with this credentials doesn't exist"})
|
||||
request.session["username"] = user.username
|
||||
return RedirectResponse(url="/profile", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.post('/logout')
|
||||
async def logout(request: Request, current_user: Annotated[User, Depends(get_current_user)]):
|
||||
request.session.pop("username", None)
|
||||
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get('/', response_class=HTMLResponse)
|
||||
async def get_root(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
|
||||
@app.get('/register', response_class=HTMLResponse)
|
||||
async def get_register_form(request: Request):
|
||||
return templates.TemplateResponse("register.html", {"request": request})
|
||||
|
||||
|
||||
@app.get('/login', response_class=HTMLResponse)
|
||||
async def get_login_form(request: Request):
|
||||
return templates.TemplateResponse("login.html", {"request": request})
|
||||
|
||||
|
||||
@app.get('/profile', response_class=HTMLResponse)
|
||||
async def get_profile_page(
|
||||
request: Request,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)]
|
||||
):
|
||||
user_orm_refreshed = await get_user(db, current_user.username)
|
||||
if not user_orm_refreshed:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found after refresh")
|
||||
|
||||
updated_user = User.model_validate(user_orm_refreshed)
|
||||
|
||||
return render_profile_template(
|
||||
request=request,
|
||||
user=updated_user,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@app.post('/seeker/create_request')
|
||||
async def create_new_seeker_request(
|
||||
request: Request,
|
||||
item_uuid: uuid.UUID = Form(...),
|
||||
description: str = Form(...),
|
||||
contact_info: str = Form(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
if current_user.role != 'seeker':
|
||||
return render_profile_template(request=request, user=current_user, error="Access denied: only 'seeker' role can create requests.")
|
||||
|
||||
seeker_request_base = SeekerRequestBase(uuid=item_uuid, description=description, contact_info=contact_info)
|
||||
error = await create_seeker_request(db, current_user, seeker_request_base)
|
||||
if error:
|
||||
return render_profile_template(request=request, user=current_user, error=error)
|
||||
|
||||
return RedirectResponse(url="/profile", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.post('/finder/find_item')
|
||||
async def find_item(
|
||||
request: Request,
|
||||
item_uuid: uuid.UUID = Form(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
if current_user.role != 'finder':
|
||||
return render_profile_template(request=request, user=current_user, error="Access denied: only 'finder' role can find items.")
|
||||
|
||||
seeker_request = await get_seeker_request_by_uuid(db, item_uuid)
|
||||
if not seeker_request:
|
||||
return render_profile_template(request=request, user=current_user, error="Item with this UUID is not currently being sought.")
|
||||
|
||||
await fulfill_seeker_request(db, item_uuid, current_user.username)
|
||||
|
||||
found_item_data = FinderItemBase(
|
||||
uuid=seeker_request.uuid,
|
||||
description=seeker_request.description,
|
||||
contact_info=seeker_request.contact_info
|
||||
)
|
||||
|
||||
return render_profile_template(request=request, user=current_user, found_item=found_item_data, success_message="You successfully found an item!")
|
||||
|
||||
|
||||
@app.post('/ghostlink/create_insight')
|
||||
async def create_new_ghostlink_insight(
|
||||
request: Request,
|
||||
insight_uuid: uuid.UUID = Form(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
if current_user.role != 'ghostlink':
|
||||
return render_profile_template(request=request, user=current_user, error="Access denied: only 'ghostlink' role can create insights.")
|
||||
|
||||
error = await create_ghostlink_insight(db, current_user, insight_uuid)
|
||||
if error:
|
||||
return render_profile_template(request=request, user=current_user, error=error) # Изменено
|
||||
|
||||
return RedirectResponse(url="/profile", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get('/ghostlink/requests', response_class=HTMLResponse)
|
||||
async def get_ghostlink_requests_page(
|
||||
request: Request,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)]
|
||||
):
|
||||
if current_user.role != 'ghostlink':
|
||||
return render_profile_template(request=request, user=current_user, error="Access denied: only 'ghostlink' role can view requests.")
|
||||
|
||||
ghostlink_requests = await get_all_ghostlink_requests(db, current_user.username)
|
||||
|
||||
return render_relationships_template(
|
||||
request=request,
|
||||
user=current_user,
|
||||
ghostlink_requests=ghostlink_requests
|
||||
)
|
||||
|
||||
|
||||
@app.post('/ghostlink/accept_request')
|
||||
async def accept_ghostlink_request_endpoint(
|
||||
request: Request,
|
||||
finder_username: str = Form(...),
|
||||
username: Optional[str] = Form(None),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
if not current_user.finder_insight_requests_as_ghostlink:
|
||||
return render_profile_template(request=request, user=current_user, error="Access denied: only \'ghostlink\' role can accept request.")
|
||||
|
||||
result = await accept_finder_insight_request(db, current_user, finder_username, username)
|
||||
if isinstance(result, str):
|
||||
ghostlink_requests = await get_all_ghostlink_requests(db, current_user.username)
|
||||
return render_relationships_template(request=request, user=current_user, ghostlink_requests=ghostlink_requests, error=result)
|
||||
|
||||
return RedirectResponse(url="/ghostlink/accept/successful", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get('/ghostlink/accept/successful', response_class=HTMLResponse)
|
||||
async def get_ghostlink_accept_successful_page(request: Request):
|
||||
return templates.TemplateResponse("successful_accept.html", {"request": request})
|
||||
|
||||
|
||||
@app.post('/ghostlink/reject_request')
|
||||
async def reject_ghostlink_request_endpoint(
|
||||
request: Request,
|
||||
finder_username: str = Form(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
if current_user.role != 'ghostlink':
|
||||
return render_profile_template(request=request, user=current_user, error="Access denied: only 'ghostlink' role can reject requests.")
|
||||
|
||||
error = await reject_finder_insight_request(db, current_user, finder_username)
|
||||
if error:
|
||||
ghostlink_requests = await get_all_ghostlink_requests(db, current_user.username)
|
||||
return render_relationships_template(request=request, user=current_user, ghostlink_requests=ghostlink_requests, error=error)
|
||||
|
||||
return RedirectResponse(url="/ghostlink/reject/successful", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get('/ghostlink/reject/successful', response_class=HTMLResponse)
|
||||
async def get_ghostlink_reject_successful_page(request: Request):
|
||||
return templates.TemplateResponse("succesful_reject.html", {"request": request})
|
||||
|
||||
|
||||
@app.get('/finder/get_fulfilled_descriptions')
|
||||
async def get_fulfilled_descriptions(
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
username: str = "anonymous"
|
||||
) -> List[FinderItemBase]:
|
||||
if current_user.role != 'finder':
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied: only 'finder' role can view fulfilled descriptions.")
|
||||
|
||||
fulfilled_requests_orm = await get_all_fulfilled_seeker_requests(db, username)
|
||||
return [FinderItemBase.model_validate(req) for req in fulfilled_requests_orm]
|
||||
|
||||
|
||||
@app.post('/finder/request_insight')
|
||||
async def request_insight_from_ghostlink(
|
||||
request: Request,
|
||||
ghostlink_username: str = Form(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
if current_user.role != 'finder':
|
||||
return render_profile_template(request=request, user=current_user, error="Access denied: only 'finder' role can request insights.")
|
||||
|
||||
error = await create_finder_insight_request(db, current_user, ghostlink_username)
|
||||
if error:
|
||||
return render_profile_template(request=request, user=current_user, error=error)
|
||||
|
||||
return RedirectResponse(url="/finder/request_insight/successful", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get('/finder/request_insight/successful', response_class=HTMLResponse)
|
||||
async def get_finder_insight_request_as_finder_insight_successful_page(request: Request):
|
||||
return templates.TemplateResponse("successful_request.html", {"request": request})
|
||||
|
||||
47
darkbazaar/src/models.py
Executable file
47
darkbazaar/src/models.py
Executable file
@@ -0,0 +1,47 @@
|
||||
import uuid
|
||||
|
||||
from typing import Literal, Optional, List
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class SeekerRequestBase(BaseModel):
|
||||
uuid: uuid.UUID
|
||||
description: str = Field(min_length=1, max_length=250)
|
||||
contact_info: str = Field(min_length=1, max_length=250)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class FinderItemBase(BaseModel):
|
||||
uuid: uuid.UUID
|
||||
description: str
|
||||
contact_info: str
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class GhostlinkInsightBase(BaseModel):
|
||||
uuid: uuid.UUID
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class FinderInsightRequestBase(BaseModel):
|
||||
finder_username: str
|
||||
ghostlink_username: str
|
||||
status: Literal['pending', 'accepted', 'rejected'] = 'pending'
|
||||
suggested_insight_uuid: Optional[uuid.UUID] = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
role: str
|
||||
seeker_requests: List[SeekerRequestBase] = []
|
||||
ghostlink_insights: List[GhostlinkInsightBase] = []
|
||||
finder_insight_request_as_finder: Optional[FinderInsightRequestBase] = None
|
||||
finder_insight_requests_as_ghostlink: Optional[List[FinderInsightRequestBase]] = []
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
role: Literal['seeker', 'finder', 'ghostlink'] = 'seeker'
|
||||
|
||||
|
||||
493
darkbazaar/src/static/style.css
Executable file
493
darkbazaar/src/static/style.css
Executable file
@@ -0,0 +1,493 @@
|
||||
|
||||
/* General Body Styles */
|
||||
body {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
background-color: #06060f; /* Very dark, almost black indigo */
|
||||
color: #a6e1fa; /* Bright light blue for main text */
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 40px auto;
|
||||
background-color: #0d0d1a; /* Darker indigo for containers */
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 25px rgba(0, 240, 255, 0.4); /* Electric blue glow */
|
||||
border: 1px solid #00f0ff; /* Electric blue border */
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #00f0ff; /* Electric blue for headers */
|
||||
text-shadow: 0 0 10px rgba(0, 240, 255, 0.7); /* Electric blue glow */
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #b3ecff; /* Slightly lighter blue for paragraphs */
|
||||
}
|
||||
|
||||
a {
|
||||
color: #bf00ff; /* Vibrant purple for links */
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease, text-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #00f0ff; /* Electric blue on hover */
|
||||
text-shadow: 0 0 8px rgba(0, 240, 255, 0.6); /* Electric blue glow on hover */
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
form {
|
||||
background-color: #1a1a33; /* Dark indigo for forms */
|
||||
padding: 25px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #bf00ff; /* Vibrant purple border */
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #00f0ff; /* Electric blue for labels */
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="number"],
|
||||
select,
|
||||
textarea {
|
||||
width: calc(100% - 20px);
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #bf00ff; /* Vibrant purple border */
|
||||
background-color: #06060f; /* Very dark, almost black indigo */
|
||||
color: #a6e1fa; /* Bright light blue for input text */
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 8px rgba(191, 0, 255, 0.3); /* Purple inner glow */
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none; /* Prevent textarea from being resizable */
|
||||
}
|
||||
|
||||
input[type="submit"],
|
||||
button.btn {
|
||||
background-color: #ff007f; /* Vibrant fuchsia for buttons */
|
||||
color: #06060f; /* Very dark indigo for button text */
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="submit"]:hover,
|
||||
button.btn:hover {
|
||||
background-color: #00f0ff; /* Electric blue on hover */
|
||||
box-shadow: 0 0 15px rgba(0, 240, 255, 0.7); /* Electric blue glow on hover */
|
||||
color: #06060f; /* Very dark indigo for button text on hover */
|
||||
}
|
||||
|
||||
/* Specific button styles */
|
||||
.btn-primary {
|
||||
background-color: #bf00ff; /* Vibrant purple */
|
||||
color: #06060f; /* Very dark indigo */
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #00f0ff; /* Electric blue on hover */
|
||||
box-shadow: 0 0 15px rgba(0, 240, 255, 0.7); /* Electric blue glow on hover */
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #3a005a; /* Deep indigo for secondary */
|
||||
color: #a6e1fa; /* Bright light blue */
|
||||
border: 1px solid #bf00ff; /* Vibrant purple border */
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: #bf00ff; /* Vibrant purple on hover */
|
||||
box-shadow: 0 0 15px rgba(191, 0, 255, 0.7); /* Purple glow on hover */
|
||||
color: #06060f; /* Very dark indigo for text on hover */
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background-color: #008cff; /* Bright blue */
|
||||
color: #06060f; /* Very dark indigo */
|
||||
}
|
||||
.btn-info:hover {
|
||||
background-color: #00bfff; /* Lighter bright blue on hover */
|
||||
box-shadow: 0 0 15px rgba(0, 140, 255, 0.7); /* Blue glow on hover */
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #ff007f; /* Vibrant fuchsia for danger */
|
||||
color: #06060f; /* Very dark indigo */
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background-color: #ff3399; /* Lighter fuchsia on hover */
|
||||
box-shadow: 0 0 15px rgba(255, 0, 127, 0.7); /* Fuchsia glow on hover */
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #00e676; /* Neon green for success */
|
||||
color: #06060f; /* Very dark indigo */
|
||||
}
|
||||
.btn-success:hover {
|
||||
background-color: #33ff99; /* Lighter neon green on hover */
|
||||
box-shadow: 0 0 15px rgba(0, 230, 118, 0.7); /* Neon green glow on hover */
|
||||
}
|
||||
|
||||
.btn-outline-dark {
|
||||
background-color: transparent;
|
||||
color: #00f0ff; /* Electric blue */
|
||||
border: 1px solid #00f0ff; /* Electric blue border */
|
||||
}
|
||||
.btn-outline-dark:hover {
|
||||
background-color: #00f0ff; /* Electric blue */
|
||||
color: #06060f; /* Very dark indigo */
|
||||
box-shadow: 0 0 15px rgba(0, 240, 255, 0.7); /* Electric blue glow on hover */
|
||||
}
|
||||
|
||||
/* Alerts and Messages */
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #2a001a; /* Dark background for danger */
|
||||
color: #ff3399; /* Lighter fuchsia for text */
|
||||
border-color: #ff007f; /* Vibrant fuchsia for border */
|
||||
box-shadow: 0 0 18px rgba(255, 0, 127, 0.5); /* Fuchsia glow */
|
||||
}
|
||||
|
||||
/* New style for success alerts */
|
||||
.alert-success {
|
||||
background-color: #0d2a1a; /* Dark background for success */
|
||||
color: #33ff99; /* Lighter neon green for text */
|
||||
border-color: #00e676; /* Neon green for border */
|
||||
box-shadow: 0 0 18px rgba(0, 230, 118, 0.5); /* Neon green glow */
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
background-color: #0d0d1a; /* Darker indigo */
|
||||
border: 1px solid #00f0ff; /* Electric blue border */
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #00f0ff; /* Electric blue border */
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
color: #b3ecff; /* Slightly lighter blue */
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #1a1a33; /* Dark indigo */
|
||||
color: #00f0ff; /* Electric blue */
|
||||
text-shadow: 0 0 8px rgba(0, 240, 255, 0.6); /* Electric blue glow */
|
||||
}
|
||||
|
||||
.risen {
|
||||
color: #a6e1fa; /* Bright light blue */
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 8px rgba(0, 240, 255, 0.6); /* Electric blue glow */
|
||||
}
|
||||
|
||||
/* Cards (for investor requests) */
|
||||
.card {
|
||||
background-color: #1a1a33; /* Dark indigo */
|
||||
border: 1px solid #bf00ff; /* Vibrant purple border */
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 20px rgba(191, 0, 255, 0.4); /* Purple glow */
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #00f0ff; /* Electric blue */
|
||||
text-shadow: 0 0 8px rgba(0, 240, 255, 0.6); /* Electric blue glow */
|
||||
}
|
||||
|
||||
.card-text {
|
||||
color: #b3ecff; /* Slightly lighter blue */
|
||||
}
|
||||
|
||||
/* List groups (for analyst requests) */
|
||||
.list-group {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
background-color: #0d0d1a; /* Darker indigo */
|
||||
border: 1px solid #00f0ff; /* Electric blue border */
|
||||
margin-bottom: 10px;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-group-item strong {
|
||||
color: #00f0ff; /* Electric blue */
|
||||
}
|
||||
|
||||
.seeker-request-item {
|
||||
display: flex;
|
||||
flex-direction: column; /* Stack details vertically */
|
||||
align-items: flex-start; /* Align text to the left */
|
||||
gap: 8px; /* Space between each detail */
|
||||
}
|
||||
|
||||
.request-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap; /* Allow items to wrap on smaller screens */
|
||||
}
|
||||
|
||||
.request-detail code {
|
||||
background-color: #06060f; /* Dark background for code */
|
||||
color: #a6e1fa; /* Bright light blue for code text */
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #bf00ff; /* Vibrant purple border */
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: #00e676; /* Neon green */
|
||||
color: #06060f; /* Very dark indigo */
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: #008cff; /* Bright blue */
|
||||
color: #06060f; /* Very dark indigo */
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: #4a006a; /* Muted deep purple */
|
||||
color: #e0b3ff; /* Light purple for contrast */
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.me-2 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.mt-2 { margin-top: 0.5rem !important; }
|
||||
.mt-3 { margin-top: 1rem !important; }
|
||||
.mt-4 { margin-top: 1.5rem !important; }
|
||||
.mt-5 { margin-top: 3rem !important; }
|
||||
.mb-3 { margin-bottom: 1rem !important; }
|
||||
.mb-4 { margin-bottom: 1.5rem !important; }
|
||||
.d-grid { display: grid !important; }
|
||||
.gap-2 { gap: 0.5rem !important; }
|
||||
.col-6 { flex: 0 0 auto; width: 50%; }
|
||||
.mx-auto { margin-right: auto !important; margin-left: auto !important; }
|
||||
.input-group { display: flex; }
|
||||
.form-control { flex: 1 1 auto; width: 1%; }
|
||||
.alert-danger { /* existing styles */ }
|
||||
.alert { /* existing styles */ }
|
||||
.d-grid.gap-2.col-6.mx-auto {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.d-grid { display: grid; }
|
||||
.gap-2 { gap: 0.5rem; }
|
||||
|
||||
.section {
|
||||
background-color: #0d0d1a; /* Darker indigo */
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #00f0ff; /* Electric blue border */
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 0 20px rgba(0, 240, 255, 0.4); /* Electric blue glow */
|
||||
}
|
||||
|
||||
/* New style for found item details card */
|
||||
.found-item-card {
|
||||
background-color: #0d0d1a; /* Darker indigo for containers */
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 20px rgba(0, 240, 255, 0.4); /* Electric blue glow */
|
||||
border: 1px solid #00f0ff; /* Electric blue border */
|
||||
margin-top: 20px; /* Add some space above it */
|
||||
}
|
||||
|
||||
.found-item-card h3 {
|
||||
color: #00f0ff; /* Electric blue for header */
|
||||
text-shadow: 0 0 10px rgba(0, 240, 255, 0.7);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.found-item-card p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.found-item-card code {
|
||||
background-color: #06060f;
|
||||
color: #a6e1fa;
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #bf00ff;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #7fbfd8; /* Muted blue for hints */
|
||||
font-style: italic;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0,0,0,0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
.col-6 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Aggressive reset for list items within the header */
|
||||
.header ul {
|
||||
list-style: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.header li {
|
||||
list-style: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Existing header styles - ensure they are after the aggressive reset */
|
||||
.header {
|
||||
background-color: #06060f; /* Very dark, almost black indigo */
|
||||
padding: 15px 40px; /* More padding for header */
|
||||
border-bottom: 1px solid #00f0ff; /* Electric blue border */
|
||||
box-shadow: 0 0 25px rgba(0, 240, 255, 0.4); /* Electric blue glow */
|
||||
display: flex;
|
||||
justify-content: space-between; /* Space out logo and nav links */
|
||||
align-items: center;
|
||||
width: 100%; /* Ensure header takes full width */
|
||||
}
|
||||
|
||||
.header .logo {
|
||||
color: #00f0ff; /* Electric blue for logo */
|
||||
font-size: 2.2em; /* Larger font size for logo */
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 12px rgba(0, 240, 255, 0.8); /* Stronger electric blue glow */
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.header .nav-links {
|
||||
display: flex; /* Arrange items horizontally */
|
||||
gap: 25px; /* Space between navigation items */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header .nav-links li {
|
||||
display: flex; /* Ensure li elements are also flex containers */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header .nav-links a {
|
||||
color: #a6e1fa; /* Bright light blue for nav links */
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
padding: 8px 15px;
|
||||
border: 1px solid transparent; /* Transparent border by default */
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap; /* Prevent text from wrapping */
|
||||
}
|
||||
|
||||
.header .nav-links a:hover {
|
||||
color: #00f0ff; /* Electric blue on hover */
|
||||
text-shadow: 0 0 8px rgba(0, 240, 255, 0.6); /* Electric blue glow on hover */
|
||||
border-color: #00f0ff; /* Electric blue border on hover */
|
||||
}
|
||||
|
||||
/* New styles for the logout link */
|
||||
.header .nav-links .nav-link-logout {
|
||||
color: #bf00ff; /* Vibrant purple */
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
padding: 8px 15px;
|
||||
border: 1px solid transparent; /* Transparent border by default */
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header .nav-links .nav-link-logout:hover {
|
||||
color: #e0b3ff; /* Lighter purple on hover */
|
||||
text-shadow: 0 0 8px rgba(191, 0, 255, 0.6); /* Purple glow on hover */
|
||||
border-color: #e0b3ff; /* Lighter purple border on hover */
|
||||
}
|
||||
|
||||
/* Ensure forms within nav li are well-behaved */
|
||||
.header .nav-links li form {
|
||||
display: flex; /* Allow content to flow nicely */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Remove old btn-logout styles */
|
||||
.header .nav-links .btn-logout {
|
||||
display: none !important;
|
||||
}
|
||||
.header .nav-links .btn-logout:hover {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
32
darkbazaar/src/templates/header.html
Executable file
32
darkbazaar/src/templates/header.html
Executable file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title | default('DarkBazaar') }}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/" class="logo">DarkBazaar</a>
|
||||
<ul class="nav-links">
|
||||
{% if 'username' in request.session %}
|
||||
<li><a href="/profile">Profile</a></li>
|
||||
{% if request.session.role == 'ghostlink' %}
|
||||
<li><a href="/ghostlink/requests">Requests</a></li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="#" onclick="document.getElementById('logout-form').submit(); return false;" class="nav-link-logout">Logout</a>
|
||||
<form id="logout-form" action="/logout" method="post" style="display: none;"></form>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="/login">Login</a></li>
|
||||
<li><a href="/register">Register</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
|
||||
22
darkbazaar/src/templates/index.html
Executable file
22
darkbazaar/src/templates/index.html
Executable file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Главная</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
{% include "header.html" %}
|
||||
<div class="container">
|
||||
<h1>Welcome</h1>
|
||||
<p>Step into DarkBazaar, where Seekers, Finders, and Ghostlinks move through a shadowy market of secrets—trust no one and navigate the hidden paths of this enigmatic world.</p>
|
||||
{% if not 'username' in request.session %}
|
||||
<p>
|
||||
<a href="/login" class="button">Login</a>
|
||||
<a href="/register" class="button">Register</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
29
darkbazaar/src/templates/login.html
Executable file
29
darkbazaar/src/templates/login.html
Executable file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Вход</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
{% include "header.html" %}
|
||||
<div class="container">
|
||||
<h1>Login</h1>
|
||||
{% if error %}
|
||||
<p class="alert alert-danger">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form action="/users/login" method="post">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
|
||||
<input type="submit" value="Login" class="btn btn-primary">
|
||||
</form>
|
||||
|
||||
<p class="mt-3">Don't have an account? <a href="/register">Register</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
154
darkbazaar/src/templates/profile.html
Executable file
154
darkbazaar/src/templates/profile.html
Executable file
@@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Профиль пользователя</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
{% include "header.html" %}
|
||||
<div class="container">
|
||||
<h1>User Profile: {{ username }}</h1>
|
||||
<p>Role: <strong>{{ role }}</strong></p>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success_message %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
{{ success_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Seeker Profile Section #}
|
||||
{% if role == 'seeker' %}
|
||||
<div class="section">
|
||||
<h2 class="mt-4">Your Search Requests</h2>
|
||||
{% if seeker_requests %}
|
||||
<ul class="list-group">
|
||||
{% for req in seeker_requests %}
|
||||
<li class="list-group-item seeker-request-item">
|
||||
<div class="request-detail-uuid">
|
||||
<strong>UUID:</strong> <code id="seeker-uuid-{{ loop.index0 }}">{{ req.uuid }}</code>
|
||||
<button class="btn btn-secondary btn-sm" onclick="copyToClipboard('seeker-uuid-{{ loop.index0 }}')">Copy</button>
|
||||
</div>
|
||||
<div class="request-detail-description">
|
||||
<strong>Description:</strong> {{ req.description }}
|
||||
</div>
|
||||
<div class="request-detail-contact">
|
||||
<strong>Contact:</strong> {{ req.contact_info }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>You have no active search requests.</p>
|
||||
{% endif %}
|
||||
<h3 class="mt-4">Create a New Search Request</h3>
|
||||
<form action="/seeker/create_request" method="post" class="mb-4">
|
||||
<label for="item_uuid">Item UUID:</label>
|
||||
<input type="text" id="item_uuid" name="item_uuid" placeholder="e.g. a1b2c3d4-e5f6-7890-1234-567890abcdef" required pattern="[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}">
|
||||
|
||||
<label for="description">Description:</label>
|
||||
<textarea id="description" name="description" rows="3" required></textarea>
|
||||
|
||||
<label for="contact_info">Contact Info:</label>
|
||||
<input type="text" id="contact_info" name="contact_info" required>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Create Request</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Finder Profile Section #}
|
||||
{% if role == 'finder' %}
|
||||
<div class="section">
|
||||
<h2 class="mt-4">Find an Item</h2>
|
||||
<form action="/finder/find_item" method="post" class="mb-4">
|
||||
<label for="find_item_uuid">Item UUID:</label>
|
||||
<input type="text" id="find_item_uuid" name="item_uuid" placeholder="e.g. a1b2c3d4-e5f6-7890-1234-567890abcdef" required pattern="[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}">
|
||||
<button type="submit" class="btn btn-primary">Find Item</button>
|
||||
</form>
|
||||
|
||||
{% if found_item %}
|
||||
<div class="found-item-card" role="alert">
|
||||
<h3>Found Item Details:</h3>
|
||||
<p><strong>UUID:</strong> <code id="found-uuid">{{ found_item.uuid }}</code> <button class="btn btn-secondary btn-sm" onclick="copyToClipboard('found-uuid')">Copy</button></p>
|
||||
<p><strong>Description:</strong> {{ found_item.description }}</p>
|
||||
<p><strong>Contact Seeker:</strong> {{ found_item.contact_info }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h2 class="mt-4">Request Insight from a Ghostlink</h2>
|
||||
{% if finder_insight_request_as_finder %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h3>Your Current Insight Request:</h3>
|
||||
<p><strong>To Ghostlink:</strong> {{ finder_insight_request_as_finder.ghostlink_username }}</p>
|
||||
<p><strong>Status:</strong> {{ finder_insight_request_as_finder.status }}</p>
|
||||
{% if finder_insight_request_as_finder.status == 'accepted' and finder_insight_request_as_finder.suggested_insight_uuid %}
|
||||
<p><strong>Suggested Insight UUID:</strong> <code id="suggested-insight-uuid">{{ finder_insight_request_as_finder.suggested_insight_uuid }}</code> <button class="btn btn-secondary btn-sm" onclick="copyToClipboard('suggested-insight-uuid')">Copy</button></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>You currently have no active insight requests. Request one from a Ghostlink:</p>
|
||||
<form action="/finder/request_insight" method="post" class="mb-4">
|
||||
<label for="ghostlink_username">Ghostlink Username:</label>
|
||||
<input type="text" id="ghostlink_username" name="ghostlink_username" required>
|
||||
<button type="submit" class="btn btn-primary">Request Insight</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{# Ghostlink Profile Section #}
|
||||
{% if role == 'ghostlink' %}
|
||||
<div class="section">
|
||||
<h2 class="mt-4">Your Insights</h2>
|
||||
{% if ghostlink_insights %}
|
||||
<ul class="list-group">
|
||||
{% for insight in ghostlink_insights %}
|
||||
<li class="list-group-item">
|
||||
<strong>UUID:</strong> <code id="insight-uuid-{{ loop.index0 }}">{{ insight.uuid }}</code> <button class="btn btn-secondary btn-sm" onclick="copyToClipboard('insight-uuid-{{ loop.index0 }}')">Copy</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>You have no insights yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<h3 class="mt-4">Create New Insight</h3>
|
||||
<form action="/ghostlink/create_insight" method="post" class="mb-4">
|
||||
<label for="insight_uuid">Insight UUID:</label>
|
||||
<input type="text" id="insight_uuid" name="insight_uuid" placeholder="e.g. d1c2b3a4-f5e6-9876-5432-10fedcba9876" required pattern="[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}">
|
||||
<button type="submit" class="btn btn-primary">Create Insight</button>
|
||||
</form>
|
||||
<p><a href="/ghostlink/requests" class="button">View Finder Requests</a></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function copyToClipboard(elementId) {
|
||||
var copyText = document.getElementById(elementId);
|
||||
var textArea = document.createElement("textarea");
|
||||
textArea.value = copyText.innerText || copyText.textContent;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand("copy");
|
||||
textArea.remove();
|
||||
|
||||
var button = document.querySelector('#' + elementId + ' + .btn');
|
||||
var originalButtonText = button.innerText;
|
||||
button.innerText = 'Copied!';
|
||||
setTimeout(function() {
|
||||
button.innerText = originalButtonText;
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
36
darkbazaar/src/templates/register.html
Executable file
36
darkbazaar/src/templates/register.html
Executable file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Регистрация</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
{% include "header.html" %}
|
||||
<div class="container">
|
||||
<h1>Register</h1>
|
||||
{% if error %}
|
||||
<p class="alert alert-danger">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form action="/users/register" method="post">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
|
||||
<label for="role">Role:</label>
|
||||
<select id="role" name="role">
|
||||
<option value="seeker">Seeker</option>
|
||||
<option value="finder">Finder</option>
|
||||
<option value="ghostlink">Ghostlink</option>
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Register" class="btn btn-primary">
|
||||
</form>
|
||||
|
||||
<p class="mt-3">Already have an account? <a href="/login">Login</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
59
darkbazaar/src/templates/relationships.html
Executable file
59
darkbazaar/src/templates/relationships.html
Executable file
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Взаимодействия</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
{% include "header.html" %}
|
||||
<div class="container">
|
||||
<h1>Requests for Ghostlink</h1>
|
||||
<p>Your role: <strong>{{ user.role }}</strong></p>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user.role == 'ghostlink' %}
|
||||
<div class="section">
|
||||
<h2 class="mt-4">Requests from Finders</h2>
|
||||
{% if ghostlink_requests %}
|
||||
<div class="list-group">
|
||||
{% for req in ghostlink_requests %}
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
Request from <strong>{{ req.finder_username }}</strong> (Status: <span class="request-status">{{ req.status }}</span>)
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
{% if req.status == 'pending' %}
|
||||
<form action="/ghostlink/accept_request" method="post" class="me-2">
|
||||
<input type="hidden" name="finder_username" value="{{ req.finder_username }}">
|
||||
<button type="submit" class="btn btn-success btn-sm">Accept</button>
|
||||
</form>
|
||||
<form action="/ghostlink/reject_request" method="post">
|
||||
<input type="hidden" name="finder_username" value="{{ req.finder_username }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Reject</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ req.status }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>You have no pending requests from Finders.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>Access denied: This page is only for Ghostlinks to manage requests.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
21
darkbazaar/src/templates/succesful_reject.html
Executable file
21
darkbazaar/src/templates/succesful_reject.html
Executable file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Запрос отклонён!</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
{% include "header.html" %}
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h1>Request Rejected!</h1>
|
||||
<p>You have successfully rejected the insight request.</p>
|
||||
<p class="mt-4">
|
||||
<a href="/profile" class="btn btn-secondary">Back to Profile</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
21
darkbazaar/src/templates/successful_accept.html
Executable file
21
darkbazaar/src/templates/successful_accept.html
Executable file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title> Запрос принят!</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
{% include "header.html" %}
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h1>Request Accepted!</h1>
|
||||
<p>You have successfully accepted the insight request and shared an insight with the Finder.</p>
|
||||
<p class="mt-4">
|
||||
<a href="/profile" class="btn btn-secondary">Back to Profile</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
23
darkbazaar/src/templates/successful_request.html
Executable file
23
darkbazaar/src/templates/successful_request.html
Executable file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Запрос отправлен!</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
{% include "header.html" %}
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h1>Request Sent!</h1>
|
||||
<div class="alert alert-success" role="alert">
|
||||
<p>You have successfully sent a request for an insight to the Ghostlink.</p>
|
||||
</div>
|
||||
<p class="mt-4">
|
||||
<a href="/profile" class="btn btn-secondary">Back to Profile</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user