chore(frontend,backend): add migrations; file cleanup; add redux reducer for dashboard state

This commit is contained in:
jerlendds
2023-10-28 07:30:29 -06:00
parent aba6749c9d
commit 7d5fa49a3a
133 changed files with 3054 additions and 5166 deletions

View File

@@ -1,73 +0,0 @@
PROJECT_NAME=osintbuddy
ENVIRONMENT=production
BASE_URL=http://localhost:8000
##########################################
# Error reporting! (TODO: Wait for sentry to finish implementing their fixes for Python 3.11.4)
# Remove if you don't want application errors
# reported to the project maintainers :(
##########################################
SENTRY_DSN=https://c5f217ca357c468cbb7cfe663318018f@o567628.ingest.sentry.io/4505363615711232
##########################################
# Port configuration
##########################################
FRONTEND_PORT=3000
BACKEND_PORT=8000
REDIS_PORT=6379
FLOWER_PORT=5555
##########################################
# S3/minio configuration (todo: add option for seaweedfs for a prod version eventually)
##########################################
MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD=minio
##########################################
# PostgreSQL configuration
##########################################
POSTGRES_IMAGE=postgres:15.4
POSTGRES_PORT=5432
POSTGRES_SERVER=db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_DB=app
PGDATA=/var/lib/postgresql/data/pgdata
##########################################
# Scylla,JanusGraph,Elastic configuration
##########################################
SCYLLADATA=/var/lib/scylla
SDB_REST_PORT=10000
CQL_PORT=9042
THRIFT_PORT=9160
INTERNODE_PORT=7000
INTERNODE_ONE_PORT=7001
JMX_PORT=7199
SOLR_PORT=8983
JANUS_IMAGE=janusgraph/janusgraph:1.0.0-rc2
JANUSGRAPH_URL=ws://janus:8182
##########################################
# Worker configuration
##########################################
CELERY_BROKER_URL=redis://redis:6379/0
# CELERY_BROKER_URL=redis://redis:6379//
CELERY_BACKEND=redis://redis:6379/1
CELERY_ENABLE_UTC=true
FLOWER_BASIC_AUTH=admin:password
CLEARLY_PORT=12223
##########################################
# FastAPI configuration
##########################################
BACKEND_CORS_ORIGINS=["https://api.YOUR_WEBSITE.com","https://YOUR_WEBSITE.com","http://YOUR_WEBSITE.com","http://localhost:3000","http://localhost","http://local.host:3000","*"]
# Run: `openssl rand -hex 32` and replace the below with your output
SECRET_KEY=d7c612d43c083f72f98a5ed6d3dc275516f0ac6ebdcc5fdb8c563a29cd114199
##########################################
# Development
##########################################
VENV_PATH=venv/bin

5
.gitignore vendored
View File

@@ -1,7 +1,4 @@
.vscode
.env
venv
.venv
docs.osintbuddy.com
data
elastic
.venv

View File

@@ -29,11 +29,13 @@
can be explored step-by-step. An easy-to-use plugin system allows any
Python developer to quickly integrate new data sources.
</p>
> 🚧 *Work in progress*
<br/>
![ob-demo](https://github.com/jerlendds/osintbuddy/assets/29207058/9a22223f-7477-4268-be85-46b94dd875bb)
---
</p>

View File

@@ -0,0 +1,58 @@
"""projects to graphs
Revision ID: 5260b5f19250
Revises: d51c30bacade
Create Date: 2023-10-24 18:08:13.190355
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '5260b5f19250'
down_revision = 'd51c30bacade'
branch_labels = None
depends_on = None
def upgrade():
op.create_table('graphs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.UUID(), nullable=True),
sa.Column('name', sa.String(), nullable=False),
sa.Column('description', sa.String(length=512), nullable=True),
sa.Column('is_favorite', sa.Boolean(), nullable=False),
sa.Column('last_seen', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('updated', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('created', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_graphs_uuid'), 'graphs', ['uuid'], unique=False)
op.drop_index('ix_projects_uuid', table_name='projects')
op.drop_table('project_entities')
op.drop_table('projects')
def downgrade():
op.create_table('project_entities',
sa.Column('project_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('entity_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['entity_id'], ['entities.id'], name='project_entities_entity_id_fkey'),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], name='project_entities_project_id_fkey'),
sa.PrimaryKeyConstraint('project_id', 'entity_id', name='project_entities_pkey')
)
op.create_table('projects',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('uuid', sa.UUID(), autoincrement=False, nullable=True),
sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('description', sa.VARCHAR(length=512), autoincrement=False, nullable=True),
sa.Column('updated', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.Column('created', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True),
sa.Column('is_favorite', sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.Column('last_seen', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name='projects_pkey')
)
op.create_index('ix_projects_uuid', 'projects', ['uuid'], unique=False)
op.drop_index(op.f('ix_graphs_uuid'), table_name='graphs')
op.drop_table('graphs')

View File

@@ -0,0 +1,62 @@
"""add username user
Revision ID: 9a20076792f7
Revises: 5260b5f19250
Create Date: 2023-10-28 01:03:22.221486
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '9a20076792f7'
down_revision = '5260b5f19250'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('user', sa.Column('username', sa.String(length=80), nullable=False))
op.alter_column('user', 'full_name',
existing_type=sa.VARCHAR(),
nullable=False)
op.alter_column('user', 'is_active',
existing_type=sa.BOOLEAN(),
nullable=False)
op.alter_column('user', 'is_superuser',
existing_type=sa.BOOLEAN(),
nullable=False)
op.alter_column('user', 'modified',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=False,
existing_server_default=sa.text('now()'))
op.alter_column('user', 'created',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=False,
existing_server_default=sa.text('now()'))
op.drop_index('ix_user_id', table_name='user')
op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=False)
def downgrade():
op.drop_index(op.f('ix_user_username'), table_name='user')
op.create_index('ix_user_id', 'user', ['id'], unique=False)
op.alter_column('user', 'created',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=True,
existing_server_default=sa.text('now()'))
op.alter_column('user', 'modified',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=True,
existing_server_default=sa.text('now()'))
op.alter_column('user', 'is_superuser',
existing_type=sa.BOOLEAN(),
nullable=True)
op.alter_column('user', 'is_active',
existing_type=sa.BOOLEAN(),
nullable=True)
op.alter_column('user', 'full_name',
existing_type=sa.VARCHAR(),
nullable=True)
op.drop_column('user', 'username')

View File

@@ -0,0 +1,51 @@
"""add disabled user
Revision ID: fab5d9c099df
Revises: 9a20076792f7
Create Date: 2023-10-28 01:13:44.024665
"""
import os
from alembic import op
import sqlalchemy as sa
from sqlalchemy_utils import database_exists, create_database, drop_database
# revision identifiers, used by Alembic.
revision = 'fab5d9c099df'
down_revision = '9a20076792f7'
branch_labels = None
depends_on = None
def get_url(db_name: str = None):
user = os.getenv("POSTGRES_USER", "postgres")
password = os.getenv("POSTGRES_PASSWORD", "password")
server = os.getenv("POSTGRES_SERVER", "db")
db = os.getenv("POSTGRES_DB", "app")
if isinstance(db_name, str) and db_name is not None:
db = db_name
return f"postgresql://{user}:{password}@{server}/{db}"
def upgrade():
op.add_column('user', sa.Column('disabled', sa.Boolean(), nullable=False))
# Casdoor supports PostgreSQL...
engine = sa.create_engine(get_url(db_name='casdoor'))
if not database_exists(engine.url):
create_database(engine.url)
create_user_sql = sa.text((
f"CREATE USER {sa.quoted_name(os.getenv('CASDOOR_PG_USER', 'casdoor'), False)} "
"WITH SUPERUSER PASSWORD :database_password")).bindparams(
database_password=os.getenv("CASDOOR_PG_PASSWORD", "casdoorpassword")
).compile(compile_kwargs={"literal_binds": True})
conn = engine.connect()
conn.execute(create_user_sql)
conn.commit()
def downgrade():
op.drop_column('user', 'disabled')
# https://casdoor.org/docs/basic/server-installation/#configure-database
engine = sa.create_engine(get_url(db_name='casdoor'))
if database_exists(engine.url):
drop_database(engine.url)

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter, Header, Depends
from app.api.api_v1.endpoints import graphs, nodes, entities, users, login, scans
from app.api.api_v1.endpoints import graphs, nodes, entities, login, scans
api_router = APIRouter()
@@ -9,4 +9,3 @@ api_router.include_router(scans.router, tags=["Scans"])
api_router.include_router(entities.router, tags=["Entities"])
api_router.include_router(graphs.router, tags=["Graphs"])
api_router.include_router(login.router, tags=["login"])
api_router.include_router(users.router, tags=["users"])

View File

@@ -1,5 +1,6 @@
import datetime
from uuid import UUID
from typing import Annotated
import json
import ujson
from fastapi import (
@@ -21,8 +22,9 @@ log = get_logger("api_v1.endpoints.entities")
router = APIRouter(prefix="/entities")
@router.get('/{entity_uuid}', operation_id="get_entity")
@router.get("/{entity_uuid}")
async def get_entity(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
entity_uuid: str,
db: Session = Depends(deps.get_db),
):
@@ -30,11 +32,9 @@ async def get_entity(
return entities
@router.post(
'',
operation_id="create_entity"
)
@router.post("")
async def create_entity(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
entity: schemas.PostEntityCreate,
db: Session = Depends(deps.get_db)
):
@@ -45,11 +45,9 @@ async def create_entity(
))
@router.put(
'/{entity_id}',
operation_id="update_entity_by_uuid"
)
async def update_entity(
@router.put("/{entity_id}")
async def update_entity_by_uuid(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
entity_id: str,
obj_in: schemas.EntityBase,
db: Session = Depends(deps.get_db)
@@ -59,11 +57,9 @@ async def update_entity(
return entity
@router.get(
'',
operation_id="get_entities",
)
async def get_many_entites_by_favorite(
@router.get("")
async def get_entities(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 100,
@@ -87,10 +83,10 @@ async def get_many_entites_by_favorite(
@router.delete(
'/{entity_id}',
operation_id="delete_entity"
"/{entity_id}",
)
async def delete_entity(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
entity_id: str,
db: Session = Depends(deps.get_db),
):
@@ -100,8 +96,9 @@ async def delete_entity(
raise HTTPException(status_code=422, detail='entity_id is a required field')
@router.put('/{entity_id}/favorite', operation_id="update_favorite_entity_uuid")
async def update_entity_favorite(
@router.put("/{entity_id}/favorite")
async def update_favorite_entity_uuid(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
entity_id: str,
is_favorite: bool = False,
db: Session = Depends(deps.get_db),

View File

@@ -1,6 +1,6 @@
import uuid
import asyncio
from typing import List, Any, Annotated
import boto3
from fastapi import (
APIRouter,
@@ -14,108 +14,105 @@ from sqlalchemy.orm import Session
from app.api import deps
from app import crud, schemas
from app.core.logger import get_logger
from app.db.janus import ProjectGraphConnection
from app.db.janus import ProjectGraphConnection, janus_create_db
log = get_logger("api_v1.endpoints.graphs")
router = APIRouter(prefix="/graphs")
@router.get('/{graph_id}', operation_id="get_graph")
async def get_project(
@router.get(
'/{graph_id}',
response_model=schemas.Graph
)
async def get_graph(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
graph_id: str,
db: Session = Depends(deps.get_db),
):
graph_project = crud.projects.get_by_uuid(
graph = crud.graphs.get_by_uuid(
db=db,
uuid=graph_id
)
return {"graph": graph_project}
return graph
@router.put('/{graph_id}/favorite', operation_id="update_favorite_graph_uuid")
async def get_project(
@router.put('/{graph_id}/favorite')
async def update_favorite_graph_uuid(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
graph_id: str,
is_favorite: bool = False,
db: Session = Depends(deps.get_db),
):
db_obj = crud.projects.get_by_uuid(
db_obj = crud.graphs.get_by_uuid(
db=db,
uuid=graph_id
)
updated_graph = crud.projects.update_favorite_by_uuid(db, db_obj=db_obj, is_favorite=is_favorite)
updated_graph = crud.graphs.update_favorite_by_uuid(db, db_obj=db_obj, is_favorite=is_favorite)
return updated_graph
@router.get('', operation_id="get_graphs")
async def get_project(
@router.get(
"",
response_model=schemas.GraphsList
)
async def get_graphs(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 100,
is_favorite: bool = False
is_favorite: bool = False,
):
if limit > 50:
limit = 50
db_projects = crud.projects.get_many_by_favorites(
db_graphs = crud.graphs.get_many_by_favorites(
db=db,
skip=skip,
limit=limit,
is_favorite=is_favorite
)
return {"projects": db_projects, "count": crud.projects.count_by_favorites(db, is_favorite)[0][0]}
return {
"graphs": db_graphs,
"count": crud.graphs.count_by_favorites(db, is_favorite)[0][0]
}
@router.post('', operation_id="create_graph")
async def create_project(
name: str,
@router.post(
'',
response_model=schemas.Graph
)
async def create_graph(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
obj_in: schemas.GraphCreate,
db: Session = Depends(deps.get_db),
s3 = Depends(deps.get_s3),
description: str = '',
):
project_uuid = uuid.uuid4().hex
create_project_graph = f"""
map = new HashMap<>()
map.put('storage.backend', 'cql')
map.put('storage.hostname', 'sdb:9042')
map.put('index.search.backend', 'solr')
map.put('index.search.solr.mode', 'http')
map.put('index.search.solr.http-urls', 'http://index:8983/solr')
map.put('graph.graphname', 'project_{project_uuid}')
ConfiguredGraphFactory.createConfiguration(new MapConfiguration(map))
ConfiguredGraphFactory.open('project_{project_uuid}')
"""
obj_in = schemas.ProjectCreate(
name=name,
description=description,
uuid=project_uuid
)
new_project = crud.projects.create(db=db, obj_in=obj_in)
cluster = await Cluster.open(
asyncio.get_event_loop(),
**{'hosts': ['janus'], 'port': 8182}
)
try:
log.info(s3.list_buckets())
obj_out = crud.graphs.create(db=db, obj_in=obj_in)
cluster = await Cluster.open(
asyncio.get_event_loop(),
**{'hosts': ['janus'], 'port': 8182}
)
client = await cluster.connect(hostname='janus')
await client.submit(create_project_graph)
except GremlinServerError as e:
log.error(e)
finally:
await cluster.close()
return new_project
await client.submit(janus_create_db(obj_out.uuid.hex))
return obj_out
except (Exception, GremlinServerError) as error:
log.error(error)
raise HTTPException(status_code=422, detail='error')
@router.delete('', operation_id = "delete_graph")
async def delete_project(
@router.delete('')
async def delete_graph(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
uuid: str,
db: Session = Depends(deps.get_db),
):
if uuid:
crud.projects.remove_by_uuid(db=db, uuid=uuid)
crud.graphs.remove_by_uuid(db=db, uuid=uuid)
else:
raise HTTPException(status_code=422, detail='UUID is a required field')
@router.get('/{graph_id}/stats', operation_id="get_graph_stats")
async def get_unique_graph_labels(
@router.get('/{graph_id}/stats')
async def get_graph_stats(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
graph_id: str,
db: Session = Depends(deps.get_db)
):
@@ -123,25 +120,25 @@ async def get_unique_graph_labels(
raise HTTPException(status_code=422, detail='graph_id is a required field')
async with ProjectGraphConnection(graph_id) as g:
unique_entities = await g.V().label().dedup().toList()
total_entites = await g.V().count().toList()
total_entities = await g.V().count().toList()
total_relations = await g.E().count().toList()
unique_entity_counts = {"series": [], "labels": []}
unique_oute_counts = {"series": [], "labels": []}
unique_out_edge_counts = {"series": [], "labels": []}
for entity in unique_entities:
entity_count = await g.V().hasLabel(entity).count().toList()
unique_entity_counts["series"].append(entity_count[0])
unique_entity_counts["labels"].append(entity)
oute_count = await g.V().hasLabel(entity).outE().count().toList()
unique_oute_counts['series'].append(oute_count[0])
unique_oute_counts['labels'].append(entity)
out_edge_count = await g.V().hasLabel(entity).outE().count().toList()
unique_out_edge_counts['series'].append(out_edge_count[0])
unique_out_edge_counts['labels'].append(entity)
return {
"entities": unique_entities,
"total_entities": total_entites[0],
"total_entities": total_entities[0],
"total_relations": total_relations[0],
"unique_entity_counts": unique_entity_counts,
"entity_oute_counts": unique_oute_counts
"entity_oute_counts": unique_out_edge_counts
}

View File

@@ -1,10 +1,10 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi import APIRouter, Body, Request, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from fastapi.responses import JSONResponse
from app import crud, schemas
from app.api import deps
from app.core import security
@@ -17,101 +17,29 @@ from app.utils import (
verify_password_reset_token,
)
log = get_logger("api_v1.endpoints.login")
router = APIRouter()
def get_login_token(db: Session, username: str, password: str) -> schemas.Token or HTTPException:
"""
OAuth2 compatible token login, get an access token for future requests
"""
user = crud.user.authenticate(
db, email=username, password=password
)
if not user:
raise HTTPException(
status_code=400,
detail="Incorrect email or password, please try again",
)
elif not crud.user.is_active(user):
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
return {
"token": security.create_access_token(
user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}
@router.get("/get-account", response_class=JSONResponse)
async def get_account(request: Request, user=Depends(deps.get_user_from_session)):
sdk = request.app.state.CASDOOR_SDK
user_data = await sdk.get_user(user["name"])
return user_data
@router.post(
"/login/access-token",
response_model=schemas.Token,
operation_id="login_access_token"
)
def login_access_token(
db: Session = Depends(deps.get_db),
form_data: OAuth2PasswordRequestForm = Depends()
) -> Any:
token: dict or HTTPException = get_login_token(
db,
form_data.username,
form_data.password
)
return token
@router.post("/sign-in", response_class=JSONResponse)
async def post_signin(code: str, request: Request):
state = request.query_params.get("state")
sdk = request.app.state.CASDOOR_SDK
token = await sdk.get_oauth_token(code)
user = sdk.parse_jwt_token(token.get("access_token", None))
request.session["obUser"] = user
return {"success": True}
@router.post(
"/password-recovery/{email}",
response_model=schemas.Msg,
operation_id="password_recovery_email"
)
def recover_password(email: str, db: Session = Depends(deps.get_db)) -> Any:
"""
Password Recovery
"""
user = crud.user.get_by_email(db, email=email)
if not user:
raise HTTPException(
status_code=404,
detail="The user with this username does not exist in the system.",
)
password_reset_token = generate_password_reset_token(email=email)
send_reset_password_email(
email_to=user.email, email=email, token=password_reset_token
)
return {"msg": "Password recovery email sent"}
@router.post(
"/reset-password/",
response_model=schemas.Msg,
operation_id="reset_password"
)
def reset_password(
token: str = Body(...),
new_password: str = Body(...),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Reset password
"""
email = verify_password_reset_token(token)
if not email:
raise HTTPException(status_code=400, detail="Invalid token")
user = crud.user.get_by_email(db, email=email)
if not user:
raise HTTPException(
status_code=404,
detail="The user with this username does not exist in the system.",
)
elif not crud.user.is_active(user):
raise HTTPException(status_code=400, detail="Inactive user")
hashed_password = get_password_hash(new_password)
user.hashed_password = hashed_password
db.add(user)
db.commit()
return {"msg": "Password updated successfully"}
@router.post("/sign-out", response_class=JSONResponse)
async def post_signout(request: Request):
del request.session["obUser"]
return {"success": True}

View File

@@ -1,6 +1,6 @@
import asyncio
from contextlib import asynccontextmanager
from typing import List, Callable, Tuple, Any, AsyncIterator
from typing import List, Callable, Tuple, Any, AsyncIterator, Annotated
from fastapi import (
APIRouter,
WebSocket,
@@ -38,9 +38,9 @@ async def fetch_node_transforms(plugin_label):
@router.get(
"/refresh",
operation_id="refresh_plugins"
)
async def refresh_plugins(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
db: Session = Depends(deps.get_db)
):
Registry.plugins = []
@@ -53,9 +53,11 @@ async def refresh_plugins(
@router.get(
"/transforms",
operation_id="get_entity_transforms"
)
async def get_node_transforms(label: str):
async def get_entity_transforms(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
label: str
):
if transforms := await fetch_node_transforms(label):
return {
"type": label,
@@ -69,9 +71,11 @@ async def get_node_transforms(label: str):
@router.post(
'/',
operation_id="create_graph_entity"
)
async def get_entity_from_drop(node: schemas.CreateNode):
async def create_graph_entity(
user: Annotated[schemas.CasdoorUser, Depends(deps.get_user_from_session)],
node: schemas.CreateNode
):
plugin = await Registry.get_plugin(plugin_label=node.label)
if plugin:
blueprint = plugin.blueprint()

View File

@@ -34,7 +34,7 @@ async def get_scan_machines(
@router.delete("")
async def delete_project(
async def delete_scan_project(
id: int,
db: Session = Depends(deps.get_db),
):

View File

@@ -1,155 +0,0 @@
from typing import Any, List
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.encoders import jsonable_encoder
from pydantic.networks import EmailStr
from sqlalchemy.orm import Session
from app import crud, models, schemas
from app.api import deps
from app.core.config import settings
from app.utils import send_new_account_email
router = APIRouter(prefix="/users")
@router.get("/", response_model=List[schemas.User], operation_id="get_users")
def read_users(
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 100,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Retrieve users.
"""
users = crud.user.get_multi(db, skip=skip, limit=limit)
return users
@router.post("/", response_model=schemas.User, operation_id="create_user")
def create_user(
*,
db: Session = Depends(deps.get_db),
user_in: schemas.UserCreate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Create new user.
"""
user = crud.user.get_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system.",
)
user = crud.user.create(db, obj_in=user_in)
if settings.EMAILS_ENABLED and user_in.email:
send_new_account_email(
email_to=user_in.email,
username=user_in.full_name,
password=user_in.password,
)
return user
@router.put("/me", response_model=schemas.User, operation_id="update_user_me")
def update_user_me(
*,
db: Session = Depends(deps.get_db),
password: str = Body(None),
full_name: str = Body(None),
email: EmailStr = Body(None),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Update own user.
"""
current_user_data = jsonable_encoder(current_user)
user_in = schemas.UserUpdate(**current_user_data)
if password is not None:
user_in.password = password
if full_name is not None:
user_in.full_name = full_name
if email is not None:
user_in.email = email
user = crud.user.update(db, db_obj=current_user, obj_in=user_in)
return user
@router.get("/me", response_model=schemas.User, operation_id="read_user")
def read_user_me(
db: Session = Depends(deps.get_db),
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Get current user.
"""
return current_user
@router.post("/open", response_model=schemas.User, operation_id="create_user_open")
def create_user_open(
*,
db: Session = Depends(deps.get_db),
password: str = Body(...),
email: EmailStr = Body(...),
full_name: str = Body(...),
) -> Any:
"""
Create new user without the need to be logged in.
"""
if not settings.USERS_OPEN_REGISTRATION:
raise HTTPException(
status_code=403,
detail="Open user registration is forbidden on this server",
)
user = crud.user.get_by_email(db, email=email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this username already exists in the system",
)
user_in = schemas.UserCreate(password=password, email=email, full_name=full_name)
user = crud.user.create(db, obj_in=user_in)
return user
@router.get("/{user_id}", response_model=schemas.User, operation_id="read_user_by_id")
def read_user_by_id(
user_id: int,
current_user: models.User = Depends(deps.get_current_active_user),
db: Session = Depends(deps.get_db),
) -> Any:
"""
Get a specific user by id.
"""
user = crud.user.count(db, id=user_id)
if user == current_user:
return user
if not crud.user.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
return user
@router.put("/{user_id}", response_model=schemas.User, operation_id="update_user")
def update_user(
*,
db: Session = Depends(deps.get_db),
user_id: int,
user_in: schemas.UserUpdate,
current_user: models.User = Depends(deps.get_current_active_superuser),
) -> Any:
"""
Update a user.
"""
user = crud.user.count(db, id=user_id)
if not user:
raise HTTPException(
status_code=404,
detail="The user with this username does not exist in the system",
)
user = crud.user.update(db, db_obj=user, obj_in=user_in)
return user

View File

@@ -1,9 +1,10 @@
from typing import Generator
from typing import Generator, Annotated
from contextlib import contextmanager
import boto3
from jose import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from fastapi import Depends, HTTPException, status, Security
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from starlette.requests import Request
from sqlalchemy.orm import Session
from pydantic import ValidationError
import undetected_chromedriver as uc
@@ -14,12 +15,24 @@ from app.core import security
from app.db.session import SessionLocal
from app import crud, models, schemas
from app.core.config import settings
from app.core.logger import get_logger
log = get_logger("api.deps")
reusable_oauth2 = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/signin",
)
async def get_user_from_session(request: Request):
try:
sdk = request.app.state.CASDOOR_SDK
user_jwt = request.headers.get("authorization", "").replace("Bearer ", "")
user = sdk.parse_jwt_token(user_jwt)
return schemas.CasdoorUser.validate(user)
except Exception as e:
log.error(e)
raise HTTPException(status_code=401, detail="Unauthorized")
def get_db() -> Generator:
try:
@@ -29,9 +42,7 @@ def get_db() -> Generator:
db.close()
def get_s3(
# current_user: models.User = Depends(get_current_active_user),
):
def get_s3():
kwargs = {
"endpoint_url": "http://s3:8333",
"aws_access_key_id": "accessKey1",
@@ -41,43 +52,6 @@ def get_s3(
return s3
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(reusable_oauth2)
) -> models.User:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = schemas.TokenPayload(**payload)
except (jwt.JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
user = crud.user.get(db, id=token_data.sub)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
def get_current_active_user(
current_user: models.User = Depends(get_current_user),
) -> models.User:
if not crud.user.is_active(current_user):
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
def get_current_active_superuser(
current_user: models.User = Depends(get_current_user),
) -> models.User:
if not crud.user.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
return current_user
@contextmanager
def get_driver() -> Generator[Session, None, None]:
"""

View File

@@ -0,0 +1,46 @@
import os
from casdoor import AsyncCasdoorSDK
certificate = '''-----BEGIN CERTIFICATE-----
MIIE2TCCAsGgAwIBAgIDAeJAMA0GCSqGSIb3DQEBCwUAMCYxDjAMBgNVBAoTBWFk
bWluMRQwEgYDVQQDDAtjZXJ0X3Q4MDVwdzAeFw0yMzEwMjgwNTAzMTNaFw00MzEw
MjgwNTAzMTNaMCYxDjAMBgNVBAoTBWFkbWluMRQwEgYDVQQDDAtjZXJ0X3Q4MDVw
dzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALuJO1TnSbUua1DgA9rW
1iJSSSsZCcBcrUEe46DY9EZH63K7d4gJdrT4lOPr/5cRrp7YWJ3/F0DnPMDlC4RH
aU8T2UQLNbP0DIqZdO8Y1QBueiuBPLdxj3k8TSn1RL+7r3ACm4g2m0VVldMAXzaX
RRqmuRetBz4RdfwAq7NZFs/mYmoLF5iFoWoniaG26Rsntk5Fxo5L0fgqjMKf6oQo
7WQ/hdtxmqrWggabON2oUdoi+guB1kGlJky6FXHmFKqLNsPjaXfwqRLqGSAkowGl
vJJYLQvLN9JuYxqcyyDT/wE7FiuLU/CT3mH9QTBPhzHrsklXGYdZdoG0UGimcTUG
0wubHaGDsarYdC9vFGFywyEe41ZBHLZt7RhzO/Mu3jKrP7KK31MYXwZo4bsKmgj1
UwirO4YSbw9ODko8RLSyR3Om5t3/XNH3VSla8VnkzDriHicCOTKmAnf+XdNgRiBd
fFb8BQdrVZ/Ua+lOQl0QlOsqzCMP2tjqmdQ4z1kwAT7QggA+iE8GC5GvOxw4BA4p
h6kQHPOX66c7NspvC++UdpxTm9NJxITU6ZMcAS9ZwSd8bk/jkMrHbjLggH1FIm6T
oJ7FYm/v31ZxXjr/TiM66lMHAfwmxoxirbOsu66IMWHCNbIMxYySmTNLE9vGSN9F
+x4PbBxyZtFfgtUZmoWrKNY9AgMBAAGjEDAOMAwGA1UdEwEB/wQCMAAwDQYJKoZI
hvcNAQELBQADggIBALt9bWjQgstTLcQSMUIRacKZ0/bP6OD6d4pk2WPYjrYJHWfp
BoRN7zW/zu7ztr4SpGD/CwC4Nib/vplcMDXNi2xDxK6sAOcwzSUD5IaAZpoFdluT
cYAXJ75ezXRhycl+aI2/R0effQiUDXp+9FmT6ENQ2NAw6JL29ih+ugDXh1/JJ+oV
gnZYEk47vyJLxOOCGNww4u75iRMC73DBO2rHkwXfR9IQxx1bPK+8CwqtO7ZQKV8v
THxlcrEbMWDRLOLrIuAOUwVOq725dLscFXPJuMbCNUjgsxFCL+MA5yFot8nf4DTW
yHfnd+lio0g83c6Xb3QaJtW5NJvIeeZ7Q66fh24kM7XiZZs1kzNYT2wTWTrJGyvm
6fT8SM5hy6xuoGmzEj6SiKiQHSzjd/gv4K2Wv3F4I+Zigxvx8LCHT3ETH18xPOB6
IJyOJjUDGY3o/T8DhVj0Fw5NHa/ujjx893rcdiYdiQnWZ9oBwDtlHDDtdEaGKrAr
OvJDjhG2J2X5IbXWc5fuTVoxhnjSuT/x2+3IBuWETKvjbno9/fTcUl9o2Y0ejTvf
iKFQ3TJfwBi51Pd3skzEJCE/C5Vzw03YrwO2Clx9iZRAv+drNn/vM++ch9+Ph3jQ
tyCMlYmLO/0DCB/PgEjqQyScFRH8mcMU2DYM5Z+PThlUvTtTMrXlZcIShcgi
-----END CERTIFICATE-----'''
class Config:
CASDOOR_SDK = AsyncCasdoorSDK(
endpoint='http://casdoor:8080',
client_id='057e00c722b1f415196b',
client_secret='17e86c4cd14e97ea968e3a08c9c8efcb8fdebf02',
certificate=certificate,
org_name='org_ob',
application_name='application_ob',
)
REDIRECT_URI = 'http://localhost:3000/callback'
SECRET_TYPE = 'filesystem'
SECRET_KEY = os.urandom(24)

View File

@@ -2,13 +2,14 @@ import os
import secrets
from typing import Any, Dict, List, Optional, Union
from fastapi import FastAPI
from fastapi.routing import APIRoute
from pydantic import (
AnyHttpUrl,
HttpUrl,
PostgresDsn,
validator
)
from pydantic_settings import BaseSettings
@@ -22,8 +23,8 @@ class Settings(BaseSettings):
API_V1_STR: str = "/api/v1"
SECRET_KEY: str = secrets.token_urlsafe(32)
ADMIN_BACKEND_SECRET_KEY: str = secrets.token_urlsafe(32)
# 60 minutes * 24 hours * 8 days = 8 days
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
# 60 minutes * 6 hours
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 6
BACKEND_CORS_ORIGINS: List[str] = os.getenv('BACKEND_CORS_ORIGINS')
@validator("BACKEND_CORS_ORIGINS", pre=True)
@@ -60,9 +61,29 @@ class Settings(BaseSettings):
JANUSGRAPH_PORT: int = 8182
SENTRY_DSN: str = None
SUPERUSER_EMAIL: str = "admin@example.com"
SUPERUSER_PASSWORD: str = "password"
SUPERUSER_USERNAME: str = "sudo"
SUPERUSER_FULL_NAME: str = "Super Admin"
BACKEND_LOG_LEVEL: str = "info"
UVICORN_HOST: str = "0.0.0.0"
class Config:
case_sensitive = True
settings = Settings()
def use_route_names_as_operation_ids(app: FastAPI) -> None:
"""
Simplify operation IDs so that generated API clients have simpler function
names.
Should be called only after all routes have been added.
"""
for route in app.routes:
if isinstance(route, APIRoute):
route.operation_id = route.name

View File

@@ -13,15 +13,16 @@ ALGORITHM = "HS256"
def create_access_token(
subject: Union[str, Any], expires_delta: timedelta = None
data: dict, expires_delta: timedelta = None
) -> str:
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode = {"exp": expire, "sub": str(subject)}
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

View File

@@ -1,4 +1,4 @@
from app.crud.crud_projects import projects # noqa
from app.crud.crud_graphs import graphs # noqa
from app.crud.crud_entities import entities # noqa
from app.crud.crud_user import user # noqa
from app.crud.crud_scan_machines import scan_machine

View File

@@ -1,15 +1,15 @@
from typing import List
from app.crud.base import CRUDBase, ModelType
from app.models.projects import Projects
from app.schemas.projects import ProjectCreate, ProjectUpdate
from app.models.graphs import Graphs
from app.schemas.graphs import GraphCreate, GraphUpdate
from sqlalchemy.orm import Session
from sqlalchemy import func
class CRUDProjects(CRUDBase[
Projects,
ProjectCreate,
ProjectUpdate
class CRUDGraphs(CRUDBase[
Graphs,
GraphCreate,
GraphUpdate
]):
def get_multi_by_user(
self, db: Session, *, skip: int = 0, limit: int = 100
@@ -35,11 +35,11 @@ class CRUDProjects(CRUDBase[
def count_by_favorites(self, db: Session, is_favorite: bool = False) -> int:
return db.query(func.count(self.model.id)).where(self.model.is_favorite == is_favorite).all()
def update_favorite_by_uuid(self, db: Session, db_obj: Projects, is_favorite: bool = False):
def update_favorite_by_uuid(self, db: Session, db_obj: Graphs, is_favorite: bool = False):
setattr(db_obj, 'is_favorite', is_favorite)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
projects = CRUDProjects(Projects)
graphs = CRUDGraphs(Graphs)

View File

@@ -30,6 +30,23 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
db.refresh(db_obj)
return db_obj
def create_superuser(self, db: Session, *, obj_in: UserCreate) -> User:
if isinstance(obj_in, dict):
user_data = obj_in
else:
user_data = obj_in.dict(exclude_unset=True)
if user_data["password"]:
hashed_password = get_password_hash(user_data["password"])
del user_data["password"]
user_data["hashed_password"] = hashed_password
user_data['is_superuser'] = True
db_obj = self.model(**user_data)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
) -> User:
@@ -61,5 +78,8 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
def is_superuser(self, user: User) -> bool:
return user.is_superuser
def is_disabled(self, user: User) -> bool:
return user.disabled
user = CRUDUser(User)

View File

@@ -4,7 +4,7 @@
from app.db.base_class import Base # noqa
from app.models import ( # noqa
User,
Projects,
Graphs,
Proxy_Type,
Proxies,
Entities,

View File

@@ -4,6 +4,7 @@ from osintbuddy import load_plugin, Registry
from app import crud, schemas
from app.core.logger import get_logger
from app.core.config import settings
# make sure all SQL Alchemy models are imported (app.db.base) before initializing DB
from .base import * # noqa
@@ -29,12 +30,10 @@ core_plugins = {
'Whois': 'whois'
}
def load_initial_plugin(db, plugin_mod, plugin_code):
load_plugin(plugin_mod, plugin_code)
plugin = Registry.get_plug(plugin_mod)
print(plugin)
obj_in = schemas.EntityCreate(
label=plugin.label,
author=plugin.author,
@@ -45,7 +44,15 @@ def load_initial_plugin(db, plugin_mod, plugin_code):
return crud.entities.create(db=db, obj_in=obj_in)
def init_db(db: Session) -> None:
users_count = crud.user.count_all(db)[0][0]
if users_count == 0:
log.info('Creating initial system user (superuser)...')
crud.user.create_superuser(db, obj_in=schemas.UserCreate(
email=settings.SUPERUSER_EMAIL,
username=settings.SUPERUSER_USERNAME,
full_name=settings.SUPERUSER_FULL_NAME,
password=settings.SUPERUSER_PASSWORD
))
entity_count = crud.entities.count_all(db)[0][0]
if entity_count < 14:
for plugin_label, plugin_mod in core_plugins.items():

View File

@@ -15,7 +15,7 @@ from app.core.config import settings
@asynccontextmanager
async def ProjectGraphConnection(
project_uuid: str,
graph_uuid: str,
host: str = settings.JANUSGRAPH_HOST,
port: int = settings.JANUSGRAPH_PORT
) -> AsyncIterator[AsyncGraphTraversal]:
@@ -24,11 +24,22 @@ async def ProjectGraphConnection(
**{'hosts': [host], 'port': port}
)
client = await cluster.connect(hostname='janus')
# await client.submit(f'project_{project_uuid}.io(IoCore.graphson()).writeGraph("data.json")')
print(f'connecting traversal: project_{project_uuid}_traversal')
print(f'connecting traversal: graph_{graph_uuid}_traversal')
async with await DriverRemoteConnection.using(
cluster,
{'g': f'project_{project_uuid.replace("-", "")}_traversal'}
{'g': f'graph_{graph_uuid.replace("-", "")}_traversal'}
) as connection:
yield Graph().traversal().withRemote(connection)
janus_create_db = lambda graph_uuid: f"""
map = new HashMap<>()
map.put('storage.backend', 'cql')
map.put('storage.hostname', 'sdb:9042')
map.put('index.search.backend', 'solr')
map.put('index.search.solr.mode', 'http')
map.put('index.search.solr.http-urls', 'http://index:8983/solr')
map.put('graph.graphname', 'graph_{graph_uuid}')
ConfiguredGraphFactory.createConfiguration(new MapConfiguration(map))
ConfiguredGraphFactory.open('graph_{graph_uuid}')
"""

View File

@@ -2,9 +2,12 @@ import logging
from app.db.init_db import init_db
from app.db.session import SessionLocal
from app.core.logger import get_logger
from app import schemas, crud
from app.core.config import settings
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
log = get_logger(__name__)
def init() -> None:

View File

@@ -1,15 +1,19 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.routing import APIRoute
from fastapi.responses import UJSONResponse
from fastapi.openapi.utils import get_openapi
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from fastapi_cache import caches, close_caches
from fastapi_cache.backends.redis import CACHE_KEY, RedisCacheBackend
# import sentry_sdk
# from sentry_sdk.integrations.asyncio import AsyncioIntegration
from osintbuddy import discover_plugins
from app.api.api_v1.api import api_router
from app.core.config import settings
from app.core.config import settings, use_route_names_as_operation_ids
from app.core.casdoor import Config as CasdoorConfig
# if settings.SENTRY_DSN:
# sentry_sdk.init(
@@ -35,6 +39,12 @@ app = FastAPI(
on_shutdown=[on_shutdown]
)
app.include_router(api_router, prefix=settings.API_V1_STR)
@app.get("/status")
def get_status():
return {"status": "ok"}
app.add_middleware(
CORSMiddleware,
allow_origins=settings.BACKEND_CORS_ORIGINS,
@@ -54,6 +64,19 @@ app.add_middleware(
],
)
app.add_middleware(
SessionMiddleware,
secret_key=CasdoorConfig.SECRET_KEY,
)
app.state.CASDOOR_SDK = CasdoorConfig().CASDOOR_SDK
app.state.REDIRECT_URI = CasdoorConfig().REDIRECT_URI
app.state.SECRET_TYPE = CasdoorConfig().SECRET_TYPE
app.state.SECRET_KEY = CasdoorConfig().SECRET_KEY
use_route_names_as_operation_ids(app)
def app_openapi_schema(app):
"""Return openapi_schema. cached."""
@@ -71,11 +94,5 @@ def app_openapi_schema(app):
return openapi_schema
app.include_router(api_router, prefix=settings.API_V1_STR)
app.openapi_schema = app_openapi_schema(app)
# @app.get("/sentry-debug")
# async def trigger_error():
# division_by_zero = 1 / 0
# return division_by_zero

View File

@@ -1,7 +1,5 @@
from .proxy import Proxy_Type, Proxies # noqa
from .projects import ( # noqa
Projects,
)
from .entities import Entities, ProjectEntities # noqa
from .graphs import Graphs # noqa
from .entities import Entities # noqa
from .user import User # noqa
from .scans import Scan_Machines, Scans

View File

@@ -8,14 +8,6 @@ from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
# http://localhost:3000/app/projects - entities are at the second table on the ui
ProjectEntities = Table(
"project_entities",
Base.metadata,
Column('project_id', ForeignKey('projects.id'), primary_key=True),
Column('entity_id', ForeignKey('entities.id'), primary_key=True)
)
# Endpoints that use entities can be found here:
#
# backend/backend/app/app/api/api_v1/endpoints/entities.py

View File

@@ -5,22 +5,16 @@ from sqlalchemy import Column, String, DateTime, Boolean, func
from sqlalchemy.dialects.postgresql import UUID
from app.db.base_class import Base
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.entities import Entities, ProjectEntities
from app.models.entities import Entities
# http://localhost:3000/app/projects - projects are the first table on the ui
# Endpoints for a users projects and their graphs can be found here:
# backend/backend/app/app/api/api_v1/endpoints/projects.py
class Projects(Base):
class Graphs(Base):
id: Mapped[int] = mapped_column(primary_key=True)
uuid: Mapped[UUID] = Column(UUID(as_uuid=True), default=uuid.uuid4, index=True)
name: Mapped[str] = mapped_column(String, nullable=False)
description: Mapped[str] = mapped_column(String(512), nullable=True)
# A project can have many entities and each entity can be for many projects
entities: Mapped[List[Entities]] = relationship(secondary=ProjectEntities)
is_favorite: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
last_seen: Mapped[DateTime] = mapped_column(
@@ -33,10 +27,12 @@ class Projects(Base):
def __repr__(self) -> str:
return (
f"Projects(id={self.id!r}, "
f"Graphs(id={self.id!r}, "
f"uuid={self.uuid!r}, "
f"name={self.name!r}, "
f"description={self.description[64:]!r}, "
f"is_favorite={self.is_favorite!r}, "
f"updated={self.updated!r}, "
f"last_seen={self.last_seen!r}, "
f"created={self.created!r})"
)

View File

@@ -1,18 +1,21 @@
from sqlalchemy import Boolean, Column, Integer, String, DateTime, func
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base_class import Base
class User(Base):
id = Column(Integer, primary_key=True, index=True)
full_name = Column(String, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(80), index=True)
full_name: Mapped[str] = mapped_column(String, index=True)
email: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
hashed_password: Mapped[str] = mapped_column(String, nullable=False)
is_active = Column(Boolean(), default=True)
is_superuser = Column(Boolean(), default=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False)
modified = Column(
disabled: Mapped[bool] = mapped_column(Boolean, default=False)
modified: Mapped[DateTime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
created = Column(DateTime(timezone=True), server_default=func.now())
created: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -1,11 +1,12 @@
from .msg import Msg # noqa
from .api_errors import ErrorDetail, HTTPError # noqa
from .projects import ( # noqa
ProjectCreate,
ProjectUpdate,
Project,
ProjectsListInDB,
ProjectInDB,
from .graphs import ( # noqa
GraphCreate,
GraphUpdate,
Graph,
GraphInDB,
GraphInDBBase,
GraphsList
)
from .node import CreateNode # noqa
from .entities import ( # noqa
@@ -18,12 +19,12 @@ from .entities import ( # noqa
EntityInDB,
PostEntityCreate,
)
from .user import User, UserUpdate, UserCreate, UserBase, UserInDB, UserInDBBase # noqa
from .token import Token, TokenPayload # noqa
from .user import User, UserUpdate, UserCreate, UserBase, UserInDB, UserInDBBase, CasdoorUser # noqa
from .token import Token, TokenPayload, TokenData # noqa
from .scan_machines import (
ScanMachine,
ScanMachineBase,
ScanMachineCreate,
ScanMachineUpdate,
ScanMachineInDB,
)
)

View File

@@ -0,0 +1,47 @@
import datetime
from uuid import UUID
from typing import Optional, List, Union
from pydantic import BaseModel
# Shared properties
class GraphsBase(BaseModel):
name: str
description: Optional[str]
is_favorite: bool = False
# Properties to receive via API on creation
class GraphCreate(GraphsBase):
pass
# Properties to receive via API on update
class GraphUpdate(GraphsBase):
pass
class GraphInDBBase(GraphsBase):
uuid: UUID
class Config:
from_orm = True
# Additional properties to return via API
class Graph(GraphInDBBase):
updated: datetime.datetime
created: datetime.datetime
last_seen: datetime.datetime
# Additional properties stored in DB
class GraphInDB(GraphInDBBase):
id: int
class GraphsList(BaseModel):
graphs: List[Graph]
count: int

View File

@@ -1,45 +0,0 @@
import datetime
from typing import Optional, List
from pydantic import BaseModel
# Cases
# Shared properties
class ProjectsBase(BaseModel):
name: str
description: str
uuid: str
# Properties to receive via API on creation
class ProjectCreate(ProjectsBase):
pass
# is_superuser: bool = False
# Properties to receive via API on update
class ProjectUpdate(ProjectsBase):
pass
class ProjectInDBBase(ProjectsBase):
id: Optional[int] = None
class Config:
from_attributes = True
# Additional properties to return via API
class Project(ProjectInDBBase):
updated: datetime.datetime
created: datetime.datetime
# Additional properties stored in DB
class ProjectInDB(ProjectInDBBase):
pass
class ProjectsListInDB(ProjectInDB):
cases: List[ProjectInDB]

View File

@@ -1,4 +1,4 @@
from typing import Optional
from typing import Optional, List
from pydantic import BaseModel
@@ -10,3 +10,8 @@ class Token(BaseModel):
class TokenPayload(BaseModel):
sub: Optional[int] = None
class TokenData(BaseModel):
username: str | None = None
scopes: List[str] = []

View File

@@ -1,9 +1,44 @@
from uuid import UUID
import datetime
from typing import Optional
from typing import Optional, List
from pydantic import BaseModel, EmailStr
class CasdoorUser(BaseModel):
id: UUID
name: str
displayName: str
createdTime: datetime.datetime
updatedTime: datetime.datetime
sub: UUID
firstName: str
lastName: str
avatar: str
avatarType: str
permanentAvatar: str
email: str
emailVerified: bool
phone: str
countryCode: str
region: str
location: str
bio: str
language: str
isOnline: bool
isAdmin: bool
isForbidden: bool
isDeleted: bool
owner: str = "org_ob"
type: str = "normal-user"
signupApplication: str = "application_ob"
aud: List[str] = []
exp: int
nbf: int
iat: int
jti: str
# Shared properties
class UserBase(BaseModel):
email: Optional[EmailStr] = None
@@ -17,7 +52,7 @@ class UserCreate(UserBase):
email: EmailStr
full_name: str
password: str
# is_superuser: bool = False
username: str = ""
# Properties to receive via API on update

View File

@@ -1,8 +1,42 @@
import os
import sys
import time
import uvicorn
from colorama import just_fix_windows_console
from termcolor import colored
from pyfiglet import figlet_format
just_fix_windows_console()
from app.core.config import settings
def print_startup_message():
print(colored("Introducing...", color="red", attrs=["bold"]))
time.sleep(1)
for line in figlet_format("the", font="small").split('\n'):
print(colored(line, color="blue"))
for line in figlet_format("OSINTBuddy", font='doom',).split('\n'):
print(colored(line, color="blue", attrs=["bold"]))
for line in figlet_format(" project", font="small").split('\n'):
print(colored(line, color="blue"))
print(
colored("Created by:", color="red"),
colored("jerlendds\n\n\n", color="red", attrs=["bold", "underline"]),
)
def main():
print_startup_message()
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=80,
loop="asyncio",
reload=True,
workers=4,
headers=[("server", "app")],
log_level=settings.BACKEND_LOG_LEVEL
)
print(sys.argv[0])
if __name__ == "__main__":
uvicorn.run("app.main:app", host="0.0.0.0", port=80, loop='asyncio', reload=True, workers=4, headers=[('server', 'app')], log_level='info')
main()

View File

@@ -10,7 +10,7 @@ def test_get_access_token(client: TestClient) -> None:
"username": settings.FIRST_SUPERUSER,
"password": settings.FIRST_SUPERUSER_PASSWORD,
}
r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data)
r = client.post(f"{settings.API_V1_STR}/login/token", data=login_data)
tokens = r.json()
assert r.status_code == 200
assert "access_token" in tokens

View File

@@ -15,7 +15,7 @@ def user_authentication_headers(
) -> Dict[str, str]:
data = {"username": email, "password": password}
r = client.post(f"{settings.API_V1_STR}/login/access-token", data=data)
r = client.post(f"{settings.API_V1_STR}/login/token", data=data)
response = r.json()
auth_token = response["access_token"]
headers = {"Authorization": f"Bearer {auth_token}"}

View File

@@ -20,7 +20,7 @@ def get_superuser_token_headers(client: TestClient) -> Dict[str, str]:
"username": settings.FIRST_SUPERUSER,
"password": settings.FIRST_SUPERUSER_PASSWORD,
}
r = client.post(f"{settings.API_V1_STR}/login/access-token", data=login_data)
r = client.post(f"{settings.API_V1_STR}/login/token", data=login_data)
tokens = r.json()
a_token = tokens["access_token"]
headers = {"Authorization": f"Bearer {a_token}"}

View File

@@ -26,13 +26,5 @@ else
echo "There is no script $PRE_START_PATH"
fi
# Start Uvicorn with live reload
# uvicorn service:app --host 0.0.0.0 --port 2222 --header server:app \
# --reload \
# --reload-dir /service/
# uvicorn service:app --host 0.0.0.0 --port 2222 --no-server-header \
# --reload \
# --reload-dir /service/
python3 ./app/start.py "app.main:app"
# exec uvicorn --loop asyncio --workers 4 --reload --reload-dir app --header server:app --host $HOST --port $PORT --log-level $LOG_LEVEL "$APP_MODULE"
python3 ./app/start.py "app.main:app"

View File

@@ -4,11 +4,10 @@ LABEL maintainer="jerlendds <support@forum.osintbuddy.com>"
WORKDIR /app/
ENV PYTHONPATH=/app/
RUN apt-get -y update && apt-get -y install apt-transport-https nmap git wget gnupg curl chromium chromium-driver && \
apt-get clean;
RUN apt-get -y update && apt-get -y install git wget gnupg curl && \
apt-get clean && pip3 install --no-cache-dir --upgrade pip;
COPY requirements.txt /app/requirements.txt
RUN pip3 install --no-cache-dir --upgrade pip && \
pip3 install --no-cache-dir -r /app/requirements.txt
RUN pip3 install --no-cache-dir -r /app/requirements.txt
COPY app/ /app/
COPY osintbuddy-plugins /app/osintbuddy-plugins/

View File

@@ -1,4 +1,7 @@
alembic==1.10.4
casdoor
itsdangerous==2.1.2
sqlalchemy_utils==0.41.1
beautifulsoup4==4.12.2
celery==5.2.7
colorama==0.4.6

View File

@@ -1,40 +1,30 @@
alembic==1.10.4
beautifulsoup4==4.12.2
boto3==1.28.64
casdoor==1.17.0
celery==5.2.7
colorama==0.4.6
cryptography==41.0.4
dataclasses-json==0.5.7
email-validator==2.0.0.post2
emails==0.6
fastapi==0.103.2
fastapi-cache==0.1.0
grequests==0.6.0
httptools==0.5.0
langdetect==1.0.9
numexpr==2.8.4
openai==0.27.6
openapi-schema-pydantic==1.2.4
osintbuddy==0.0.4rc46.post1
# osintbuddy==0.0.4
itsdangerous==2.1.2
passlib==1.7.4
pathspec==0.11.1
pip==23.3
pipdeptree==2.13.0
psycopg2-binary==2.9.6
pydantic-settings==2.0.3
pyfiglet==0.8.post1
PySocks==1.7.1
pytest==7.3.1
python-jose==3.3.0
python-multipart==0.0.6
python3-nmap==1.6.0
redis==4.5.4
sqlalchemy-json==0.7.0
SQLAlchemy-Utils==0.41.1
tenacity==8.2.2
termcolor==2.3.0
ujson==5.8.0
undetected-chromedriver==3.4.7
uvicorn==0.22.0
validators==0.20.0
watchfiles==0.19.0
boto3==1.28.64
gremlinpy @ git+https://github.com/jerlendds/gremlinpy.git@7d3033e6a55ed9cb1f982ec3b58ca233e01c58e3

View File

@@ -1,3 +0,0 @@
#!/usr/bin/env bash
# This package is auto generated please don't edit manually
export VERSION="v0.3.0"

View File

@@ -1,2 +0,0 @@
node_modules
npm-debug.log

View File

@@ -1,24 +1,9 @@
FROM node:18-alpine as build-stage
FROM node:18-alpine
LABEL maintainer="jerlendds <support@forum.osintbuddy.com>"
WORKDIR /app/
COPY package.json yarn.lock ./
RUN yarn
RUN yarn install && ls && pwd
COPY . .
ARG NODE_ENV=${ENVIRONMENT}
ARG REACT_APP_BASE_URL=${BASE_URL}
# TODO: Improve docker file for dev...
# if [ $NODE_ENV=='production' ]; then yarn build
# if [ $NODE_ENV=='development' ]; then yarn start
RUN yarn build
FROM nginx:1.25.0-alpine
COPY --from=build-stage /app/build/ /usr/share/nginx/html
COPY --from=build-stage /app/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD [ "yarn", "start" ]

View File

@@ -3,6 +3,8 @@
"version": "0.3.0",
"private": true,
"dependencies": {
"@axa-fr/oidc-client": "^7.7.2",
"@axa-fr/react-oidc": "^7.7.2",
"@codemirror/lang-python": "^6.1.3",
"@excalidraw/excalidraw": "^0.16.1",
"@headlessui/react": "^1.7.7",
@@ -11,12 +13,13 @@
"@lexical/headless": "^0.12.2",
"@lexical/mark": "^0.12.2",
"@locker/near-membrane-dom": "^0.12.14",
"@open-rpc/client-js": "^1.8.1",
"@reactour/tour": "^3.4.0",
"@reduxjs/toolkit": "^1.9.1",
"@types/katex": "^0.16.3",
"@types/lodash-es": "^4.17.9",
"@uiw/codemirror-theme-tokyo-night": "^4.21.8",
"@uiw/react-codemirror": "^4.21.8",
"@uiw/codemirror-theme-tokyo-night": "^4.21.20",
"@uiw/react-codemirror": "^4.21.20",
"@yaireo/dragsort": "^1.3.1",
"@yaireo/tagify": "^4.17.8",
"axios": "^1.2.1",
@@ -39,6 +42,8 @@
"react-use-websocket": "^4.3.1",
"reactflow": "^11.5.3",
"sigma": "^3.0.0-alpha3",
"swr": "^2.2.4",
"vscode-languageserver-protocol": "^3.17.5",
"y-websocket": "^1.5.0",
"yjs": "^13.6.8",
"yup": "^0.32.11"
@@ -47,8 +52,7 @@
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject",
"swagger:codegen": "rm -f -dr ./src/app/openapi && mkdir ./src/app/openapi && openapi --name obSDK -i http://localhost:8000/api/v1/openapi.json --output ./src/app/openapi --client axios --exportModels true --exportSchemas true --indent 2 --postfixServices v1"
"swagger:codegen": "rm -f -dr ./src/app/openapi && mkdir ./src/app/openapi && openapi --name obSDK --client axios -i http://localhost:8000/api/v1/openapi.json --output ./src/app/openapi --exportModels true --exportSchemas true --indent 2 --postfixServices v1"
},
"eslintConfig": {
"extends": [

View File

@@ -6,12 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0F172A" />
<meta name="description" content="Mine, map, and merge data for novel insights" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Lexend:wght@100;200;300;400;500;600;700;800;900&display=swap&family=Fira+Code&family=Crimson+Text:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap"
rel="stylesheet"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>OSINTBuddy</title>

View File

@@ -22,3 +22,4 @@ interface ApiTransforms {
type: string;
transforms: { label: string; icon: TablerIcon };
}

8
frontend/src/@types/casdoor.d.ts vendored Executable file
View File

@@ -0,0 +1,8 @@
import SDK from 'casdoor-js-sdk';
declare global {
interface Window {
sdk: SDK
}
}

3
frontend/src/@types/globals.d.ts vendored Executable file → Normal file
View File

@@ -3,5 +3,4 @@ declare module '@yaireo/dragsort';
declare type JSONObject = {
[any: string]: any
};
type HexColor = `#${string}`;
type HexColor = `#${string}`;

View File

@@ -19,7 +19,6 @@ interface EditState {
interface ActiveProjectGraph {
id: number;
uuid: string;

View File

@@ -1,5 +1,17 @@
import { BrowserRouter } from 'react-router-dom';
import AppRoutes from '@/routes/AppRoutes';
import SDK from 'casdoor-js-sdk'
import { SdkConfig } from 'casdoor-js-sdk/lib/esm/sdk'
const config: SdkConfig = {
serverUrl: "http://localhost:8080",
clientId: "057e00c722b1f415196b",
organizationName: "org_ob",
appName: "application_ob",
redirectPath: "/callback",
signinPath: "/api/v1/sign-in",
}
window.sdk = new SDK(config)
function App() {
return (

View File

@@ -18,19 +18,16 @@ const api = axios.create({
});
const setHeader = () => {
const user = JSON.parse(localStorage.getItem(LS_USER_AUTH_KEY) || '{}');
if (user && user.token) {
api.defaults.headers.common['Authorization'] = `Bearer ${user.token}`;
return `Bearer ${user.token}`;
const accessToken = sessionStorage.getItem('accessToken')
if (accessToken) {
api.defaults.headers.common['Authorization'] = `Bearer ${sessionStorage.getItem('accessToken')}`;
return accessToken;
}
return null;
};
setHeader();
// TODO: Move all api calls to new SDK
export const sdk = new obSDK({
BASE: BASE_URL
BASE: BASE_URL,
TOKEN: setHeader(),
})
export { BASE_URL, LS_USER_AUTH_KEY, WS_URL, API_PREFIX, setHeader };

View File

@@ -10,43 +10,31 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';
export type { Body_create_user_open } from './models/Body_create_user_open';
export type { Body_login_access_token } from './models/Body_login_access_token';
export type { Body_reset_password } from './models/Body_reset_password';
export type { Body_update_user_me } from './models/Body_update_user_me';
export type { CreateNode } from './models/CreateNode';
export type { EntityBase } from './models/EntityBase';
export type { Graph } from './models/Graph';
export type { GraphCreate } from './models/GraphCreate';
export type { GraphsList } from './models/GraphsList';
export type { HTTPValidationError } from './models/HTTPValidationError';
export type { Msg } from './models/Msg';
export type { PostEntityCreate } from './models/PostEntityCreate';
export type { ScanMachineCreate } from './models/ScanMachineCreate';
export type { Token } from './models/Token';
export type { User } from './models/User';
export type { UserCreate } from './models/UserCreate';
export type { UserUpdate } from './models/UserUpdate';
export type { ValidationError } from './models/ValidationError';
export type { XYPosition } from './models/XYPosition';
export { $Body_create_user_open } from './schemas/$Body_create_user_open';
export { $Body_login_access_token } from './schemas/$Body_login_access_token';
export { $Body_reset_password } from './schemas/$Body_reset_password';
export { $Body_update_user_me } from './schemas/$Body_update_user_me';
export { $CreateNode } from './schemas/$CreateNode';
export { $EntityBase } from './schemas/$EntityBase';
export { $Graph } from './schemas/$Graph';
export { $GraphCreate } from './schemas/$GraphCreate';
export { $GraphsList } from './schemas/$GraphsList';
export { $HTTPValidationError } from './schemas/$HTTPValidationError';
export { $Msg } from './schemas/$Msg';
export { $PostEntityCreate } from './schemas/$PostEntityCreate';
export { $ScanMachineCreate } from './schemas/$ScanMachineCreate';
export { $Token } from './schemas/$Token';
export { $User } from './schemas/$User';
export { $UserCreate } from './schemas/$UserCreate';
export { $UserUpdate } from './schemas/$UserUpdate';
export { $ValidationError } from './schemas/$ValidationError';
export { $XYPosition } from './schemas/$XYPosition';
export { Defaultv1 } from './services/Defaultv1';
export { Entitiesv1 } from './services/Entitiesv1';
export { Graphsv1 } from './services/Graphsv1';
export { Loginv1 } from './services/Loginv1';
export { Nodesv1 } from './services/Nodesv1';
export { Scansv1 } from './services/Scansv1';
export { Usersv1 } from './services/Usersv1';

View File

@@ -1,14 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Body_login_access_token = {
grant_type?: (string | null);
username: string;
password: string;
scope?: string;
client_id?: (string | null);
client_secret?: (string | null);
};

View File

@@ -1,10 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Body_reset_password = {
token: string;
new_password: string;
};

View File

@@ -1,11 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Body_update_user_me = {
password?: string;
full_name?: string;
email?: string;
};

View File

@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Graph = {
name: string;
description: (string | null);
is_favorite?: boolean;
uuid: string;
updated: string;
created: string;
last_seen: string;
};

View File

@@ -3,9 +3,9 @@
/* tslint:disable */
/* eslint-disable */
export type Body_create_user_open = {
password: string;
email: string;
full_name: string;
export type GraphCreate = {
name: string;
description: (string | null);
is_favorite?: boolean;
};

View File

@@ -3,7 +3,10 @@
/* tslint:disable */
/* eslint-disable */
export type Msg = {
status: string;
import type { Graph } from './Graph';
export type GraphsList = {
graphs: Array<Graph>;
count: number;
};

View File

@@ -1,10 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Token = {
token: string;
token_type: string;
};

View File

@@ -1,15 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type User = {
email?: (string | null);
is_active?: (boolean | null);
is_superuser?: boolean;
full_name?: (string | null);
id?: (number | null);
modified: string;
created: string;
};

View File

@@ -1,13 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UserCreate = {
email: string;
is_active?: (boolean | null);
is_superuser?: boolean;
full_name: string;
password: string;
};

View File

@@ -1,13 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UserUpdate = {
email?: (string | null);
is_active?: (boolean | null);
is_superuser?: boolean;
full_name?: (string | null);
hashed_password?: (string | null);
};

View File

@@ -6,23 +6,23 @@ import type { BaseHttpRequest } from './core/BaseHttpRequest';
import type { OpenAPIConfig } from './core/OpenAPI';
import { AxiosHttpRequest } from './core/AxiosHttpRequest';
import { Defaultv1 } from './services/Defaultv1';
import { Entitiesv1 } from './services/Entitiesv1';
import { Graphsv1 } from './services/Graphsv1';
import { Loginv1 } from './services/Loginv1';
import { Nodesv1 } from './services/Nodesv1';
import { Scansv1 } from './services/Scansv1';
import { Usersv1 } from './services/Usersv1';
type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest;
export class obSDK {
public readonly default: Defaultv1;
public readonly entities: Entitiesv1;
public readonly graphs: Graphsv1;
public readonly login: Loginv1;
public readonly nodes: Nodesv1;
public readonly scans: Scansv1;
public readonly users: Usersv1;
public readonly request: BaseHttpRequest;
@@ -39,12 +39,12 @@ export class obSDK {
ENCODE_PATH: config?.ENCODE_PATH,
});
this.default = new Defaultv1(this.request);
this.entities = new Entitiesv1(this.request);
this.graphs = new Graphsv1(this.request);
this.login = new Loginv1(this.request);
this.nodes = new Nodesv1(this.request);
this.scans = new Scansv1(this.request);
this.users = new Usersv1(this.request);
}
}

View File

@@ -1,21 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $Body_create_user_open = {
properties: {
password: {
type: 'string',
isRequired: true,
},
email: {
type: 'string',
isRequired: true,
format: 'email',
},
full_name: {
type: 'string',
isRequired: true,
},
},
} as const;

View File

@@ -1,44 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $Body_login_access_token = {
properties: {
grant_type: {
type: 'any-of',
contains: [{
type: 'string',
pattern: 'password',
}, {
type: 'null',
}],
},
username: {
type: 'string',
isRequired: true,
},
password: {
type: 'string',
isRequired: true,
},
scope: {
type: 'string',
},
client_id: {
type: 'any-of',
contains: [{
type: 'string',
}, {
type: 'null',
}],
},
client_secret: {
type: 'any-of',
contains: [{
type: 'string',
}, {
type: 'null',
}],
},
},
} as const;

View File

@@ -1,18 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $Body_update_user_me = {
properties: {
password: {
type: 'string',
},
full_name: {
type: 'string',
},
email: {
type: 'string',
format: 'email',
},
},
} as const;

View File

@@ -2,45 +2,30 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $User = {
export const $Graph = {
properties: {
email: {
name: {
type: 'string',
isRequired: true,
},
description: {
type: 'any-of',
contains: [{
type: 'string',
format: 'email',
}, {
type: 'null',
}],
isRequired: true,
},
is_active: {
type: 'any-of',
contains: [{
type: 'boolean',
}, {
type: 'null',
}],
},
is_superuser: {
is_favorite: {
type: 'boolean',
},
full_name: {
type: 'any-of',
contains: [{
type: 'string',
}, {
type: 'null',
}],
uuid: {
type: 'string',
isRequired: true,
format: 'uuid',
},
id: {
type: 'any-of',
contains: [{
type: 'number',
}, {
type: 'null',
}],
},
modified: {
updated: {
type: 'string',
isRequired: true,
format: 'date-time',
@@ -50,5 +35,10 @@ export const $User = {
isRequired: true,
format: 'date-time',
},
last_seen: {
type: 'string',
isRequired: true,
format: 'date-time',
},
},
} as const;

View File

@@ -2,31 +2,23 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $UserCreate = {
export const $GraphCreate = {
properties: {
email: {
name: {
type: 'string',
isRequired: true,
format: 'email',
},
is_active: {
description: {
type: 'any-of',
contains: [{
type: 'boolean',
type: 'string',
}, {
type: 'null',
}],
isRequired: true,
},
is_superuser: {
is_favorite: {
type: 'boolean',
},
full_name: {
type: 'string',
isRequired: true,
},
password: {
type: 'string',
isRequired: true,
},
},
} as const;

View File

@@ -2,14 +2,17 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $Body_reset_password = {
export const $GraphsList = {
properties: {
token: {
type: 'string',
graphs: {
type: 'array',
contains: {
type: 'Graph',
},
isRequired: true,
},
new_password: {
type: 'string',
count: {
type: 'number',
isRequired: true,
},
},

View File

@@ -1,12 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $Msg = {
properties: {
status: {
type: 'string',
isRequired: true,
},
},
} as const;

View File

@@ -1,16 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $Token = {
properties: {
token: {
type: 'string',
isRequired: true,
},
token_type: {
type: 'string',
isRequired: true,
},
},
} as const;

View File

@@ -1,44 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $UserUpdate = {
properties: {
email: {
type: 'any-of',
contains: [{
type: 'string',
format: 'email',
}, {
type: 'null',
}],
},
is_active: {
type: 'any-of',
contains: [{
type: 'boolean',
}, {
type: 'null',
}],
},
is_superuser: {
type: 'boolean',
},
full_name: {
type: 'any-of',
contains: [{
type: 'string',
}, {
type: 'null',
}],
},
hashed_password: {
type: 'any-of',
contains: [{
type: 'string',
}, {
type: 'null',
}],
},
},
} as const;

View File

@@ -0,0 +1,24 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CancelablePromise } from '../core/CancelablePromise';
import type { BaseHttpRequest } from '../core/BaseHttpRequest';
export class Defaultv1 {
constructor(public readonly httpRequest: BaseHttpRequest) {}
/**
* Get Status
* @returns any Successful Response
* @throws ApiError
*/
public getStatus(): CancelablePromise<any> {
return this.httpRequest.request({
method: 'GET',
url: '/status',
});
}
}

View File

@@ -54,7 +54,7 @@ export class Entitiesv1 {
}
/**
* Get Many Entites By Favorite
* Get Entities
* @param skip
* @param limit
* @param isFavorite
@@ -81,7 +81,7 @@ export class Entitiesv1 {
}
/**
* Update Entity
* Update Entity By Uuid
* @param entityId
* @param requestBody
* @returns any Successful Response
@@ -127,7 +127,7 @@ export class Entitiesv1 {
}
/**
* Update Entity Favorite
* Update Favorite Entity Uuid
* @param entityId
* @param isFavorite
* @returns any Successful Response

View File

@@ -2,6 +2,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Graph } from '../models/Graph';
import type { GraphCreate } from '../models/GraphCreate';
import type { GraphsList } from '../models/GraphsList';
import type { CancelablePromise } from '../core/CancelablePromise';
import type { BaseHttpRequest } from '../core/BaseHttpRequest';
@@ -10,14 +14,14 @@ export class Graphsv1 {
constructor(public readonly httpRequest: BaseHttpRequest) {}
/**
* Get Project
* Get Graph
* @param graphId
* @returns any Successful Response
* @returns Graph Successful Response
* @throws ApiError
*/
public getGraph(
graphId: string,
): CancelablePromise<any> {
): CancelablePromise<Graph> {
return this.httpRequest.request({
method: 'GET',
url: '/api/v1/graphs/{graph_id}',
@@ -31,7 +35,7 @@ export class Graphsv1 {
}
/**
* Get Project
* Update Favorite Graph Uuid
* @param graphId
* @param isFavorite
* @returns any Successful Response
@@ -57,18 +61,18 @@ export class Graphsv1 {
}
/**
* Get Project
* Get Graphs
* @param skip
* @param limit
* @param isFavorite
* @returns any Successful Response
* @returns GraphsList Successful Response
* @throws ApiError
*/
public getGraphs(
skip?: number,
limit: number = 100,
isFavorite: boolean = false,
): CancelablePromise<any> {
): CancelablePromise<GraphsList> {
return this.httpRequest.request({
method: 'GET',
url: '/api/v1/graphs',
@@ -84,23 +88,19 @@ export class Graphsv1 {
}
/**
* Create Project
* @param name
* @param description
* @returns any Successful Response
* Create Graph
* @param requestBody
* @returns Graph Successful Response
* @throws ApiError
*/
public createGraph(
name: string,
description: string = '',
): CancelablePromise<any> {
requestBody: GraphCreate,
): CancelablePromise<Graph> {
return this.httpRequest.request({
method: 'POST',
url: '/api/v1/graphs',
query: {
'name': name,
'description': description,
},
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
@@ -108,7 +108,7 @@ export class Graphsv1 {
}
/**
* Delete Project
* Delete Graph
* @param uuid
* @returns any Successful Response
* @throws ApiError
@@ -129,7 +129,7 @@ export class Graphsv1 {
}
/**
* Get Unique Graph Labels
* Get Graph Stats
* @param graphId
* @returns any Successful Response
* @throws ApiError

View File

@@ -2,11 +2,6 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Body_login_access_token } from '../models/Body_login_access_token';
import type { Body_reset_password } from '../models/Body_reset_password';
import type { Msg } from '../models/Msg';
import type { Token } from '../models/Token';
import type { CancelablePromise } from '../core/CancelablePromise';
import type { BaseHttpRequest } from '../core/BaseHttpRequest';
@@ -15,40 +10,31 @@ export class Loginv1 {
constructor(public readonly httpRequest: BaseHttpRequest) {}
/**
* Login Access Token
* @param formData
* @returns Token Successful Response
* Get Account
* @returns any Successful Response
* @throws ApiError
*/
public loginAccessToken(
formData: Body_login_access_token,
): CancelablePromise<Token> {
public getAccount(): CancelablePromise<any> {
return this.httpRequest.request({
method: 'POST',
url: '/api/v1/login/access-token',
formData: formData,
mediaType: 'application/x-www-form-urlencoded',
errors: {
422: `Validation Error`,
},
method: 'GET',
url: '/api/v1/get-account',
});
}
/**
* Recover Password
* Password Recovery
* @param email
* @returns Msg Successful Response
* Post Signin
* @param code
* @returns any Successful Response
* @throws ApiError
*/
public passwordRecoveryEmail(
email: string,
): CancelablePromise<Msg> {
public postSignin(
code: string,
): CancelablePromise<any> {
return this.httpRequest.request({
method: 'POST',
url: '/api/v1/password-recovery/{email}',
path: {
'email': email,
url: '/api/v1/sign-in',
query: {
'code': code,
},
errors: {
422: `Validation Error`,
@@ -57,23 +43,14 @@ export class Loginv1 {
}
/**
* Reset Password
* Reset password
* @param requestBody
* @returns Msg Successful Response
* Post Signout
* @returns any Successful Response
* @throws ApiError
*/
public resetPassword(
requestBody: Body_reset_password,
): CancelablePromise<Msg> {
public postSignout(): CancelablePromise<any> {
return this.httpRequest.request({
method: 'POST',
url: '/api/v1/reset-password/',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
url: '/api/v1/sign-out',
});
}

View File

@@ -24,7 +24,7 @@ export class Nodesv1 {
}
/**
* Get Node Transforms
* Get Entity Transforms
* @param label
* @returns any Successful Response
* @throws ApiError
@@ -45,7 +45,7 @@ export class Nodesv1 {
}
/**
* Get Entity From Drop
* Create Graph Entity
* @param requestBody
* @returns any Successful Response
* @throws ApiError

View File

@@ -17,7 +17,7 @@ export class Scansv1 {
* @returns any Successful Response
* @throws ApiError
*/
public createScanMachineApiV1ScansMachinesPost(
public createScanMachine(
requestBody: ScanMachineCreate,
): CancelablePromise<any> {
return this.httpRequest.request({
@@ -38,7 +38,7 @@ export class Scansv1 {
* @returns any Successful Response
* @throws ApiError
*/
public getScanMachinesApiV1ScansMachinesGet(
public getScanMachines(
skip?: number,
limit: number = 50,
): CancelablePromise<any> {
@@ -56,12 +56,12 @@ export class Scansv1 {
}
/**
* Delete Project
* Delete Scan Project
* @param id
* @returns any Successful Response
* @throws ApiError
*/
public deleteProjectApiV1ScansDelete(
public deleteScanProject(
id: number,
): CancelablePromise<any> {
return this.httpRequest.request({

View File

@@ -1,167 +0,0 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Body_create_user_open } from '../models/Body_create_user_open';
import type { Body_update_user_me } from '../models/Body_update_user_me';
import type { User } from '../models/User';
import type { UserCreate } from '../models/UserCreate';
import type { UserUpdate } from '../models/UserUpdate';
import type { CancelablePromise } from '../core/CancelablePromise';
import type { BaseHttpRequest } from '../core/BaseHttpRequest';
export class Usersv1 {
constructor(public readonly httpRequest: BaseHttpRequest) {}
/**
* Read Users
* Retrieve users.
* @param skip
* @param limit
* @returns User Successful Response
* @throws ApiError
*/
public getUsers(
skip?: number,
limit: number = 100,
): CancelablePromise<Array<User>> {
return this.httpRequest.request({
method: 'GET',
url: '/api/v1/users/',
query: {
'skip': skip,
'limit': limit,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Create User
* Create new user.
* @param requestBody
* @returns User Successful Response
* @throws ApiError
*/
public createUser(
requestBody: UserCreate,
): CancelablePromise<User> {
return this.httpRequest.request({
method: 'POST',
url: '/api/v1/users/',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Read User Me
* Get current user.
* @returns User Successful Response
* @throws ApiError
*/
public readUser(): CancelablePromise<User> {
return this.httpRequest.request({
method: 'GET',
url: '/api/v1/users/me',
});
}
/**
* Update User Me
* Update own user.
* @param requestBody
* @returns User Successful Response
* @throws ApiError
*/
public updateUserMe(
requestBody?: Body_update_user_me,
): CancelablePromise<User> {
return this.httpRequest.request({
method: 'PUT',
url: '/api/v1/users/me',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Create User Open
* Create new user without the need to be logged in.
* @param requestBody
* @returns User Successful Response
* @throws ApiError
*/
public createUserOpen(
requestBody: Body_create_user_open,
): CancelablePromise<User> {
return this.httpRequest.request({
method: 'POST',
url: '/api/v1/users/open',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Read User By Id
* Get a specific user by id.
* @param userId
* @returns User Successful Response
* @throws ApiError
*/
public readUserById(
userId: number,
): CancelablePromise<User> {
return this.httpRequest.request({
method: 'GET',
url: '/api/v1/users/{user_id}',
path: {
'user_id': userId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Update User
* Update a user.
* @param userId
* @param requestBody
* @returns User Successful Response
* @throws ApiError
*/
public updateUser(
userId: number,
requestBody: UserUpdate,
): CancelablePromise<User> {
return this.httpRequest.request({
method: 'PUT',
url: '/api/v1/users/{user_id}',
path: {
'user_id': userId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
}

View File

@@ -1,8 +1,10 @@
import { configureStore, ThunkAction, Action, combineReducers } from '@reduxjs/toolkit';
import settings from '@/features/settings/settingsSlice';
import graph from '@/features/graph/graphSlice';
import dashboard from '@/features/dashboard/dashboardSlice';
const reducer = combineReducers({
dashboard,
settings,
graph,
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -5,6 +5,24 @@
@import './tables.css';
@import './charts.css';
@font-face {
font-family: 'Lexend';
src: url('@/assets/fonts/Lexend-Regular.woff2') format('woff2'),
url('@/assets/fonts/Lexend-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('@/assets/fonts/Inter-Regular.woff2') format('woff2'),
url('@/assets/fonts/Inter-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
*,
:after,
:before {
@@ -249,6 +267,6 @@ input[type='file'] {
}
.cm-line span {
@apply !font-code text-sm;
@apply text-sm;
}
/* end react-grid-layout and codemirror */

View File

@@ -1,35 +1,67 @@
// @ts-nocheck
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import "react-grid-layout/css/styles.css";
import { Responsive, WidthProvider } from "react-grid-layout";
import CodeMirror from "@uiw/react-codemirror";
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/outline";
import { tokyoNightInit } from "@uiw/codemirror-theme-tokyo-night";
import CodeMirror, { Extension } from "@uiw/react-codemirror";
import { tags as t } from "@lezer/highlight";
import { python } from "@codemirror/lang-python";
import { Icon } from "@/components/Icons";
import { useParams } from "react-router-dom"
import { useEffectOnce } from "@/components/utils";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useEffectOnce } from "../utils";
import { useAppDispatch, useAppSelector } from "@/app/hooks";
import { selectActiveEntity, setActiveEntityId } from "@/features/dashboard/dashboardSlice";
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/outline";
import { Responsive, WidthProvider } from "react-grid-layout";
import "react-grid-layout/css/styles.css";
import { Icon } from "../Icons";
import { sdk } from "@/app/api";
export const tokyoNightTheme = tokyoNightInit({
settings: {
caret: "#c6c6c6",
background: '#1a1b26',
fontFamily: 'monospace'
},
styles: [{ tag: t.comment, color: "#6272a4" }],
})
export function CodeEditor({ code, setCode, lsp }: JSONObject) {
return (
<>{lsp ? (
<CodeMirror
theme={tokyoNightTheme}
value={code}
onChange={(value) => setCode(value)}
extensions={[python(), lsp]}
/>
) : (
<CodeMirror
theme={tokyoNightTheme}
value={code}
onChange={(value) => setCode(value)}
extensions={[python()]}
/>
)}
</>
)
}
const ResponsiveGridLayout = WidthProvider(Responsive);
export function ResizableHandles({ activeEntity }: JSONObject) {
const [pythonCode, setPythonCode] = useState(activeEntity.source);
export default function EntityEditor({ activeEntity }: JSONObject) {
const dispatch = useAppDispatch()
const [isEntityDraggable, setEntityDraggable] = useState(false);
const [isElementsDraggable, setElementsDraggable] = useState(false);
const [code, setCode] = useState(activeEntity?.source)
useEffect(() => {
setPythonCode(activeEntity.source)
}, [activeEntity])
if (activeEntity?.source) setCode(activeEntity.source)
}, [activeEntity?.source])
return (
<>
<ResponsiveGridLayout
compactType={null}
className="w-auto flex h-full z-[99] absolute"
rowHeight={60}
rowHeight={56}
maxRows={50}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 20, md: 20, sm: 20, xs: 18, xxs: 16 }}
@@ -37,7 +69,7 @@ export function ResizableHandles({ activeEntity }: JSONObject) {
isResizable={true}
>
<div
className=" overflow-hidden rounded-md z-10 border border-dark-300 flex flex-col h-full"
className=" overflow-hidden rounded-md z-10 bg-dark-600 border border-dark-300 flex flex-col h-full"
key="b"
data-grid={{
x: 0,
@@ -54,7 +86,7 @@ export function ResizableHandles({ activeEntity }: JSONObject) {
<li className="flex items-start">
<div className="flex items-center">
<span className="text-slate-500 font-display truncate">
Entity Editor{" "}
Entity Editor
<span className="font-medium font-display">/&nbsp;</span>
</span>
</div>
@@ -64,9 +96,8 @@ export function ResizableHandles({ activeEntity }: JSONObject) {
<span
className="text-slate-500 text-inherit whitespace-nowrap font-display"
title={"placeholder"}
aria-current={"placeholder"}
>
{activeEntity.label}
{activeEntity && activeEntity.label}
<span className="font-medium font-display ">&nbsp;/</span>
</span>
</div>
@@ -75,16 +106,16 @@ export function ResizableHandles({ activeEntity }: JSONObject) {
<div className="flex justify-between items-center w-full text-slate-400 ">
<button
onClick={() => {
sdk.entities.updateEntityByUuid(activeEntity.uuid, {
source: pythonCode,
label: activeEntity.label,
description: activeEntity.description,
author: activeEntity.author
}).then(() => {
toast.info(
`The ${activeEntity.label} entity has been saved.`
);
}).catch(error => console.error(error));
// sdk.entities.updateEntityByUuid(activeEntity.uuid, {
// source: pythonCode,
// label: activeEntity.label,
// description: activeEntity.description,
// author: activeEntity.author
// }).then(() => {
// toast.info(
// `The ${activeEntity.label} entity has been saved.`
// );
// }).catch(error => console.error(error));
}}
>
<Icon
@@ -105,28 +136,9 @@ export function ResizableHandles({ activeEntity }: JSONObject) {
</div>
</li>
</ol>
<div className="container overflow-y-scroll h-full !bg-[#1a1b26]">
<div className="container bg-[#1A1B26] overflow-y-scroll h-full ">
<div className="editor ">
<CodeMirror
theme={tokyoNightInit({
settings: {
caret: "#c6c6c6",
fontFamily: "FiraCode",
background: '#1a1b26'
},
styles: [{ tag: t.comment, color: "#6272a4" }],
})}
value={pythonCode}
onChange={(value) => setPythonCode(value)}
extensions={[python({ jsx: true })]}
options={{
mode: "python",
theme: "default",
lineNumbers: true,
}}
/>
<CodeEditor code={code} setCode={setCode} />
</div>
</div>
</div>
@@ -134,30 +146,3 @@ export function ResizableHandles({ activeEntity }: JSONObject) {
</>
);
}
export default function EntityDetails() {
const params: any = useParams()
const [activeGraph, setActiveGraph] = useState<any>(null);
const [graphStats, setGraphStats] = useState<any>(null);
const [activeEntity, setActiveEntity] = useState({});
useEffect(() => {
if (params?.entityId) {
sdk.entities.getEntity(params.entityId).then(data => {
setActiveEntity(data)
})
}
}, [params?.entityId])
return (
<>
<section className="flex flex-col h-screen w-full">
{/* <GraphHeader stats={graphStats} graph={activeGraph} /> */}
<section className="flex w-full h-full relative">
<ResizableHandles activeEntity={activeEntity} />
</section>
</section>
</>
)
}

View File

@@ -0,0 +1,558 @@
// @ts-nocheck
import { setDiagnostics } from '@codemirror/lint';
import { Facet } from '@codemirror/state';
import { EditorView,Tooltip} from '@codemirror/view';
import {
RequestManager,
Client,
WebSocketTransport,
} from '@open-rpc/client-js';
import {
DiagnosticSeverity,
CompletionItemKind,
CompletionTriggerKind,
} from 'vscode-languageserver-protocol';
import type {
Completion,
CompletionContext,
CompletionResult,
} from '@codemirror/autocomplete';
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol';
import type { ViewUpdate, PluginValue } from '@codemirror/view';
import type { Text } from '@codemirror/state';
import type * as LSP from 'vscode-languageserver-protocol';
import { Transport } from '@open-rpc/client-js/build/transports/Transport';
const timeout = 10000;
const changesDelay = 500;
const CompletionItemKindMap = Object.fromEntries(
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
) as Record<CompletionItemKind, string>;
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
// Client to server then server to client
export interface LSPRequestMap {
initialize: [LSP.InitializeParams, LSP.InitializeResult];
'textDocument/hover': [LSP.HoverParams, LSP.Hover];
'textDocument/completion': [
LSP.CompletionParams,
LSP.CompletionItem[] | LSP.CompletionList | null
];
}
// Client to server
export interface LSPNotifyMap {
initialized: LSP.InitializedParams;
'textDocument/didChange': LSP.DidChangeTextDocumentParams;
'textDocument/didOpen': LSP.DidOpenTextDocumentParams;
}
// Server to client
export interface LSPEventMap {
'textDocument/publishDiagnostics': LSP.PublishDiagnosticsParams;
}
export type Notification = {
[key in keyof LSPEventMap]: {
jsonrpc: '2.0';
id?: null | undefined;
method: key;
params: LSPEventMap[key];
};
}[keyof LSPEventMap];
export class LanguageServerClient {
private rootUri: string;
private workspaceFolders: LSP.WorkspaceFolder[];
private autoClose?: boolean;
private transport: Transport;
private requestManager: RequestManager;
private client: Client;
public ready: boolean;
public capabilities: LSP.ServerCapabilities<any>;
private plugins: LanguageServerPlugin[];
public initializePromise: Promise<void>;
constructor(options: LanguageServerClientOptions) {
this.rootUri = options.rootUri as string;
this.workspaceFolders = options.workspaceFolders as LSP.WorkspaceFolder[];
this.autoClose = options.autoClose;
this.plugins = [];
this.transport = options.transport;
this.requestManager = new RequestManager([this.transport]);
this.client = new Client(this.requestManager);
this.client.onNotification((data) => {
console.log('on notification constructor', data)
this.processNotification(data as any);
});
const webSocketTransport = <WebSocketTransport>this.transport
if (webSocketTransport && webSocketTransport.connection) {
// XXX(hjr265): Need a better way to do this. Relevant issue:
// https://github.com/FurqanSoftware/codemirror-languageserver/issues/9
webSocketTransport.connection.addEventListener('message', (message: JSONObject) => {
const data = JSON.parse(message.data);
if (data.method && data.id) {
webSocketTransport.connection.send(JSON.stringify({
jsonrpc: '2.0',
id: data.id,
result: null
}));
}
});
}
this.initializePromise = this.initialize();
this.transport.connection.addEventListener('message', (message) => {
const data = JSON.parse(message.data);
if (data.method && data.id)
console.log('BEFORE process request', data, message)
this.processRequest(data);
});
}
processRequest({ id }: { id: string }) {
this.transport.connection.send(JSON.stringify({
jsonrpc: '2.0',
id,
result: null
}));
}
async initialize() {
const { capabilities } = await this.request('initialize', {
capabilities: {
textDocument: {
hover: {
dynamicRegistration: true,
contentFormat: ['plaintext', 'markdown'],
},
moniker: {},
synchronization: {
dynamicRegistration: true,
willSave: false,
didSave: false,
willSaveWaitUntil: false,
},
completion: {
dynamicRegistration: true,
completionItem: {
snippetSupport: false,
commitCharactersSupport: true,
documentationFormat: ['plaintext', 'markdown'],
deprecatedSupport: false,
preselectSupport: false,
},
contextSupport: false,
},
signatureHelp: {
dynamicRegistration: true,
signatureInformation: {
documentationFormat: ['plaintext', 'markdown'],
},
},
declaration: {
dynamicRegistration: true,
linkSupport: true,
},
definition: {
dynamicRegistration: true,
linkSupport: true,
},
typeDefinition: {
dynamicRegistration: true,
linkSupport: true,
},
implementation: {
dynamicRegistration: true,
linkSupport: true,
},
},
workspace: {
didChangeConfiguration: {
dynamicRegistration: true,
},
},
},
initializationOptions: null,
processId: null,
rootUri: this.rootUri,
workspaceFolders: this.workspaceFolders,
}, timeout * 3);
this.capabilities = capabilities;
this.notify('initialized', {});
this.ready = true;
}
close() {
this.client.close();
}
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
return this.notify('textDocument/didOpen', params);
}
textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
return this.notify('textDocument/didChange', params)
}
async textDocumentHover(params: LSP.HoverParams) {
return await this.request('textDocument/hover', params, timeout)
}
async textDocumentCompletion(params: LSP.CompletionParams) {
return await this.request('textDocument/completion', params, timeout)
}
attachPlugin(plugin: LanguageServerPlugin) {
this.plugins.push(plugin);
}
detachPlugin(plugin: LanguageServerPlugin) {
const i = this.plugins.indexOf(plugin);
if (i === -1) return;
this.plugins.splice(i, 1);
if (this.autoClose) this.close();
}
private request<K extends keyof LSPRequestMap>(
method: K,
params: LSPRequestMap[K][0],
timeout: number
): Promise<LSPRequestMap[K][1]> {
return this.client.request({ method, params }, timeout);
}
private notify<K extends keyof LSPNotifyMap>(
method: K,
params: LSPNotifyMap[K]
): Promise<LSPNotifyMap[K]> {
return this.client.notify({ method, params });
}
private processNotification(notification: Notification) {
for (const plugin of this.plugins)
plugin.processNotification(notification);
}
}
export class LanguageServerPlugin implements PluginValue {
public client: LanguageServerClient;
private documentUri: string;
private languageId: string;
private documentVersion: number;
private changesTimeout: number;
constructor(private view: EditorView, private allowHTMLContent: boolean) {
this.client = this.view.state.facet(client);
this.documentUri = this.view.state.facet(documentUri);
this.languageId = this.view.state.facet(languageId);
this.documentVersion = 0;
this.changesTimeout = 0;
this.client.attachPlugin(this);
try {
this.initialize({
documentText: this.view.state.doc.toString(),
});
} catch (e) {
console.warn('wtfd', e)
}
}
update({ docChanged }: ViewUpdate) {
if (!docChanged) return;
if (this.changesTimeout) clearTimeout(this.changesTimeout);
this.changesTimeout = self.setTimeout(() => {
this.sendChange({
documentText: this.view.state.doc.toString(),
});
}, changesDelay);
}
destroy() {
this.client.detachPlugin(this);
}
async initialize({ documentText }: { documentText: string }) {
if (this.client.initializePromise) {
try {
await this.client.initializePromise;
} catch (e) {
console.log('catching init')
console.warn(e)
}
}
this.client.textDocumentDidOpen({
textDocument: {
uri: this.documentUri,
languageId: this.languageId,
text: documentText,
version: this.documentVersion,
}
});
}
async sendChange({ documentText }: { documentText: string }) {
if (!this.client.ready) return;
try {
await this.client.textDocumentDidChange({
textDocument: {
uri: this.documentUri,
version: this.documentVersion++,
},
contentChanges: [{ text: documentText }],
});
} catch (e) {
console.error(e);
}
}
requestDiagnostics(view: EditorView) {
this.sendChange({ documentText: view.state.doc.toString() });
}
async requestHoverTooltip(
view: EditorView,
{ line, character }: { line: number; character: number }
): Promise<Tooltip | null> {
if (!this.client.ready || !this.client.capabilities!.hoverProvider) return null;
this.sendChange({ documentText: view.state.doc.toString() });
const result = await this.client.textDocumentHover({
textDocument: { uri: this.documentUri },
position: { line, character },
});
if (!result) return null;
const { contents, range } = result;
let pos = posToOffset(view.state.doc, { line, character })!;
let end: number = 0;
if (range) {
pos = posToOffset(view.state.doc, range.start)!;
end = posToOffset(view.state.doc, range.end)!;
}
if (pos === null) return null;
const dom = document.createElement('div');
dom.classList.add('documentation');
if (this.allowHTMLContent) dom.innerHTML = formatContents(contents);
else dom.textContent = formatContents(contents);
return { pos, end, create: (view) => ({ dom }), above: true };
}
async requestCompletion(
context: CompletionContext,
{ line, character }: { line: number; character: number },
{
triggerKind,
triggerCharacter,
}: {
triggerKind: CompletionTriggerKind;
triggerCharacter: string | undefined;
}
): Promise<CompletionResult | null> {
if (!this.client.ready || !this.client.capabilities!.completionProvider) return null;
this.sendChange({
documentText: context.state.doc.toString(),
});
const result = await this.client.textDocumentCompletion({
textDocument: { uri: this.documentUri },
position: { line, character },
context: {
triggerKind,
triggerCharacter,
}
});
if (!result) return null;
const items = 'items' in result ? result.items : result;
let options = items.map(
({
detail,
label,
kind,
textEdit,
documentation,
sortText,
filterText,
}) => {
const completion: Completion & {
filterText: string;
sortText?: string;
apply: string;
} = {
label,
detail,
apply: textEdit?.newText ?? label,
type: kind && CompletionItemKindMap[kind].toLowerCase(),
sortText: sortText ?? label,
filterText: filterText ?? label,
};
if (documentation) {
completion.info = formatContents(documentation);
}
return completion;
}
);
const [span, match] = prefixMatch(options);
const token = context.matchBefore(match);
let { pos } = context;
if (token) {
pos = token.from;
const word = token.text.toLowerCase();
if (/^\w+$/.test(word)) {
options = options
.filter(({ filterText }) =>
filterText.toLowerCase().startsWith(word)
)
.sort(({ apply: a }, { apply: b }) => {
switch (true) {
case a.startsWith(token.text) &&
!b.startsWith(token.text):
return -1;
case !a.startsWith(token.text) &&
b.startsWith(token.text):
return 1;
}
return 0;
});
}
}
return {
from: pos,
options,
};
}
processNotification(notification: Notification) {
try {
console.log('processing notification')
switch (notification.method) {
case 'textDocument/publishDiagnostics':
this.processDiagnostics(notification.params);
}
} catch (error) {
console.error(error);
}
}
processDiagnostics(params: PublishDiagnosticsParams) {
if (params.uri !== this.documentUri) return;
const diagnostics = params.diagnostics
.map(({ range, message, severity }) => ({
from: posToOffset(this.view.state.doc, range.start)!,
to: posToOffset(this.view.state.doc, range.end)!,
severity: ({
[DiagnosticSeverity.Error]: 'error',
[DiagnosticSeverity.Warning]: 'warning',
[DiagnosticSeverity.Information]: 'info',
[DiagnosticSeverity.Hint]: 'info',
} as const)[severity!],
message,
}))
.filter(({ from, to }) => from !== null && to !== null && from !== undefined && to !== undefined)
.sort((a, b) => {
switch (true) {
case a.from < b.from:
return -1;
case a.from > b.from:
return 1;
}
return 0;
});
this.view.dispatch(setDiagnostics(this.view.state, diagnostics));
}
}
export interface LanguageServerBaseOptions {
rootUri: string | null;
workspaceFolders?: LSP.WorkspaceFolder[] | null;
documentUri: string;
languageId: string;
}
export interface LanguageServerClientOptions extends LanguageServerBaseOptions {
transport: Transport,
autoClose?: boolean;
}
export function posToOffset(doc: Text, pos: { line: number; character: number }) {
if (pos.line >= doc.lines) return;
const offset = doc.line(pos.line + 1).from + pos.character;
if (offset > doc.length) return;
return offset;
}
export function offsetToPos(doc: Text, offset: number) {
const line = doc.lineAt(offset);
return {
line: line.number - 1,
character: offset - line.from,
};
}
export function formatContents(
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
): string {
if (Array.isArray(contents)) {
return contents.map((c) => formatContents(c) + '\n\n').join('');
} else if (typeof contents === 'string') {
return contents;
} else {
return contents.value;
}
}
export function toSet(chars: Set<string>) {
let preamble = '';
let flat = Array.from(chars).join('');
const words = /\w/.test(flat);
if (words) {
preamble += '\\w';
flat = flat.replace(/\w/g, '');
}
return `[${preamble}${flat.replace(/[^\w\s]/g, '\\$&')}]`;
}
export function prefixMatch(options: Completion[]) {
const first = new Set<string>();
const rest = new Set<string>();
for (const { apply } of options) {
const [initial, ...restStr] = apply as string;
first.add(initial);
for (const char of restStr) {
rest.add(char);
}
}
const source = toSet(first) + toSet(rest) + '*$';
return [new RegExp('^' + source), new RegExp(source)];
}

View File

@@ -0,0 +1,89 @@
import { Facet, ViewPlugin, hoverTooltip } from "@uiw/react-codemirror";
import { LanguageServerBaseOptions, LanguageServerClient, LanguageServerClientOptions, LanguageServerPlugin, offsetToPos } from "./language";
import { autocompletion } from '@codemirror/autocomplete';
import {
CompletionTriggerKind,
} from 'vscode-languageserver-protocol';
import {
WebSocketTransport,
} from '@open-rpc/client-js';
interface LanguageServerOptions extends LanguageServerClientOptions {
client?: LanguageServerClient;
allowHTMLContent?: boolean;
}
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '');
const client = Facet.define<LanguageServerClient, LanguageServerClient>({ combine: useLast });
const documentUri = Facet.define<string, string>({ combine: useLast });
const languageId = Facet.define<string, string>({ combine: useLast });
export function languageServerWithTransport(options: LanguageServerOptions) {
let plugin: LanguageServerPlugin | null = null;
return [
client.of(options.client || new LanguageServerClient({ ...options, autoClose: true })),
documentUri.of(options.documentUri),
languageId.of(options.languageId),
ViewPlugin.define((view) => (plugin = new LanguageServerPlugin(view, options.allowHTMLContent as boolean))),
hoverTooltip(
(view, pos) =>
plugin?.requestHoverTooltip(
view,
offsetToPos(view.state.doc, pos)
) ?? null
),
autocompletion({
override: [
async (context) => {
if (plugin == null) return null;
const { state, pos, explicit } = context;
const line = state.doc.lineAt(pos);
let trigKind: CompletionTriggerKind =
CompletionTriggerKind.Invoked;
let trigChar: string | undefined;
if (
!explicit &&
plugin.client.capabilities?.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter;
trigChar = line.text[pos - line.from - 1];
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null;
}
return await plugin.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
);
},
],
}),
];
}
export interface LanguageServerWebsocketOptions extends LanguageServerBaseOptions {
serverUri?: `ws://${string}` | `wss://${string}`;
}
export function languageServer(options: LanguageServerWebsocketOptions) {
const serverUri = options.serverUri;
delete options.serverUri;
return languageServerWithTransport({
...options,
transport: new WebSocketTransport(serverUri as string)
})
}

View File

@@ -17,11 +17,6 @@ export default function IncidentCard({ closeModal }: JSONObject) {
return option.label.toLowerCase().includes(query.toLowerCase());
}) ?? [];
useEffect(() => {
sdk.graphs.getGraphs(0, 1000).then((data) => {
setOptions(data);
});
}, []);
return (
<div className='bg-dark-600 w-full shadow sm:rounded-lg'>

View File

@@ -72,12 +72,12 @@ export function formatAMPM(date: Date) {
return strTime;
}
export function formatPGDate(date: string, showAt: boolean = false) {
export function formatPGDate(date: string, showAt: boolean = false): string {
if (date) {
const dateStr = date.replace(' ', 'T')
return `${new Date(dateStr).toDateString()}
return `${new Date(dateStr).toLocaleDateString()}
${showAt ? 'at' : ''}
${formatAMPM(new Date(dateStr))}`
}
return null
return ''
}

View File

@@ -1,89 +0,0 @@
import { LS_USER_AUTH_KEY, sdk } from '@/app/api';
import { LoginFormValues } from '@/routes/public/SigninPage';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export interface User {
email: string;
isActive: boolean;
isSuperuser: boolean;
fullName: string;
id: number;
modified: Date;
created: Date;
}
export interface AuthState {
auth: {
isAuthenticated: boolean;
token?: string;
user?: User;
};
}
const rememberState = JSON.parse(localStorage.getItem(LS_USER_AUTH_KEY) || '{}');
let initialState;
if (rememberState) {
initialState = {
isAuthenticated: true,
token: rememberState.token,
user: {
...rememberState.user,
},
};
} else {
initialState = {
isAuthenticated: false,
user: null,
token: null,
};
}
export const login = createAsyncThunk('auth/login', async (user: LoginFormValues, thunkAPI) => {
try {
const data = await sdk.login.loginAccessToken({
username: user.email,
password: user.password
})
if (data && data.token) {
localStorage.setItem(LS_USER_AUTH_KEY, JSON.stringify(data));
return data
}
} catch (error) {
console.error(error);
return thunkAPI.rejectWithValue(error);
}
});
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
logout: (state) => {
state.isAuthenticated = false;
state.user = null;
state.token = null;
},
},
extraReducers: (builder) => {
builder.addCase(login.fulfilled, (state, action) => {
const user = action?.payload;
state.isAuthenticated = true;
state.token = action?.payload?.token;
state.user = {
...user,
};
});
},
});
export const { logout } = authSlice.actions;
export const selectAuthenticated = (state: AuthState) => state.auth.isAuthenticated;
export const selectToken = (state: AuthState) => {
if (state.auth.user) return state.auth.token;
return null;
};
export default authSlice.reducer;

View File

@@ -0,0 +1,225 @@
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
import { RootState } from '@/app/store';
import { sdk } from '@/app/api';
import { Entitiesv1, Graph, GraphCreate } from '@/app/openapi';
import Entities from '../../routes/dashboard/Entities';
export interface Graphs {
graphs: Graph[];
favoriteGraphs: Graph[];
isLoadingGraphs: boolean;
isGraphsError: boolean;
isLoadingFavoriteGraphs: boolean;
isFavoriteGraphsError: boolean;
isLoadingEntities: boolean;
isLoadingFavoriteEntities: boolean;
isEntitiesError: boolean;
isFavoriteEntitesError: boolean;
favoriteEntities: JSONObject[];
entities: JSONObject[];
activeGraph: null | Graph;
selectedEntity: null | JSONObject;
activeTab: "graphs" | "entities" | "market";
graphsCount: number;
favoriteGraphsCount: number
favoriteEntitiesCount: number;
entitiesCount: number;
activeEntityId: JSONObject | null;
}
const initialState: Graphs = {
isLoadingGraphs: false,
isGraphsError: false,
favoriteGraphs: [],
graphs: [],
graphsCount: 0,
favoriteGraphsCount: 0,
activeGraph: null,
favoriteEntitiesCount: 0,
entitiesCount: 0,
selectedEntity: null,
entities: [],
favoriteEntities: [],
isLoadingFavoriteGraphs: false,
isFavoriteGraphsError: false,
isLoadingEntities: false,
isLoadingFavoriteEntities: false,
isEntitiesError: false,
isFavoriteEntitesError: false,
activeTab: "graphs",
activeEntityId: null
};
interface UpdateGraphFavorite {
uuid: string
isFavorite: boolean
}
interface DataFavorite {
data: Graph
isFavorite: boolean
}
interface ErrorFavorite {
data: Graph;
isFavorite: boolean;
}
// TODO: Fix these stupid fucking types in the builder...
export const updateGraphFavorite = createAsyncThunk("graphs/updateGraphFavorite", async ({ uuid, isFavorite }: UpdateGraphFavorite, thunkAPI): Promise<DataFavorite | ErrorFavorite> => await sdk.graphs.updateFavoriteGraphUuid(uuid, isFavorite)
.then((data: Graph) => ({ data, isFavorite }))
.catch((error) => ({ data: error, isFavorite }))
);
export const createGraph = createAsyncThunk("graphs/createGraph", async (createGraph: GraphCreate, thunkAPI) => await sdk.graphs.createGraph(createGraph)
.then(graph => graph)
.catch(error => {
console.error(error);
return thunkAPI.rejectWithValue(error)
})
);
export const deleteGraph = createAsyncThunk("graphs/deleteGraph", async (uuid: string, thunkAPI) => await sdk.graphs.deleteGraph(uuid)
.then(() => uuid)
.catch((error: Error) => {
console.error(error);
return thunkAPI.rejectWithValue(error);
})
);
interface GetGraphs {
pageSize: number, pageIndex: number, isFavorite: boolean
}
export const getGraphs = createAsyncThunk("graphs/getGraphs", async ({
pageSize,
pageIndex,
isFavorite }: GetGraphs,
thunkAPI
) => await sdk.graphs.getGraphs(pageSize * pageIndex, pageSize, isFavorite)
.then((data) => {
return { data, isFavorite }
})
.catch(error => {
console.error(error);
return thunkAPI.rejectWithValue({ error, isFavorite });
}))
export const getEntities = createAsyncThunk("entities/getEntities", async ({
pageSize,
pageIndex,
isFavorite }: GetGraphs,
thunkAPI
) => await sdk.entities.getEntities(pageSize * pageIndex, pageSize, isFavorite)
.then((data) => {
return { data, isFavorite }
})
.catch(error => {
console.error(error);
return thunkAPI.rejectWithValue({ error, isFavorite });
}))
export const dashboard = createSlice({
name: "dashboard",
initialState,
reducers: {
setActiveTab: (state, action) => {
state.activeTab = action.payload;
},
setActiveEntityId: (state, action) => {
state.activeEntityId = action.payload
}
},
extraReducers: (builder) => {
builder
.addCase(createGraph.fulfilled, (state, action) => {
state.graphs.push(action.payload)
})
.addCase(deleteGraph.fulfilled, (state, action) => {
state.graphs = state.graphs.filter((graph) => graph.uuid !== action.payload)
state.favoriteGraphs = state.favoriteGraphs.filter((graph) => graph.uuid !== action.payload)
})
.addCase(getGraphs.fulfilled, (state, action) => {
if (action.payload.isFavorite) {
state.favoriteGraphs = action.payload.data.graphs
state.favoriteGraphsCount = action.payload.data.count
} else {
state.graphs = action.payload.data.graphs
state.graphsCount = action.payload.data.count
}
})
.addCase(updateGraphFavorite.fulfilled, (state, action) => {
if (action.payload.isFavorite) {
state.graphs = state.graphs.filter((graph) => graph.uuid !== action.payload.data.uuid)
state.favoriteGraphsCount += 1
state.graphsCount -= 1
state.favoriteGraphs.push(action.payload.data)
} else {
state.favoriteGraphs = state.favoriteGraphs.filter((graph) => graph.uuid !== action.payload.data.uuid)
state.graphsCount += 1
state.favoriteGraphsCount -= 1
state.graphs.push(action.payload.data)
}
state.isLoadingFavoriteGraphs = false
state.isLoadingGraphs = false
})
.addCase(updateGraphFavorite.pending, (state, action) => {
state.isLoadingFavoriteGraphs = true
state.isLoadingGraphs = true
})
.addCase(updateGraphFavorite.rejected, (state, action: JSONObject) => {
if (action.payload.isFavorite) {
state.isFavoriteGraphsError = true
state.isLoadingFavoriteGraphs = false
}
if (!action.payload.isFavorite) {
state.isGraphsError = true
state.isLoadingGraphs = false
}
})
.addCase(getEntities.fulfilled, (state, action) => {
if (action.payload.isFavorite) {
state.favoriteEntities = action.payload.data.entities
state.favoriteEntitiesCount = action.payload.data.count
} else {
state.entities = action.payload.data.entities
state.entitiesCount = action.payload.data.count
}
})
}
});
export const { setActiveTab, setActiveEntityId } = dashboard.actions;
export const selectDashboardGraphs = (state: RootState) => ({
graphs: state.dashboard.graphs,
graphsCount: state.dashboard.graphsCount,
activeGraph: state.dashboard.activeGraph,
isLoadingGraphs: state.dashboard.isLoadingGraphs,
isLoadingFavoriteGraphs: state.dashboard.isLoadingFavoriteGraphs,
isGraphsError: state.dashboard.isGraphsError,
isFavoriteGraphsError: state.dashboard.isFavoriteGraphsError,
favoriteGraphs: state.dashboard.favoriteGraphs,
})
export const selectDashboardEntities = (state: RootState) => ({
entities: state.dashboard.entities,
favoriteEntities: state.dashboard.favoriteEntities,
entitiesCount: state.dashboard.entitiesCount,
favoriteEntitiesCount: state.dashboard.favoriteEntitiesCount,
isLoadingEntities: state.dashboard.isLoadingEntities,
isLoadingFavoriteEntities: state.dashboard.isLoadingFavoriteEntities,
isEntitiesError: state.dashboard.isEntitiesError,
isFavoriteEntitiesError: state.dashboard.isFavoriteEntitesError
});
export const selectActiveTab = (state: RootState) => state.dashboard.activeTab
export const selectActiveEntity = (state: RootState) => state.dashboard.entities.filter((entity) => entity.uuid === state.dashboard.activeEntityId)[0]
export default dashboard.reducer;

View File

@@ -23,7 +23,7 @@ import { isSidebarOpen, setSidebar } from "@/features/settings/settingsSlice";
import IncidentCard from "@/components/IncidentCard";
const navigation = [
{ name: "Inquiries", to: "/app/inquiries", icon: InboxIcon },
{ name: "Dashboard", to: "/app/dashboard/graphs", icon: InboxIcon },
{ name: "Incidents *", to: "/app/incidents", icon: FolderOpenIcon },
{ name: "Scans", to: "/app/scans", icon: DocumentMagnifyingGlassIcon },
];

View File

@@ -1,27 +1,29 @@
import React, { ReactElement, Suspense, lazy } from "react";
import { Route, Routes } from "react-router-dom";
import { Route, Routes, useParams } from "react-router-dom";
import AppLayout from "./AppLayout";
import NotFound from "./NotFound";
import PublicLayout from "./PublicLayout";
import ScansCreatePage from "./scans-create";
import GraphDetails from "./graphs/GraphDetails";
import Graphs from "./graphs/Graphs";
import Entities from "./graphs/Entities";
import Market from "./graphs/Market";
import EntityDetails from "./graphs/EntityDetails";
const SigninPage = lazy(() => import("./public/SigninPage"));
const DashboardPage = lazy(() => import("./graphs"));
import GraphDetails from "./dashboard/GraphDetailsPage";
import Graphs from "./dashboard/_components/Graphs";
import Entities from "./dashboard/Entities";
import Market from "./dashboard/MarketPage";
import EntityDetails from "./dashboard/EntityDetailsPage";
const DashboardPage = lazy(() => import("./dashboard"));
const AboutPage = lazy(() => import("@routes/public/AboutPage"));
const LandingPage = lazy(() => import("@routes/public/LandingPage"));
const InquiryGraph = lazy(() => import("./inquiry-graph"));
const EntityGraphPage = lazy(() => import("./entity-graph"));
const SettingsPage = lazy(() => import("./settings"));
const IncidentsPage = lazy(() => import("./incidents"));
const ScansPage = lazy(() => import("./scans"));
import CallbackPage from './public/CallbackPage';
export default function AppRoutes(): ReactElement {
return (
<Routes>
<Route path="/callback" element={<><CallbackPage /></>} />
<Route path="/" element={<PublicLayout />}>
<Route
index
@@ -39,18 +41,10 @@ export default function AppRoutes(): ReactElement {
</Suspense>
}
/>
<Route
path="sign-in"
element={
<Suspense>
<SigninPage />
</Suspense>
}
/>
</Route>
<Route path="/app" element={<AppLayout />}>
<Route
path="inquiries"
path="dashboard"
element={
<Suspense>
<DashboardPage />
@@ -101,21 +95,14 @@ export default function AppRoutes(): ReactElement {
}
/>
<Route
path="inquiries/graph/:graphId"
path="dashboard/graph/:graphId"
element={
<Suspense>
<InquiryGraph />
</Suspense>
}
/>
<Route
path="entity/:entityId"
element={
<Suspense>
<EntityGraphPage />
</Suspense>
}
/>
<Route
path="incidents"
element={
@@ -153,5 +140,6 @@ export default function AppRoutes(): ReactElement {
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
);
}

View File

@@ -0,0 +1,32 @@
// @ts-nocheck
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom"
import { useAppDispatch, useAppSelector } from "@/app/hooks";
import { selectActiveEntity, setActiveEntityId } from "@/features/dashboard/dashboardSlice";
import EntityEditor from "../../components/EntityEditor/EntityEditor";
export default function EntityDetailsPage() {
const params: { entityId: string | undefined } = useParams()
const [activeGraph, setActiveGraph] = useState<any>(null);
const [graphStats, setGraphStats] = useState<any>(null);
const dispatch = useAppDispatch()
useEffect(() => {
params?.entityId && dispatch(setActiveEntityId(params.entityId))
}, [params])
const activeEntity = useAppSelector((state) => selectActiveEntity(state))
return (
<>
<div className="flex flex-col h-screen w-full">
<section className="flex w-full h-full relative">
<EntityEditor activeEntity={activeEntity} />
</section>
</div>
</>
)
}

View File

@@ -15,7 +15,7 @@ export default function GraphDetails() {
useEffect(() => {
sdk.graphs.getGraph(params.graphId)
.then((data) => {
setActiveGraph(data.graph)
setActiveGraph(data)
})
sdk.graphs.getGraphStats(params.graphId)
.then((data) => setGraphStats(data))
@@ -54,7 +54,6 @@ export default function GraphDetails() {
uniqueOuteChartRef.current,
data,
{
distributeSeries: true,
reverseData: true,
horizontalBars: true,
@@ -70,7 +69,7 @@ export default function GraphDetails() {
return (
<>
<section className="flex flex-col w-full">
<GraphHeader stats={graphStats} graph={activeGraph} />
<GraphHeader stats={graphStats} graph={activeGraph} setActiveGraph={setActiveGraph} />
<section className="flex w-full h-full relative">
<div className="flex flex-col w-2/5">
<div className="flex flex-col pl-4 mx-4 mt-4 bg-dark-600 rounded-md ">
@@ -90,7 +89,6 @@ export default function GraphDetails() {
<CaseNotes />
</section>
</section>
</section>
</>
)

Some files were not shown because too many files have changed in this diff Show More