mirror of
https://github.com/DarrylNixon/ghostforge
synced 2024-04-22 06:27:20 -07:00
Add markdown page, server-side autocomplete PoC, etc.
This commit is contained in:
parent
9dab48a219
commit
8f6f566b71
15 changed files with 455 additions and 14 deletions
213
ghostforge/ethnicities.py
Normal file
213
ghostforge/ethnicities.py
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
|
||||||
|
NATIONALITIES = (
|
||||||
|
"Afghan",
|
||||||
|
"Albanian",
|
||||||
|
"Algerian",
|
||||||
|
"American",
|
||||||
|
"Andorran",
|
||||||
|
"Angolan",
|
||||||
|
"Antiguans",
|
||||||
|
"Argentinean",
|
||||||
|
"Armenian",
|
||||||
|
"Australian",
|
||||||
|
"Austrian",
|
||||||
|
"Azerbaijani",
|
||||||
|
"Bahamian",
|
||||||
|
"Bahraini",
|
||||||
|
"Bangladeshi",
|
||||||
|
"Barbadian",
|
||||||
|
"Barbudans",
|
||||||
|
"Batswana",
|
||||||
|
"Belarusian",
|
||||||
|
"Belgian",
|
||||||
|
"Belizean",
|
||||||
|
"Beninese",
|
||||||
|
"Bhutanese",
|
||||||
|
"Bolivian",
|
||||||
|
"Bosnian",
|
||||||
|
"Brazilian",
|
||||||
|
"British",
|
||||||
|
"Bruneian",
|
||||||
|
"Bulgarian",
|
||||||
|
"Burkinabe",
|
||||||
|
"Burmese",
|
||||||
|
"Burundian",
|
||||||
|
"Cambodian",
|
||||||
|
"Cameroonian",
|
||||||
|
"Canadian",
|
||||||
|
"Cape Verdean",
|
||||||
|
"Central African",
|
||||||
|
"Chadian",
|
||||||
|
"Chilean",
|
||||||
|
"Chinese",
|
||||||
|
"Colombian",
|
||||||
|
"Comoran",
|
||||||
|
"Congolese",
|
||||||
|
"Costa Rican",
|
||||||
|
"Croatian",
|
||||||
|
"Cuban",
|
||||||
|
"Cypriot",
|
||||||
|
"Czech",
|
||||||
|
"Danish",
|
||||||
|
"Djibouti",
|
||||||
|
"Dominican",
|
||||||
|
"Dutch",
|
||||||
|
"Dutchman",
|
||||||
|
"Dutchwoman",
|
||||||
|
"East Timorese",
|
||||||
|
"Ecuadorean",
|
||||||
|
"Egyptian",
|
||||||
|
"Emirian",
|
||||||
|
"Equatorial Guinean",
|
||||||
|
"Eritrean",
|
||||||
|
"Estonian",
|
||||||
|
"Ethiopian",
|
||||||
|
"Fijian",
|
||||||
|
"Filipino",
|
||||||
|
"Finnish",
|
||||||
|
"French",
|
||||||
|
"Gabonese",
|
||||||
|
"Gambian",
|
||||||
|
"Georgian",
|
||||||
|
"German",
|
||||||
|
"Ghanaian",
|
||||||
|
"Greek",
|
||||||
|
"Grenadian",
|
||||||
|
"Guatemalan",
|
||||||
|
"Guinea-Bissauan",
|
||||||
|
"Guinean",
|
||||||
|
"Guyanese",
|
||||||
|
"Haitian",
|
||||||
|
"Herzegovinian",
|
||||||
|
"Honduran",
|
||||||
|
"Hungarian",
|
||||||
|
"I-Kiribati",
|
||||||
|
"Icelander",
|
||||||
|
"Indian",
|
||||||
|
"Indonesian",
|
||||||
|
"Iranian",
|
||||||
|
"Iraqi",
|
||||||
|
"Irish",
|
||||||
|
"Israeli",
|
||||||
|
"Italian",
|
||||||
|
"Ivorian",
|
||||||
|
"Jamaican",
|
||||||
|
"Japanese",
|
||||||
|
"Jordanian",
|
||||||
|
"Kazakhstani",
|
||||||
|
"Kenyan",
|
||||||
|
"Kittian and Nevisian",
|
||||||
|
"Kuwaiti",
|
||||||
|
"Kyrgyz",
|
||||||
|
"Laotian",
|
||||||
|
"Latvian",
|
||||||
|
"Lebanese",
|
||||||
|
"Liberian",
|
||||||
|
"Libyan",
|
||||||
|
"Liechtensteiner",
|
||||||
|
"Lithuanian",
|
||||||
|
"Luxembourger",
|
||||||
|
"Macedonian",
|
||||||
|
"Malagasy",
|
||||||
|
"Malawian",
|
||||||
|
"Malaysian",
|
||||||
|
"Maldivan",
|
||||||
|
"Malian",
|
||||||
|
"Maltese",
|
||||||
|
"Marshallese",
|
||||||
|
"Mauritanian",
|
||||||
|
"Mauritian",
|
||||||
|
"Mexican",
|
||||||
|
"Micronesian",
|
||||||
|
"Moldovan",
|
||||||
|
"Monacan",
|
||||||
|
"Mongolian",
|
||||||
|
"Moroccan",
|
||||||
|
"Mosotho",
|
||||||
|
"Motswana",
|
||||||
|
"Mozambican",
|
||||||
|
"Namibian",
|
||||||
|
"Nauruan",
|
||||||
|
"Nepalese",
|
||||||
|
"Netherlander",
|
||||||
|
"New Zealander",
|
||||||
|
"Ni-Vanuatu",
|
||||||
|
"Nicaraguan",
|
||||||
|
"Nigerian",
|
||||||
|
"Nigerien",
|
||||||
|
"North Korean",
|
||||||
|
"Northern Irish",
|
||||||
|
"Norwegian",
|
||||||
|
"Omani",
|
||||||
|
"Pakistani",
|
||||||
|
"Palauan",
|
||||||
|
"Panamanian",
|
||||||
|
"Papua New Guinean",
|
||||||
|
"Paraguayan",
|
||||||
|
"Peruvian",
|
||||||
|
"Polish",
|
||||||
|
"Portuguese",
|
||||||
|
"Qatari",
|
||||||
|
"Romanian",
|
||||||
|
"Russian",
|
||||||
|
"Rwandan",
|
||||||
|
"Saint Lucian",
|
||||||
|
"Salvadoran",
|
||||||
|
"Samoan",
|
||||||
|
"San Marinese",
|
||||||
|
"Sao Tomean",
|
||||||
|
"Saudi",
|
||||||
|
"Scottish",
|
||||||
|
"Senegalese",
|
||||||
|
"Serbian",
|
||||||
|
"Seychellois",
|
||||||
|
"Sierra Leonean",
|
||||||
|
"Singaporean",
|
||||||
|
"Slovakian",
|
||||||
|
"Slovenian",
|
||||||
|
"Solomon Islander",
|
||||||
|
"Somali",
|
||||||
|
"South African",
|
||||||
|
"South Korean",
|
||||||
|
"Spanish",
|
||||||
|
"Sri Lankan",
|
||||||
|
"Sudanese",
|
||||||
|
"Surinamer",
|
||||||
|
"Swazi",
|
||||||
|
"Swedish",
|
||||||
|
"Swiss",
|
||||||
|
"Syrian",
|
||||||
|
"Taiwanese",
|
||||||
|
"Tajik",
|
||||||
|
"Tanzanian",
|
||||||
|
"Thai",
|
||||||
|
"Togolese",
|
||||||
|
"Tongan",
|
||||||
|
"Trinidadian or Tobagonian",
|
||||||
|
"Tunisian",
|
||||||
|
"Turkish",
|
||||||
|
"Tuvaluan",
|
||||||
|
"Ugandan",
|
||||||
|
"Ukrainian",
|
||||||
|
"Uruguayan",
|
||||||
|
"Uzbekistani",
|
||||||
|
"Venezuelan",
|
||||||
|
"Vietnamese",
|
||||||
|
"Welsh",
|
||||||
|
"Yemenite",
|
||||||
|
"Zambian",
|
||||||
|
"Zimbabwean",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
gf = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@gf.get("/ethnicities")
|
||||||
|
async def get_ethnicities(search: str) -> List[str]:
|
||||||
|
matching = [x for x in NATIONALITIES if search.lower() in x.lower()]
|
||||||
|
return matching
|
|
@ -120,10 +120,15 @@ async def read_ghost(
|
||||||
.join(User, Ghost.owner_id == User.id)
|
.join(User, Ghost.owner_id == User.id)
|
||||||
.where(Ghost.id == ghost_id)
|
.where(Ghost.id == ghost_id)
|
||||||
)
|
)
|
||||||
ghost = result.scalars().first()
|
ghost = result.first()
|
||||||
if not ghost:
|
if not ghost:
|
||||||
raise HTTPException(status_code=404, detail="No ghost with that ID")
|
raise HTTPException(status_code=404, detail="No ghost with that ID")
|
||||||
data = {"ghost": ghost, "user": current_user, "crumbs": [("ghosts", "/ghosts"), (ghost.id, False)]}
|
data = {
|
||||||
|
"ghost": ghost.Ghost,
|
||||||
|
"owner": ghost.owner_username,
|
||||||
|
"user": current_user,
|
||||||
|
"crumbs": [("ghosts", "/ghosts"), (ghost.Ghost.id, False)],
|
||||||
|
}
|
||||||
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
||||||
return ghost
|
return ghost
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,11 @@ from typing import Callable
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy.engine.row import Row
|
||||||
|
|
||||||
from ghostforge.templates import templates
|
from ghostforge.templates import templates
|
||||||
|
|
||||||
# Original credit: https://github.com/acmpo6ou/fastapi_html_json/blob/master/html_json.py
|
# Adapted from original at https://github.com/acmpo6ou/fastapi_html_json/blob/master/html_json.py
|
||||||
|
|
||||||
|
|
||||||
class HtmlJson:
|
class HtmlJson:
|
||||||
|
@ -50,6 +51,11 @@ class HtmlJson:
|
||||||
result = result.dict()
|
result = result.dict()
|
||||||
elif isinstance(result, list):
|
elif isinstance(result, list):
|
||||||
result = {"data": result[:]}
|
result = {"data": result[:]}
|
||||||
|
elif isinstance(result, Row):
|
||||||
|
tmp = {}
|
||||||
|
for k in result.keys():
|
||||||
|
tmp[k] = result[k]
|
||||||
|
result = tmp
|
||||||
|
|
||||||
if hasattr(request.state, "ghostforge"):
|
if hasattr(request.state, "ghostforge"):
|
||||||
for key, value in request.state.ghostforge.items():
|
for key, value in request.state.ghostforge.items():
|
||||||
|
|
10
ghostforge/markdown/about.md
Normal file
10
ghostforge/markdown/about.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
# Security Decisions
|
||||||
|
Setting up the application on a self-hosted server for maximum privacy
|
||||||
|
|
||||||
|
# Updating
|
||||||
|
Best practices for keeping the application and your data secure and up to date.
|
||||||
|
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
Troubleshooting common issues and errors in the application
|
51
ghostforge/markdown/guidebook.md
Normal file
51
ghostforge/markdown/guidebook.md
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# Guidebook
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
In today's digital age, privacy is more important than ever. With multinational corporations collecting massive amounts of data on their customers (and even non-customers), many people are necessarily turning to privacy-centric tools to protect their digital identities. <kbd>ghostforge</kbd> is a full-featured false persona management application. Essentially, it's a self-hosted open source platform designed to help users manage, plan, and or track their activity while masquerading as someone other than themselves – a ghost.
|
||||||
|
|
||||||
|
With ghostforge, users can be used for a variety of purposes, from maintaining privacy while browsing the internet to more complex operations like digital activism, red teaming, investigative journalism, and more. The platform provides a secure and customizable way to manage sensitive information related to each ghost, such as passwords, credit cards, and contact information, all while encouraging strict operational security to prevent compromise. In this guidebook, we will explore the various features of ghostforge, discuss best practices for maintaining operational security and privacy, and examine potential legal and ethical considerations surrounding the use of ghosts.
|
||||||
|
|
||||||
|
## Privacy-First Internet Use
|
||||||
|
In today's world of hyper-connectedness, online privacy has become increasingly important. With websites and ad tech companies constantly tracking and collecting user data, individuals are at risk of having their personal information compromised or sold to third-party advertisers without their consent. Additionally, cybercrime and organized criminals often take advantage of online security weaknesses to steal personal information and perpetrate fraud.
|
||||||
|
|
||||||
|
One way to combat this is through privacy-first internet use, which involves using tools and techniques to protect personal data and online identity. Anonymous browsing is a key component of this, allowing users to browse the internet without revealing identifying information such as IP address or location. Tools like the Tor Browser and VPNs can be used to achieve anonymous browsing, providing increased security for online browsing and helping individuals avoid unwanted tracking and profiling by ad tech companies. On top of this, while signing up for internet-based services, associating accounts with a ghost instead of a true identity can help hide your collective internet activity from interested parties. ghostforge provides a safe place to store credentials and payment information for such services, enabling a true point of demarcation between your true identity and each individual ghost.
|
||||||
|
|
||||||
|
Secure communication is also an important element of privacy-first internet use. Encrypted communication tools like Signal, Telegram, and ProtonMail provide secure channels for messaging and email correspondence, protecting users from prying eyes and safeguarding sensitive information. These tools help to ensure that communications remain private and cannot be accessed by third parties, providing individuals with much-needed peace of mind. Likewise, ghostforge does not provide a secure messaging apparatus, but provides a secure storage facility for credentials for such services.
|
||||||
|
|
||||||
|
## How to be a Good Ghost
|
||||||
|
Creating and managing ghosts (or, "ghosting") is a delicate balance between believability and operational security. While it is important to create a believable persona, it is equally important to ensure that the data associated with the ghost cannot be traced back to the true user.
|
||||||
|
|
||||||
|
By following these best practices, users can create realistic ghost profiles that can be used for various purposes without risking exposure. However, it's important to note that proper operational security measures should always be taken to prevent leaks or mistakes that could compromise the authenticity of the persona.
|
||||||
|
|
||||||
|
#### Start with a basic persona
|
||||||
|
When creating a ghost, it's important to start with a basic persona that reflects the characteristics of individuals within a specific demographic or age group. It's important to tailor the persona to the intended purpose and select attributes that are common for that age group.
|
||||||
|
|
||||||
|
#### Research thoroughly and be patient
|
||||||
|
Research is essential in creating an authentic and believable ghost profile. As a rule of thumb, avoid using information from personal experiences and instead research the characteristics, preferences, and interests of individuals within the ghost demographic. Social media websites like Facebook, Twitter, and LinkedIn can help in gathering data while avoiding suspicions by creating numerous accounts on the social media sites.
|
||||||
|
|
||||||
|
Social media sites and many other web services rightly have false identity detection mitigations in place to hamper the ability for accounts to be created arbitrarily. In some cases, these mitigations can be based on risk analysis of initial user activity, but are more often supplemented by the use of third party ad technology services. It can be difficult to sufficiently build an advertisement identity for a ghost in a short period of time, so be patient and consider developing multiple ghosts at once in parallel but in segregated environments (e.g., different VPNs, VMs)
|
||||||
|
|
||||||
|
#### Maintain consistency
|
||||||
|
Ensure consistency in all aspects of the ghost profile, including name, age, geographic location, occupation, interests, and education. The persona should be carefully crafted to make it difficult for anyone to realize that it is fake. Supplementary information should be appropriate for that demographic – a 21-year-old bartender is less likely to have a Ph.D. and international assets, whereas it may be more believable for them to have hundreds of dollars in cryptocurrencies and a bevy of inactive social media accounts.
|
||||||
|
|
||||||
|
#### Use appropriate identifiers, services, and configurations
|
||||||
|
A key aspect of creating a believable ghost profile is to select usernames and service providers that are appropriate to the persona. For example, names and email providers that align with the demographic, culture, or language will give the ghost more credibility and reduce the likelihood of detection. Likewise, international ghosts that primarily use a foreign language might be predisposed to a more popular search engine in their locality and would be less likely to immediately set their language to English.
|
||||||
|
|
||||||
|
#### Financial information
|
||||||
|
When it's legal to prepare and operate financial assets and liabilities like credit cards with a ghost, ensure that the details match the ghost profile's overall financial situation while still maintaining strong operational security. It's important to consider which types of financial services are common for the demographic and tailor the profile accordingly.
|
||||||
|
|
||||||
|
## Legality of Ghosts
|
||||||
|
The use of false personas (ghosts) raises important legal and ethical questions around when, where, and how they may be used. Use of web services often necessitate agreement with end-user license agreements (EULAs), which govern how those users may access and use the service. These agreements often contain clauses prohibiting the use of false identities or personas, and violating these terms can result in the suspension or termination of the user's account. However, the legality of using false identities on web services is a complex issue, with laws differing across jurisdictions.
|
||||||
|
|
||||||
|
In major countries like the United States, using false personas for web services is generally not considered illegal. However, when it comes to banking and financial transactions, using false identities can be viewed as fraudulent and can easily result in prosecution under local or federal law. It is important that individuals using false identities for financial purposes understand the potential legal risks they may be taking on and take necessary steps to protect themselves.
|
||||||
|
|
||||||
|
It is important for users of ghostforge to understand that they are responsible for following the laws and regulations of their jurisdiction when using the software. The developers of ghostforge are not responsible for any activity, legal or not, carried out using the platform and cannot be held liable for any consequences resulting from deliberate misuse of the software. It remains the responsibility of the users to use software responsibly and ensure their own compliance with their own relevant laws and regulations.
|
||||||
|
|
||||||
|
## Managing your Credentials
|
||||||
|
Managing passwords and other sensitive information is an essential part of maintaining security and remains essential when creating and managing ghosts. While ghostforge is not intended to function as a dynamic password manager, users should still use sane and modern methods to securely generate and manage passwords. One option is to use a trusted password manager, such as Bitwarden, KeePass, or 1Password, in conjunction with ghostforge. This approach can help ensure password security while keeping consistent historical tracking for each ghost for any and all password changes.
|
||||||
|
|
||||||
|
The ghostforge interface provides functonality to securely generate and manage passwords using the current NIST guidelines. NIST recommends using lengthy rather than overly complex passwords, as longer phrases are harder to crack through brute force attacks. By choosing long, unique phrases for each ghost profile, users can significantly reduce the likelihood of their passwords being guessed or brute-forced.
|
||||||
|
|
||||||
|
One of the advantages of using ghostforge for password management is that it maintains the complete history of password changes for each ghost profile, which can be useful when identifying which password was in use at a snapshot in time. It is important to keep the passwords of each ghost up-to-date and change them in cases of compromise to maintain security.
|
||||||
|
|
||||||
|
In summary, while ghostforge may not currently function as a dynamic password manager, it offers robust password management features and may eventually be integrated with trusted password managers. It is our recommendation that users follow the current best practice guidelines for password decisions to ensure the highest level of security for their ghost profiles.
|
33
ghostforge/pages.py
Normal file
33
ghostforge/pages.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi import Depends
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
from ghostforge.templates import templates
|
||||||
|
from ghostforge.users import get_current_user
|
||||||
|
from markdown import markdown
|
||||||
|
|
||||||
|
gf = APIRouter()
|
||||||
|
|
||||||
|
markdown_dir = Path("ghostforge/markdown/")
|
||||||
|
|
||||||
|
|
||||||
|
def render_markdown(md_file):
|
||||||
|
with open(markdown_dir / md_file, "r") as f:
|
||||||
|
return markdown(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
@gf.get("/guidebook", response_class=HTMLResponse)
|
||||||
|
async def guidebook_page(request: Request, current_user=Depends(get_current_user(optional=True))):
|
||||||
|
if not current_user:
|
||||||
|
return HTTPException(status_code=401, detail="Not authenticated")
|
||||||
|
data = {
|
||||||
|
"title": "Guidebook",
|
||||||
|
"html": render_markdown("guidebook.md"),
|
||||||
|
"user": current_user,
|
||||||
|
"crumbs": [("research", False), ("guidebook", False)],
|
||||||
|
}
|
||||||
|
return templates.TemplateResponse("pages.html", data | {"request": request})
|
|
@ -10,7 +10,9 @@ from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from ghostforge.db import create_db_and_tables
|
from ghostforge.db import create_db_and_tables
|
||||||
from ghostforge.db import User
|
from ghostforge.db import User
|
||||||
|
from ghostforge.ethnicities import gf as gf_ethnicities
|
||||||
from ghostforge.ghosts import gf as gf_ghosts
|
from ghostforge.ghosts import gf as gf_ghosts
|
||||||
|
from ghostforge.pages import gf as gf_pages
|
||||||
from ghostforge.templates import templates
|
from ghostforge.templates import templates
|
||||||
from ghostforge.users import fastapi_users
|
from ghostforge.users import fastapi_users
|
||||||
from ghostforge.users import get_current_user
|
from ghostforge.users import get_current_user
|
||||||
|
@ -24,6 +26,8 @@ from ghostforge.users import web_backend
|
||||||
gf = FastAPI()
|
gf = FastAPI()
|
||||||
gf.mount("/static", StaticFiles(directory="ghostforge/static"), name="static")
|
gf.mount("/static", StaticFiles(directory="ghostforge/static"), name="static")
|
||||||
gf.include_router(gf_ghosts)
|
gf.include_router(gf_ghosts)
|
||||||
|
gf.include_router(gf_pages)
|
||||||
|
gf.include_router(gf_ethnicities)
|
||||||
|
|
||||||
gf.include_router(fastapi_users.get_auth_router(jwt_backend), prefix="/auth/jwt", tags=["auth"])
|
gf.include_router(fastapi_users.get_auth_router(jwt_backend), prefix="/auth/jwt", tags=["auth"])
|
||||||
gf.include_router(fastapi_users.get_auth_router(web_backend), prefix="/auth/cookie", tags=["auth"])
|
gf.include_router(fastapi_users.get_auth_router(web_backend), prefix="/auth/cookie", tags=["auth"])
|
||||||
|
|
|
@ -6,7 +6,7 @@ function switchTab(tab) {
|
||||||
x[i].style.display = "none";
|
x[i].style.display = "none";
|
||||||
}
|
}
|
||||||
for (i = 0; i < a.length; i++) {
|
for (i = 0; i < a.length; i++) {
|
||||||
if (a[i].id.endsWith(tab)) {
|
if (a[i].id.startsWith("Tab") && a[i].id.endsWith(tab)) {
|
||||||
a[i].classList.add("active");
|
a[i].classList.add("active");
|
||||||
} else {
|
} else {
|
||||||
a[i].classList.remove("active");
|
a[i].classList.remove("active");
|
||||||
|
|
|
@ -27,7 +27,6 @@ templates.env.globals["gf_navbar"] = {
|
||||||
"Manage Users": "/manage",
|
"Manage Users": "/manage",
|
||||||
},
|
},
|
||||||
"Meta": {
|
"Meta": {
|
||||||
"About GhostForge": "/about",
|
|
||||||
"System Logs": "/logs",
|
"System Logs": "/logs",
|
||||||
"Logout": "/logout",
|
"Logout": "/logout",
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,12 +3,13 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column col-12">
|
<div class="column col-12">
|
||||||
<h1><span class="text-light bg-dark ghost_view_title">{% block title %}
|
<h1><span class="text-light bg-dark ghost_view_title">
|
||||||
{{
|
{% block title %}
|
||||||
ghost.first_name }} {{
|
{{ ghost.first_name }}
|
||||||
ghost.middle_name
|
{{ ghost.middle_name }}
|
||||||
}} {{
|
{{ ghost.last_name }}
|
||||||
ghost.last_name }}{% endblock title %}</span>
|
{% endblock title %}
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h5>[age] year old American Male</h5>
|
<h5>[age] year old American Male</h5>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Browse Ghosts{% endblock title %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{{ url_for('static', path='/js/jquery-3.7.0.min.js') }}"></script>
|
<script src="{{ url_for('static', path='/js/jquery-3.7.0.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', path='/js/colReorder.dataTables.min.js') }}"></script>
|
<script src="{{ url_for('static', path='/js/colReorder.dataTables.min.js') }}"></script>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Ethnicities
|
<!-- <label class="form-label">Ethnicities
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<select class="form-select form-inline" multiple>
|
<select class="form-select form-inline" multiple>
|
||||||
<option>German</option>
|
<option>German</option>
|
||||||
|
@ -23,5 +23,114 @@
|
||||||
<option>Chinese</option>
|
<option>Chinese</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</label> -->
|
||||||
|
<label class="form-label">Ethnicities
|
||||||
|
<div class="form-autocomplete">
|
||||||
|
<div class="form-autocomplete-input form-input" id="ethnicity_chips">
|
||||||
|
<input class="form-input" type="text" id="ethnicities">
|
||||||
|
</div>
|
||||||
|
<ul class="menu" id="ethnicities_menu" style="display: none;">
|
||||||
|
<li class="menu-item">
|
||||||
|
<a href="#">
|
||||||
|
<div class="tile tile-centered">
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Get the input element
|
||||||
|
const input = document.getElementById("ethnicities");
|
||||||
|
|
||||||
|
// Get the menu element
|
||||||
|
const menu = document.getElementById('ethnicities_menu');
|
||||||
|
const menuItems = menu.querySelectorAll("#ethnicities_menu li.menu-item");
|
||||||
|
|
||||||
|
// Create an event listener for input changes
|
||||||
|
input.addEventListener("input", async () => {
|
||||||
|
// Get the current input value
|
||||||
|
const value = input.value;
|
||||||
|
|
||||||
|
// Clear the existing menu items
|
||||||
|
menu.innerHTML = "";
|
||||||
|
|
||||||
|
// Reset the menu items with original list
|
||||||
|
menu.append(...menuItems);
|
||||||
|
|
||||||
|
// If the input is blank, do not show the autocomplete list
|
||||||
|
if (!value.trim()) {
|
||||||
|
menu.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an AJAX request to the backend
|
||||||
|
const response = await fetch(`/ethnicities?search=${value}`);
|
||||||
|
const ethnicities = await response.json();
|
||||||
|
|
||||||
|
// Add a list item for each ethnicity that matches the search
|
||||||
|
for (const ethnicity of ethnicities) {
|
||||||
|
const tile = document.createElement("div");
|
||||||
|
tile.classList.add("tile", "tile-centered");
|
||||||
|
tile.innerHTML = `
|
||||||
|
<div class="tile-content">${ethnicity}</div>
|
||||||
|
`;
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.setAttribute("href", "#");
|
||||||
|
link.appendChild(tile);
|
||||||
|
const listItem = document.createElement("li");
|
||||||
|
listItem.classList.add("menu-item");
|
||||||
|
listItem.appendChild(link);
|
||||||
|
menu.appendChild(listItem);
|
||||||
|
}
|
||||||
|
menu.style.display = "block";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an event listener for clicks on the menu items
|
||||||
|
menu.addEventListener("click", (event) => {
|
||||||
|
// Check if the target is a menu item
|
||||||
|
if (event.target.closest(".menu-item")) {
|
||||||
|
// Get the selected ethnicity
|
||||||
|
const selected = event.target.closest(".menu-item").querySelector(".tile-content").textContent;
|
||||||
|
|
||||||
|
// Create a chip element
|
||||||
|
const chip = document.createElement("div");
|
||||||
|
chip.classList.add("chip");
|
||||||
|
chip.textContent = selected;
|
||||||
|
|
||||||
|
// Add a close button
|
||||||
|
const closeButton = document.createElement("a");
|
||||||
|
closeButton.classList.add("btn", "btn-clear");
|
||||||
|
closeButton.setAttribute("aria-label", "Close");
|
||||||
|
closeButton.setAttribute("role", "button");
|
||||||
|
closeButton.addEventListener("click", () => {
|
||||||
|
chip.remove();
|
||||||
|
});
|
||||||
|
chip.appendChild(closeButton);
|
||||||
|
|
||||||
|
// Insert the chip before the input element
|
||||||
|
input.before(chip);
|
||||||
|
|
||||||
|
// Clear the input text
|
||||||
|
input.value = "";
|
||||||
|
|
||||||
|
// Close the autocomplete menu
|
||||||
|
menu.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an event listener for clicks on the clear buttons
|
||||||
|
const clearButtons = document.querySelectorAll("#ethnicity_chips .btn-clear");
|
||||||
|
for (const clearButton of clearButtons) {
|
||||||
|
clearButton.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.target.closest(".chip").remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<a href="#" onclick="switchTab('Acquisitions')" class="profile_tab_link" id="TabAcquisitions">Acquisitions</a>
|
<a href="#" onclick="switchTab('Acquisitions')" class="profile_tab_link" id="TabAcquisitions">Acquisitions</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="tab-item">
|
<li class="tab-item">
|
||||||
<a href="#" onclick="switchTab('Meta')" class="profile_tab_link" id="TabMeta">Meta</a>
|
<a href="#" onclick="switchTab('History')" class="profile_tab_link" id="TabHistory">History</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form>
|
<form>
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
<div id="History" class="profile_tab" style="display:none">
|
<div id="History" class="profile_tab" style="display:none">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title h5">Meta</div>
|
<div class="card-title h5">History</div>
|
||||||
<div class="card-subtitle text-gray">"now where was i..."</div>
|
<div class="card-subtitle text-gray">"now where was i..."</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">History is very important</div>
|
<div class="card-body">History is very important</div>
|
||||||
|
|
7
ghostforge/templates/pages.html
Normal file
7
ghostforge/templates/pages.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{{ html | safe }}
|
||||||
|
{% endblock content %}
|
|
@ -23,6 +23,7 @@ dependencies = [
|
||||||
"fastapi-users==11.0.0",
|
"fastapi-users==11.0.0",
|
||||||
"fastapi-users-db-sqlmodel==0.3.0",
|
"fastapi-users-db-sqlmodel==0.3.0",
|
||||||
"sqlmodel==0.0.8",
|
"sqlmodel==0.0.8",
|
||||||
|
"markdown==3.4.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|
Loading…
Reference in a new issue