mirror of
https://github.com/bec-project/bec_atlas.git
synced 2025-07-14 07:01:48 +02:00
feat: towards a first version
This commit is contained in:
@ -1,6 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import Literal
|
from typing import TYPE_CHECKING, Literal, Type, TypeVar
|
||||||
|
|
||||||
import pymongo
|
import pymongo
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
@ -11,6 +13,11 @@ from bec_atlas.model.model import User, UserCredentials
|
|||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bson import ObjectId
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
class MongoDBDatasource:
|
class MongoDBDatasource:
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: dict) -> None:
|
||||||
@ -107,19 +114,19 @@ class MongoDBDatasource:
|
|||||||
return UserCredentials(**out)
|
return UserCredentials(**out)
|
||||||
|
|
||||||
def find_one(
|
def find_one(
|
||||||
self, collection: str, query_filter: dict, dtype: BaseModel, user: User | None = None
|
self, collection: str, query_filter: dict, dtype: Type[T], user: User | None = None
|
||||||
) -> BaseModel | None:
|
) -> T | None:
|
||||||
"""
|
"""
|
||||||
Find one document in the collection.
|
Find one document in the collection.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
collection (str): The collection name
|
collection (str): The collection name
|
||||||
query_filter (dict): The filter to apply
|
query_filter (dict): The filter to apply
|
||||||
dtype (BaseModel): The data type to return
|
dtype (Type[T]): The data type to return
|
||||||
user (User): The user making the request
|
user (User): The user making the request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
BaseModel: The data type with the document data
|
T: The data type with the document data
|
||||||
"""
|
"""
|
||||||
if user is not None:
|
if user is not None:
|
||||||
query_filter = self.add_user_filter(user, query_filter)
|
query_filter = self.add_user_filter(user, query_filter)
|
||||||
@ -129,15 +136,15 @@ class MongoDBDatasource:
|
|||||||
return dtype(**out)
|
return dtype(**out)
|
||||||
|
|
||||||
def find(
|
def find(
|
||||||
self, collection: str, query_filter: dict, dtype: BaseModel, user: User | None = None
|
self, collection: str, query_filter: dict, dtype: Type[T], user: User | None = None
|
||||||
) -> list[BaseModel]:
|
) -> list[T]:
|
||||||
"""
|
"""
|
||||||
Find all documents in the collection.
|
Find all documents in the collection.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
collection (str): The collection name
|
collection (str): The collection name
|
||||||
query_filter (dict): The filter to apply
|
query_filter (dict): The filter to apply
|
||||||
dtype (BaseModel): The data type to return
|
dtype (Type[T]): The data type to return
|
||||||
user (User): The user making the request
|
user (User): The user making the request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -148,20 +155,88 @@ class MongoDBDatasource:
|
|||||||
out = self.db[collection].find(query_filter)
|
out = self.db[collection].find(query_filter)
|
||||||
return [dtype(**x) for x in out]
|
return [dtype(**x) for x in out]
|
||||||
|
|
||||||
|
def post(self, collection: str, data: dict, dtype: Type[T], user: User | None = None) -> T:
|
||||||
|
"""
|
||||||
|
Post a single document to the collection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
collection (str): The collection name
|
||||||
|
data (dict): The data to insert
|
||||||
|
dtype (Type[T]): The data type to return
|
||||||
|
user (User): The user making the request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The data type with the document data
|
||||||
|
"""
|
||||||
|
if user is not None:
|
||||||
|
data = self.add_user_filter(user, data, operation="w")
|
||||||
|
out = self.db[collection].insert_one(data)
|
||||||
|
return dtype(**data)
|
||||||
|
|
||||||
|
def patch(
|
||||||
|
self,
|
||||||
|
collection: str,
|
||||||
|
id: ObjectId,
|
||||||
|
update: dict,
|
||||||
|
dtype: Type[T],
|
||||||
|
user: User | None = None,
|
||||||
|
return_document: bool = True,
|
||||||
|
) -> T | None:
|
||||||
|
"""
|
||||||
|
Patch a single document in the collection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
collection (str): The collection name
|
||||||
|
id (ObjectId): The document id
|
||||||
|
update (dict): The update to apply
|
||||||
|
dtype (Type[T]): The data type to return
|
||||||
|
user (User): The user making the request
|
||||||
|
return_document (bool): When True, return the updated document, otherwise return the original document
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Type[T]: The data type with the document data
|
||||||
|
"""
|
||||||
|
search_filter = {"_id": id}
|
||||||
|
if user is not None:
|
||||||
|
search_filter = self.add_user_filter(user, search_filter, operation="w")
|
||||||
|
out = self.db[collection].find_one_and_update(
|
||||||
|
filter=search_filter, update={"$set": update}, return_document=return_document
|
||||||
|
)
|
||||||
|
if out is None:
|
||||||
|
return None
|
||||||
|
return dtype(**out)
|
||||||
|
|
||||||
|
def delete_one(self, collection: str, filter: dict, user: User | None = None) -> bool:
|
||||||
|
"""
|
||||||
|
Delete a single document in the collection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
collection (str): The collection name
|
||||||
|
filter (dict): The filter to apply
|
||||||
|
user (User): The user making the request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the document was deleted, otherwise False
|
||||||
|
"""
|
||||||
|
if user is not None:
|
||||||
|
filter = self.add_user_filter(user, filter, operation="w")
|
||||||
|
out = self.db[collection].delete_one(filter)
|
||||||
|
return out.deleted_count > 0
|
||||||
|
|
||||||
def aggregate(
|
def aggregate(
|
||||||
self, collection: str, pipeline: list[dict], dtype: BaseModel, user: User | None = None
|
self, collection: str, pipeline: list[dict], dtype: Type[T], user: User | None = None
|
||||||
) -> list[BaseModel]:
|
) -> list[T]:
|
||||||
"""
|
"""
|
||||||
Aggregate documents in the collection.
|
Aggregate documents in the collection.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
collection (str): The collection name
|
collection (str): The collection name
|
||||||
pipeline (list[dict]): The aggregation pipeline
|
pipeline (list[dict]): The aggregation pipeline
|
||||||
dtype (BaseModel): The data type to return
|
dtype (Type[T]): The data type to return
|
||||||
user (User): The user making the request
|
user (User): The user making the request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[BaseModel]: The data type with the document data
|
list[T]: The data type with the document data
|
||||||
"""
|
"""
|
||||||
if user is not None:
|
if user is not None:
|
||||||
# Add the user filter to the lookup pipeline
|
# Add the user filter to the lookup pipeline
|
||||||
|
@ -7,7 +7,7 @@ from redis.asyncio import Redis as AsyncRedis
|
|||||||
from redis.exceptions import AuthenticationError
|
from redis.exceptions import AuthenticationError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bec_atlas.model.model import Deployments
|
from bec_atlas.model.model import DeploymentCredential
|
||||||
|
|
||||||
|
|
||||||
class RedisDatasource:
|
class RedisDatasource:
|
||||||
@ -63,31 +63,35 @@ class RedisDatasource:
|
|||||||
"default", enabled=True, categories=["-@all"], commands=["+auth", "+acl|whoami"]
|
"default", enabled=True, categories=["-@all"], commands=["+auth", "+acl|whoami"]
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_deployment_acl(self, deployment: Deployments):
|
def add_deployment_acl(self, deployment_credential: DeploymentCredential):
|
||||||
"""
|
"""
|
||||||
Add ACLs for the deployment.
|
Add ACLs for the deployment.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
deployment (Deployments): The deployment object
|
deployment (Deployments): The deployment object
|
||||||
"""
|
"""
|
||||||
print(f"Adding ACLs for deployment <{deployment.name}>({deployment.id})")
|
print(f"Adding ACLs for deployment {deployment_credential.id}")
|
||||||
|
dep_id = deployment_credential.id
|
||||||
|
dep_key = deployment_credential.credential
|
||||||
self.connector._redis_conn.acl_setuser(
|
self.connector._redis_conn.acl_setuser(
|
||||||
f"ingestor_{deployment.id}",
|
f"ingestor_{dep_id}",
|
||||||
enabled=True,
|
enabled=True,
|
||||||
passwords=f"+{deployment.deployment_key}",
|
passwords=f"+{dep_key}",
|
||||||
categories=["+@all", "-@dangerous"],
|
categories=["+@all", "-@dangerous"],
|
||||||
keys=[
|
keys=[
|
||||||
f"internal/deployment/{deployment.id}/*",
|
f"internal/deployment/{dep_id}/*",
|
||||||
f"internal/deployment/{deployment.id}/*/state",
|
f"internal/deployment/{dep_id}/*/state",
|
||||||
f"internal/deployment/{deployment.id}/*/data/*",
|
f"internal/deployment/{dep_id}/*/data/*",
|
||||||
|
f"internal/deployment/{dep_id}/bec_access",
|
||||||
],
|
],
|
||||||
channels=[
|
channels=[
|
||||||
f"internal/deployment/{deployment.id}/*/state",
|
f"internal/deployment/{dep_id}/*/state",
|
||||||
f"internal/deployment/{deployment.id}/*",
|
f"internal/deployment/{dep_id}/*",
|
||||||
f"internal/deployment/{deployment.id}/request",
|
f"internal/deployment/{dep_id}/request",
|
||||||
f"internal/deployment/{deployment.id}/request_response/*",
|
f"internal/deployment/{dep_id}/request_response/*",
|
||||||
|
f"internal/deployment/{dep_id}/bec_access",
|
||||||
],
|
],
|
||||||
commands=[f"+keys|internal/deployment/{deployment.id}/*/state"],
|
commands=[f"+keys|internal/deployment/{dep_id}/*/state"],
|
||||||
reset_channels=True,
|
reset_channels=True,
|
||||||
reset_keys=True,
|
reset_keys=True,
|
||||||
)
|
)
|
||||||
|
@ -163,7 +163,7 @@ class DataIngestor:
|
|||||||
self.handle_message(out, deployment_id)
|
self.handle_message(out, deployment_id)
|
||||||
self.redis._redis_conn.xack(stream, "ingestor", message[0])
|
self.redis._redis_conn.xack(stream, "ingestor", message[0])
|
||||||
|
|
||||||
def handle_message(self, msg_dict: dict, deploymend_id: str):
|
def handle_message(self, msg_dict: dict, deployment_id: str):
|
||||||
"""
|
"""
|
||||||
Handle a message from the Redis queue.
|
Handle a message from the Redis queue.
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ class DataIngestor:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(data, messages.ScanStatusMessage):
|
if isinstance(data, messages.ScanStatusMessage):
|
||||||
self.update_scan_status(data, deploymend_id)
|
self.update_scan_status(data, deployment_id)
|
||||||
|
|
||||||
@lru_cache()
|
@lru_cache()
|
||||||
def get_default_session_id(self, deployment_id: str):
|
def get_default_session_id(self, deployment_id: str):
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from bec_atlas.datasources.datasource_manager import DatasourceManager
|
from bec_atlas.datasources.datasource_manager import DatasourceManager
|
||||||
|
from bec_atlas.router.bec_access_router import BECAccessRouter
|
||||||
|
from bec_atlas.router.deployment_access_router import DeploymentAccessRouter
|
||||||
|
from bec_atlas.router.deployment_credentials import DeploymentCredentialsRouter
|
||||||
from bec_atlas.router.deployments_router import DeploymentsRouter
|
from bec_atlas.router.deployments_router import DeploymentsRouter
|
||||||
from bec_atlas.router.realm_router import RealmRouter
|
from bec_atlas.router.realm_router import RealmRouter
|
||||||
from bec_atlas.router.redis_router import RedisRouter, RedisWebsocket
|
from bec_atlas.router.redis_router import RedisRouter, RedisWebsocket
|
||||||
@ -13,6 +17,8 @@ CONFIG = {
|
|||||||
"mongodb": {"host": "localhost", "port": 27017},
|
"mongodb": {"host": "localhost", "port": 27017},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
origins = ["http://localhost:4200", "http://localhost"]
|
||||||
|
|
||||||
|
|
||||||
class AtlasApp:
|
class AtlasApp:
|
||||||
API_VERSION = "v1"
|
API_VERSION = "v1"
|
||||||
@ -20,6 +26,13 @@ class AtlasApp:
|
|||||||
def __init__(self, config=None):
|
def __init__(self, config=None):
|
||||||
self.config = config or CONFIG
|
self.config = config or CONFIG
|
||||||
self.app = FastAPI()
|
self.app = FastAPI()
|
||||||
|
self.app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
self.server = None
|
self.server = None
|
||||||
self.prefix = f"/api/{self.API_VERSION}"
|
self.prefix = f"/api/{self.API_VERSION}"
|
||||||
self.datasources = DatasourceManager(config=self.config)
|
self.datasources = DatasourceManager(config=self.config)
|
||||||
@ -43,12 +56,31 @@ class AtlasApp:
|
|||||||
raise ValueError("Datasources not loaded")
|
raise ValueError("Datasources not loaded")
|
||||||
self.scan_router = ScanRouter(prefix=self.prefix, datasources=self.datasources)
|
self.scan_router = ScanRouter(prefix=self.prefix, datasources=self.datasources)
|
||||||
self.app.include_router(self.scan_router.router, tags=["Scan"])
|
self.app.include_router(self.scan_router.router, tags=["Scan"])
|
||||||
|
|
||||||
self.user_router = UserRouter(prefix=self.prefix, datasources=self.datasources)
|
self.user_router = UserRouter(prefix=self.prefix, datasources=self.datasources)
|
||||||
self.app.include_router(self.user_router.router, tags=["User"])
|
self.app.include_router(self.user_router.router, tags=["User"])
|
||||||
|
|
||||||
self.deployment_router = DeploymentsRouter(prefix=self.prefix, datasources=self.datasources)
|
self.deployment_router = DeploymentsRouter(prefix=self.prefix, datasources=self.datasources)
|
||||||
self.app.include_router(self.deployment_router.router, tags=["Deployment"])
|
self.app.include_router(self.deployment_router.router, tags=["Deployment"])
|
||||||
|
|
||||||
|
self.deployment_credentials_router = DeploymentCredentialsRouter(
|
||||||
|
prefix=self.prefix, datasources=self.datasources
|
||||||
|
)
|
||||||
|
self.app.include_router(
|
||||||
|
self.deployment_credentials_router.router, tags=["Deployment Credentials"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.deployment_access_router = DeploymentAccessRouter(
|
||||||
|
prefix=self.prefix, datasources=self.datasources
|
||||||
|
)
|
||||||
|
self.app.include_router(self.deployment_access_router.router, tags=["Deployment Access"])
|
||||||
|
|
||||||
|
self.bec_access_router = BECAccessRouter(prefix=self.prefix, datasources=self.datasources)
|
||||||
|
self.app.include_router(self.bec_access_router.router, tags=["BEC Access"])
|
||||||
|
|
||||||
self.realm_router = RealmRouter(prefix=self.prefix, datasources=self.datasources)
|
self.realm_router = RealmRouter(prefix=self.prefix, datasources=self.datasources)
|
||||||
self.app.include_router(self.realm_router.router, tags=["Realm"])
|
self.app.include_router(self.realm_router.router, tags=["Realm"])
|
||||||
|
|
||||||
self.redis_router = RedisRouter(prefix=self.prefix, datasources=self.datasources)
|
self.redis_router = RedisRouter(prefix=self.prefix, datasources=self.datasources)
|
||||||
self.app.include_router(self.redis_router.router, tags=["Redis"])
|
self.app.include_router(self.redis_router.router, tags=["Redis"])
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from bec_lib import messages
|
from bec_lib import messages
|
||||||
@ -53,7 +52,6 @@ class UserInfo(BaseModel):
|
|||||||
class Deployments(MongoBaseModel, AccessProfile):
|
class Deployments(MongoBaseModel, AccessProfile):
|
||||||
realm_id: str | ObjectId
|
realm_id: str | ObjectId
|
||||||
name: str
|
name: str
|
||||||
deployment_key: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
||||||
active_session_id: str | ObjectId | None = None
|
active_session_id: str | ObjectId | None = None
|
||||||
config_templates: list[str | ObjectId] = []
|
config_templates: list[str | ObjectId] = []
|
||||||
|
|
||||||
@ -61,11 +59,57 @@ class Deployments(MongoBaseModel, AccessProfile):
|
|||||||
class DeploymentsPartial(MongoBaseModel, AccessProfilePartial):
|
class DeploymentsPartial(MongoBaseModel, AccessProfilePartial):
|
||||||
realm_id: str | ObjectId | None = None
|
realm_id: str | ObjectId | None = None
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
deployment_key: str | None = None
|
|
||||||
active_session_id: str | ObjectId | None = None
|
active_session_id: str | ObjectId | None = None
|
||||||
config_templates: list[str | ObjectId] | None = None
|
config_templates: list[str | ObjectId] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class DeploymentCredential(MongoBaseModel):
|
||||||
|
credential: str
|
||||||
|
|
||||||
|
|
||||||
|
class DeploymentAccess(MongoBaseModel, AccessProfile):
|
||||||
|
"""
|
||||||
|
The DeploymentAccess model is used to store the access control
|
||||||
|
lists for the deployment. The access control lists are used to
|
||||||
|
control access to the BEC deployment and contain either user
|
||||||
|
or group names.
|
||||||
|
Once the access control lists are updated, the corresponding
|
||||||
|
BECAccessProfiles for this deployment are updated to reflect
|
||||||
|
the changes.
|
||||||
|
|
||||||
|
Owner: beamline staff
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_read_access: list[str] = []
|
||||||
|
user_write_access: list[str] = []
|
||||||
|
su_read_access: list[str] = []
|
||||||
|
su_write_access: list[str] = []
|
||||||
|
remote_access: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
class BECAccessProfile(MongoBaseModel, AccessProfile):
|
||||||
|
"""
|
||||||
|
The BECAccessProfile model is used to store the Redis ACL config
|
||||||
|
for BEC of a user. The username can be either a user or a group.
|
||||||
|
The config fields (categories, keys, channels, commands) are determined
|
||||||
|
based on the access level given through the corresponding DeploymentAccess
|
||||||
|
document.
|
||||||
|
|
||||||
|
Owner: admin
|
||||||
|
Access: user or group matching the username
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
deployment_id: str | ObjectId
|
||||||
|
username: str
|
||||||
|
passwords: dict[str, str] = {}
|
||||||
|
categories: list[str] = []
|
||||||
|
keys: list[str] = []
|
||||||
|
channels: list[str] = []
|
||||||
|
commands: list[str] = []
|
||||||
|
profile: str = ""
|
||||||
|
|
||||||
|
|
||||||
class Realm(MongoBaseModel, AccessProfile):
|
class Realm(MongoBaseModel, AccessProfile):
|
||||||
realm_id: str
|
realm_id: str
|
||||||
deployments: list[Deployments | DeploymentsPartial] = []
|
deployments: list[Deployments | DeploymentsPartial] = []
|
||||||
|
51
backend/bec_atlas/router/bec_access_router.py
Normal file
51
backend/bec_atlas/router/bec_access_router.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from bson import ObjectId
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
|
||||||
|
from bec_atlas.authentication import get_current_user
|
||||||
|
from bec_atlas.datasources.mongodb.mongodb import MongoDBDatasource
|
||||||
|
from bec_atlas.model.model import BECAccessProfile, UserInfo
|
||||||
|
from bec_atlas.router.base_router import BaseRouter
|
||||||
|
|
||||||
|
|
||||||
|
class BECAccessRouter(BaseRouter):
|
||||||
|
def __init__(self, prefix="/api/v1", datasources=None):
|
||||||
|
super().__init__(prefix, datasources)
|
||||||
|
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||||
|
self.router = APIRouter(prefix=prefix)
|
||||||
|
self.router.add_api_route(
|
||||||
|
"/bec_access",
|
||||||
|
self.get_bec_access,
|
||||||
|
methods=["GET"],
|
||||||
|
description="Retrieve the access key for a specific deployment and user.",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_bec_access(
|
||||||
|
self,
|
||||||
|
deployment_id: str,
|
||||||
|
user: str = Query(None),
|
||||||
|
current_user: UserInfo = Depends(get_current_user),
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Retrieve the access key for a specific deployment and user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
deployment_id (str): The deployment id
|
||||||
|
user (str): The user name to retrieve the access key for. If not provided,
|
||||||
|
the access key for the current user will be retrieved.
|
||||||
|
current_user (UserInfo): The current user
|
||||||
|
"""
|
||||||
|
if not user:
|
||||||
|
user = current_user.email
|
||||||
|
out = self.db.find_one(
|
||||||
|
"bec_access_profiles",
|
||||||
|
{"deployment_id": ObjectId(deployment_id), "username": user},
|
||||||
|
BECAccessProfile,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not out:
|
||||||
|
raise HTTPException(status_code=404, detail="Access key not found.")
|
||||||
|
|
||||||
|
# Return the newest access key
|
||||||
|
timestamps = sorted(out.passwords.keys())
|
||||||
|
return {"token": out.passwords[timestamps[-1]]}
|
227
backend/bec_atlas/router/deployment_access_router.py
Normal file
227
backend/bec_atlas/router/deployment_access_router.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import secrets
|
||||||
|
import time
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from bec_lib.endpoints import EndpointInfo, MessageOp
|
||||||
|
from bec_lib.serialization import MsgpackSerialization
|
||||||
|
from bson import ObjectId
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from bec_atlas.authentication import get_current_user
|
||||||
|
from bec_atlas.datasources.mongodb.mongodb import MongoDBDatasource
|
||||||
|
from bec_atlas.model.model import BECAccessProfile, DeploymentAccess, UserInfo
|
||||||
|
from bec_atlas.router.base_router import BaseRouter
|
||||||
|
from bec_atlas.router.redis_router import RedisAtlasEndpoints
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bec_atlas.datasources.redis_datasource import RedisDatasource
|
||||||
|
|
||||||
|
|
||||||
|
class DeploymentAccessRouter(BaseRouter):
|
||||||
|
def __init__(self, prefix="/api/v1", datasources=None):
|
||||||
|
super().__init__(prefix, datasources)
|
||||||
|
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||||
|
self.router = APIRouter(prefix=prefix)
|
||||||
|
self.router.add_api_route(
|
||||||
|
"/deployment_access",
|
||||||
|
self.get_deployment_access,
|
||||||
|
methods=["GET"],
|
||||||
|
description="Get the access lists for a specific deployment.",
|
||||||
|
response_model=DeploymentAccess,
|
||||||
|
)
|
||||||
|
self.router.add_api_route(
|
||||||
|
"/deployment_access",
|
||||||
|
self.patch_deployment_access,
|
||||||
|
methods=["PATCH"],
|
||||||
|
description="Update the access lists for a specific deployment.",
|
||||||
|
response_model=DeploymentAccess,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_deployment_access(
|
||||||
|
self, deployment_id: str, current_user: UserInfo = Depends(get_current_user)
|
||||||
|
) -> DeploymentAccess:
|
||||||
|
"""
|
||||||
|
Get the access lists for a specific deployment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
deployment_id (str): The deployment id
|
||||||
|
current_user (UserInfo): The current user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DeploymentAccess: The access lists for the deployment
|
||||||
|
"""
|
||||||
|
return self.db.find_one(
|
||||||
|
"deployments", {"_id": ObjectId(deployment_id)}, DeploymentAccess, user=current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
async def patch_deployment_access(
|
||||||
|
self,
|
||||||
|
deployment_id: str,
|
||||||
|
deployment_access: dict,
|
||||||
|
current_user: UserInfo = Depends(get_current_user),
|
||||||
|
) -> DeploymentAccess:
|
||||||
|
"""
|
||||||
|
Update the access lists for a specific deployment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
deployment_id (str): The deployment id
|
||||||
|
deployment_access (DeploymentAccess): The deployment access object
|
||||||
|
current_user (UserInfo): The current user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DeploymentAccess: The updated access lists for the deployment
|
||||||
|
"""
|
||||||
|
deployment_access.pop("_id", None)
|
||||||
|
deployment_access.pop("id", None)
|
||||||
|
deployment_access.pop("owner_groups", None)
|
||||||
|
deployment_access.pop("access_groups", None)
|
||||||
|
original = self.db.find_one(
|
||||||
|
"deployment_access",
|
||||||
|
{"_id": ObjectId(deployment_id)},
|
||||||
|
DeploymentAccess,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
out = self.db.patch(
|
||||||
|
collection="deployment_access",
|
||||||
|
id=ObjectId(deployment_id),
|
||||||
|
update=deployment_access,
|
||||||
|
dtype=DeploymentAccess,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
self._update_bec_access_profiles(original=original, updated=out)
|
||||||
|
self._refresh_redis_bec_access(deployment_id)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _update_bec_access_profiles(self, original: DeploymentAccess, updated: DeploymentAccess):
|
||||||
|
"""
|
||||||
|
Update the BEC access profiles in the database. This will not update the redis access.
|
||||||
|
Call _refresh_redis_bec_access to update the redis access.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
deployment_access (DeploymentAccess): The deployment access object
|
||||||
|
"""
|
||||||
|
db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||||
|
|
||||||
|
new_profiles = set(
|
||||||
|
updated.user_read_access
|
||||||
|
+ updated.user_write_access
|
||||||
|
+ updated.su_read_access
|
||||||
|
+ updated.su_write_access
|
||||||
|
)
|
||||||
|
old_profiles = set(
|
||||||
|
original.user_read_access
|
||||||
|
+ original.user_write_access
|
||||||
|
+ original.su_read_access
|
||||||
|
+ original.su_write_access
|
||||||
|
)
|
||||||
|
removed_profiles = old_profiles - new_profiles
|
||||||
|
for profile in removed_profiles:
|
||||||
|
db.delete_one("bec_access_profiles", {"username": profile, "deployment_id": updated.id})
|
||||||
|
for profile in new_profiles:
|
||||||
|
if profile in updated.su_write_access:
|
||||||
|
access = self._get_redis_access_profile("su_write", profile, updated.id)
|
||||||
|
elif profile in updated.su_read_access:
|
||||||
|
access = self._get_redis_access_profile("su_read", profile, updated.id)
|
||||||
|
elif profile in updated.user_write_access:
|
||||||
|
access = self._get_redis_access_profile("user_write", profile, updated.id)
|
||||||
|
else:
|
||||||
|
access = self._get_redis_access_profile("user_read", profile, updated.id)
|
||||||
|
|
||||||
|
existing_profile = db.find_one(
|
||||||
|
"bec_access_profiles",
|
||||||
|
{"username": profile, "deployment_id": updated.id},
|
||||||
|
BECAccessProfile,
|
||||||
|
)
|
||||||
|
if existing_profile:
|
||||||
|
# access.passwords = existing_profile.passwords
|
||||||
|
db.patch(
|
||||||
|
"bec_access_profiles",
|
||||||
|
existing_profile.id,
|
||||||
|
access.model_dump(exclude_none=True, exclude_defaults=True),
|
||||||
|
BECAccessProfile,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
access.passwords = {str(time.time()): secrets.token_urlsafe(32)}
|
||||||
|
db.post(
|
||||||
|
"bec_access_profiles", access.model_dump(exclude_none=True), BECAccessProfile
|
||||||
|
)
|
||||||
|
|
||||||
|
def _refresh_redis_bec_access(self, deployment_id: str):
|
||||||
|
"""
|
||||||
|
Refresh the redis BEC access.
|
||||||
|
"""
|
||||||
|
redis: RedisDatasource = self.datasources.datasources.get("redis")
|
||||||
|
db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||||
|
profiles = db.find(
|
||||||
|
"bec_access_profiles", {"deployment_id": ObjectId(deployment_id)}, BECAccessProfile
|
||||||
|
)
|
||||||
|
profiles = [profile.model_dump(exclude_none=True) for profile in profiles]
|
||||||
|
for profile in profiles:
|
||||||
|
profile.pop("owner_groups", None)
|
||||||
|
profile.pop("access_groups", None)
|
||||||
|
profile.pop("deployment_id", None)
|
||||||
|
profile.pop("_id", None)
|
||||||
|
|
||||||
|
endpoint_info = EndpointInfo(
|
||||||
|
RedisAtlasEndpoints.redis_bec_acl_user(deployment_id), Any, MessageOp.SET_PUBLISH
|
||||||
|
)
|
||||||
|
|
||||||
|
redis.connector.set_and_publish(endpoint_info, MsgpackSerialization.dumps(profiles))
|
||||||
|
|
||||||
|
def _get_redis_access_profile(self, access_profile: str, username: str, deployment_id: str):
|
||||||
|
"""
|
||||||
|
Get the redis access profile.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
access_profile (str): The access profile
|
||||||
|
username (str): The username
|
||||||
|
deployment_id (str): The deployment id
|
||||||
|
|
||||||
|
"""
|
||||||
|
if access_profile == "su_write":
|
||||||
|
return BECAccessProfile(
|
||||||
|
owner_groups=["admin"],
|
||||||
|
access_groups=[username],
|
||||||
|
deployment_id=deployment_id,
|
||||||
|
username=username,
|
||||||
|
categories=["+@all"],
|
||||||
|
keys=["*"],
|
||||||
|
channels=["*"],
|
||||||
|
commands=["+all"],
|
||||||
|
profile="su_write",
|
||||||
|
)
|
||||||
|
if access_profile == "su_read":
|
||||||
|
return BECAccessProfile(
|
||||||
|
owner_groups=["admin"],
|
||||||
|
access_groups=[username],
|
||||||
|
deployment_id=deployment_id,
|
||||||
|
username=username,
|
||||||
|
categories=["+@all", "-@dangerous"],
|
||||||
|
keys=["*"],
|
||||||
|
channels=["*"],
|
||||||
|
commands=["+read"],
|
||||||
|
profile="su_read",
|
||||||
|
)
|
||||||
|
if access_profile == "user_write":
|
||||||
|
return BECAccessProfile(
|
||||||
|
owner_groups=["admin"],
|
||||||
|
access_groups=[username],
|
||||||
|
deployment_id=deployment_id,
|
||||||
|
username=username,
|
||||||
|
categories=["+@all", "-@dangerous"],
|
||||||
|
keys=["*"],
|
||||||
|
channels=["*"],
|
||||||
|
commands=["+write"],
|
||||||
|
profile="user_write",
|
||||||
|
)
|
||||||
|
return BECAccessProfile(
|
||||||
|
owner_groups=["admin"],
|
||||||
|
access_groups=[username],
|
||||||
|
deployment_id=deployment_id,
|
||||||
|
username=username,
|
||||||
|
categories=["+@all", "-@dangerous"],
|
||||||
|
keys=["*"],
|
||||||
|
channels=["*"],
|
||||||
|
commands=["+read"],
|
||||||
|
profile="user_read",
|
||||||
|
)
|
85
backend/bec_atlas/router/deployment_credentials.py
Normal file
85
backend/bec_atlas/router/deployment_credentials.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import secrets
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from bson import ObjectId
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
|
from bec_atlas.authentication import get_current_user
|
||||||
|
from bec_atlas.datasources.mongodb.mongodb import MongoDBDatasource
|
||||||
|
from bec_atlas.model.model import DeploymentCredential, UserInfo
|
||||||
|
from bec_atlas.router.base_router import BaseRouter
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bec_atlas.datasources.redis_datasource import RedisDatasource
|
||||||
|
|
||||||
|
|
||||||
|
class DeploymentCredentialsRouter(BaseRouter):
|
||||||
|
def __init__(self, prefix="/api/v1", datasources=None):
|
||||||
|
super().__init__(prefix, datasources)
|
||||||
|
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||||
|
self.router = APIRouter(prefix=prefix)
|
||||||
|
self.router.add_api_route(
|
||||||
|
"/deploymentCredentials",
|
||||||
|
self.deployment_credential,
|
||||||
|
methods=["GET"],
|
||||||
|
description="Retrieve the deployment key for a specific deployment.",
|
||||||
|
response_model=DeploymentCredential,
|
||||||
|
)
|
||||||
|
self.router.add_api_route(
|
||||||
|
"/deploymentCredentials/refresh",
|
||||||
|
self.refresh_deployment_credentials,
|
||||||
|
methods=["POST"],
|
||||||
|
description="Refresh the deployment key for a specific deployment.",
|
||||||
|
response_model=DeploymentCredential,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def deployment_credential(
|
||||||
|
self, deployment_id: str, current_user: UserInfo = Depends(get_current_user)
|
||||||
|
) -> DeploymentCredential:
|
||||||
|
"""
|
||||||
|
Get the credentials for a deployment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
deployment_id (str): The deployment id
|
||||||
|
"""
|
||||||
|
if set(current_user.groups) & set(["admin", "bec_group"]):
|
||||||
|
out = self.db.find(
|
||||||
|
"deployment_credentials", {"_id": ObjectId(deployment_id)}, DeploymentCredential
|
||||||
|
)
|
||||||
|
if len(out) > 0:
|
||||||
|
return out[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403, detail="User does not have permission to access this resource."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def refresh_deployment_credentials(
|
||||||
|
self, deployment_id: str, current_user: UserInfo = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Refresh the deployment credentials.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
deployment_id (str): The deployment id
|
||||||
|
|
||||||
|
"""
|
||||||
|
if set(current_user.groups) & set(["admin", "bec_group"]):
|
||||||
|
token = secrets.token_urlsafe(32)
|
||||||
|
out = self.db.patch(
|
||||||
|
"deployment_credentials",
|
||||||
|
id=ObjectId(deployment_id),
|
||||||
|
update={"credential": token},
|
||||||
|
dtype=DeploymentCredential,
|
||||||
|
)
|
||||||
|
if out is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Deployment not found")
|
||||||
|
|
||||||
|
# update the redis deployment key
|
||||||
|
redis: RedisDatasource = self.datasources.datasources.get("redis")
|
||||||
|
redis.add_deployment_acl(out)
|
||||||
|
|
||||||
|
return out
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403, detail="User does not have permission to access this resource."
|
||||||
|
)
|
@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends
|
|||||||
|
|
||||||
from bec_atlas.authentication import get_current_user
|
from bec_atlas.authentication import get_current_user
|
||||||
from bec_atlas.datasources.mongodb.mongodb import MongoDBDatasource
|
from bec_atlas.datasources.mongodb.mongodb import MongoDBDatasource
|
||||||
from bec_atlas.model.model import Deployments, UserInfo
|
from bec_atlas.model.model import DeploymentCredential, Deployments, UserInfo
|
||||||
from bec_atlas.router.base_router import BaseRouter
|
from bec_atlas.router.base_router import BaseRouter
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -18,14 +18,14 @@ class DeploymentsRouter(BaseRouter):
|
|||||||
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||||
self.router = APIRouter(prefix=prefix)
|
self.router = APIRouter(prefix=prefix)
|
||||||
self.router.add_api_route(
|
self.router.add_api_route(
|
||||||
"/deployments/realm/{realm}",
|
"/deployments/realm",
|
||||||
self.deployments,
|
self.deployments,
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
description="Get all deployments for the realm",
|
description="Get all deployments for the realm",
|
||||||
response_model=list[Deployments],
|
response_model=list[Deployments],
|
||||||
)
|
)
|
||||||
self.router.add_api_route(
|
self.router.add_api_route(
|
||||||
"/deployments/id/{deployment_id}",
|
"/deployments/id",
|
||||||
self.deployment_with_id,
|
self.deployment_with_id,
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
description="Get a single deployment by id for a realm",
|
description="Get a single deployment by id for a realm",
|
||||||
@ -65,10 +65,11 @@ class DeploymentsRouter(BaseRouter):
|
|||||||
Update the available deployments.
|
Update the available deployments.
|
||||||
"""
|
"""
|
||||||
self.available_deployments = self.db.find("deployments", {}, Deployments)
|
self.available_deployments = self.db.find("deployments", {}, Deployments)
|
||||||
|
credentials = self.db.find("deployment_credentials", {}, DeploymentCredential)
|
||||||
|
|
||||||
redis: RedisDatasource = self.datasources.datasources.get("redis")
|
redis: RedisDatasource = self.datasources.datasources.get("redis")
|
||||||
msg = json.dumps([msg.model_dump() for msg in self.available_deployments])
|
msg = json.dumps([msg.model_dump() for msg in self.available_deployments])
|
||||||
redis.connector.set_and_publish("deployments", msg)
|
redis.connector.set_and_publish("deployments", msg)
|
||||||
if redis.reconfigured_acls:
|
if redis.reconfigured_acls:
|
||||||
for deployment in self.available_deployments:
|
for deployment in credentials:
|
||||||
redis.add_deployment_acl(deployment)
|
redis.add_deployment_acl(deployment)
|
||||||
|
@ -20,7 +20,7 @@ class RealmRouter(BaseRouter):
|
|||||||
response_model_exclude_none=True,
|
response_model_exclude_none=True,
|
||||||
)
|
)
|
||||||
self.router.add_api_route(
|
self.router.add_api_route(
|
||||||
"/realms/{realm_id}",
|
"/realms/id",
|
||||||
self.realm_with_id,
|
self.realm_with_id,
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
description="Get a single realm by id",
|
description="Get a single realm by id",
|
||||||
|
@ -95,6 +95,19 @@ class RedisAtlasEndpoints:
|
|||||||
"""
|
"""
|
||||||
return f"internal/deployment/{deployment}/request_response/{request_id}"
|
return f"internal/deployment/{deployment}/request_response/{request_id}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def redis_bec_acl_user(deployment_id: str):
|
||||||
|
"""
|
||||||
|
Endpoint for the redis BEC ACL user for a deployment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
deployment_id (str): The deployment id
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The endpoint for the redis BEC ACL user
|
||||||
|
"""
|
||||||
|
return f"internal/deployment/{deployment_id}/bec_access"
|
||||||
|
|
||||||
|
|
||||||
class MsgResponse(Response):
|
class MsgResponse(Response):
|
||||||
media_type = "application/json"
|
media_type = "application/json"
|
||||||
@ -115,7 +128,7 @@ class RedisRouter(BaseRouter):
|
|||||||
|
|
||||||
self.router = APIRouter(prefix=prefix)
|
self.router = APIRouter(prefix=prefix)
|
||||||
self.router.add_api_route(
|
self.router.add_api_route(
|
||||||
"/redis/{deployment}", self.redis_get, methods=["GET"], response_class=MsgResponse
|
"/redis", self.redis_get, methods=["GET"], response_class=MsgResponse
|
||||||
)
|
)
|
||||||
self.router.add_api_route("/redis", self.redis_post, methods=["POST"])
|
self.router.add_api_route("/redis", self.redis_post, methods=["POST"])
|
||||||
self.router.add_api_route("/redis", self.redis_delete, methods=["DELETE"])
|
self.router.add_api_route("/redis", self.redis_delete, methods=["DELETE"])
|
||||||
|
@ -12,14 +12,14 @@ class ScanRouter(BaseRouter):
|
|||||||
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||||
self.router = APIRouter(prefix=prefix)
|
self.router = APIRouter(prefix=prefix)
|
||||||
self.router.add_api_route(
|
self.router.add_api_route(
|
||||||
"/scans/session/{session_id}",
|
"/scans/session",
|
||||||
self.scans,
|
self.scans,
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
description="Get all scans for a session",
|
description="Get all scans for a session",
|
||||||
response_model=list[ScanStatus],
|
response_model=list[ScanStatus],
|
||||||
)
|
)
|
||||||
self.router.add_api_route(
|
self.router.add_api_route(
|
||||||
"/scans/id/{scan_id}",
|
"/scans/id",
|
||||||
self.scans_with_id,
|
self.scans_with_id,
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
description="Get a single scan by id for a session",
|
description="Get a single scan by id for a session",
|
||||||
|
@ -39,13 +39,14 @@ class UserRouter(BaseRouter):
|
|||||||
return {"access_token": out, "token_type": "bearer"}
|
return {"access_token": out, "token_type": "bearer"}
|
||||||
|
|
||||||
async def user_login(self, user_login: UserLoginRequest):
|
async def user_login(self, user_login: UserLoginRequest):
|
||||||
|
exc = HTTPException(status_code=401, detail="User not found or password is incorrect")
|
||||||
user = self.db.get_user_by_email(user_login.username)
|
user = self.db.get_user_by_email(user_login.username)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise exc
|
||||||
credentials = self.db.get_user_credentials(user.id)
|
credentials = self.db.get_user_credentials(user.id)
|
||||||
if credentials is None:
|
if credentials is None:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise exc
|
||||||
if not verify_password(user_login.password, credentials.password):
|
if not verify_password(user_login.password, credentials.password):
|
||||||
raise HTTPException(status_code=401, detail="Invalid password")
|
raise exc
|
||||||
|
|
||||||
return create_access_token(data={"groups": list(user.groups), "email": user.email})
|
return create_access_token(data={"groups": list(user.groups), "email": user.email})
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import secrets
|
||||||
|
|
||||||
import pymongo
|
import pymongo
|
||||||
|
|
||||||
from bec_atlas.model import Deployments, Realm, Session
|
from bec_atlas.model import Deployments, Realm, Session
|
||||||
@ -43,8 +45,29 @@ class DemoSetupLoader:
|
|||||||
owner_groups=["admin", "demo"],
|
owner_groups=["admin", "demo"],
|
||||||
access_groups=["demo"],
|
access_groups=["demo"],
|
||||||
)
|
)
|
||||||
if self.db["deployments"].find_one({"name": deployment.name}) is None:
|
existing_deployment = self.db["deployments"].find_one({"name": deployment.name})
|
||||||
|
if existing_deployment is None:
|
||||||
self.db["deployments"].insert_one(deployment.__dict__)
|
self.db["deployments"].insert_one(deployment.__dict__)
|
||||||
|
existing_deployment = self.db["deployments"].find_one({"name": deployment.name})
|
||||||
|
deployment = existing_deployment
|
||||||
|
|
||||||
|
if self.db["deployment_credentials"].find_one({"_id": deployment["_id"]}) is None:
|
||||||
|
deployment_credential = {
|
||||||
|
"_id": deployment["_id"],
|
||||||
|
"credential": secrets.token_urlsafe(32),
|
||||||
|
}
|
||||||
|
self.db["deployment_credentials"].insert_one(deployment_credential)
|
||||||
|
deployment_access = {
|
||||||
|
"_id": deployment["_id"],
|
||||||
|
"owner_groups": ["admin", "demo"],
|
||||||
|
"access_groups": [],
|
||||||
|
"user_read_access": [],
|
||||||
|
"user_write_access": [],
|
||||||
|
"su_read_access": [],
|
||||||
|
"su_write_access": [],
|
||||||
|
"remote_access": [],
|
||||||
|
}
|
||||||
|
self.db["deployment_access"].insert_one(deployment_access)
|
||||||
|
|
||||||
if self.db["sessions"].find_one({"name": "_default_"}) is None:
|
if self.db["sessions"].find_one({"name": "_default_"}) is None:
|
||||||
deployment = self.db["deployments"].find_one({"name": deployment.name})
|
deployment = self.db["deployments"].find_one({"name": deployment.name})
|
||||||
|
@ -20,9 +20,7 @@
|
|||||||
"outputPath": "dist/bec_atlas",
|
"outputPath": "dist/bec_atlas",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"browser": "src/main.ts",
|
"browser": "src/main.ts",
|
||||||
"polyfills": [
|
"polyfills": ["zone.js"],
|
||||||
"zone.js"
|
|
||||||
],
|
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
@ -35,7 +33,7 @@
|
|||||||
"@angular/material/prebuilt-themes/cyan-orange.css",
|
"@angular/material/prebuilt-themes/cyan-orange.css",
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": ["./node_modules/plotly.js/dist/plotly.min.js"]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@ -79,10 +77,7 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"polyfills": [
|
"polyfills": ["zone.js", "zone.js/testing"],
|
||||||
"zone.js",
|
|
||||||
"zone.js/testing"
|
|
||||||
],
|
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
|
5109
frontend/bec_atlas/package-lock.json
generated
5109
frontend/bec_atlas/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,6 +23,7 @@
|
|||||||
"angular-gridster2": "^19.0.0",
|
"angular-gridster2": "^19.0.0",
|
||||||
"gridstack": "^11.1.2",
|
"gridstack": "^11.1.2",
|
||||||
"gridstack-angular": "^0.6.0-dev",
|
"gridstack-angular": "^0.6.0-dev",
|
||||||
|
"plotly.js": "^2.35.3",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
@ -33,6 +34,7 @@
|
|||||||
"@angular/cli": "^19.0.5",
|
"@angular/cli": "^19.0.5",
|
||||||
"@angular/compiler-cli": "^19.0.0",
|
"@angular/compiler-cli": "^19.0.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"@types/plotly.js": "^2.35.2",
|
||||||
"jasmine-core": "~5.4.0",
|
"jasmine-core": "~5.4.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
@ -41,4 +43,4 @@
|
|||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.6.2"
|
"typescript": "~5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
16
frontend/bec_atlas/src/app/app-config.service.spec.ts
Normal file
16
frontend/bec_atlas/src/app/app-config.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AppConfigService } from './app-config.service';
|
||||||
|
|
||||||
|
describe('AppConfigService', () => {
|
||||||
|
let service: AppConfigService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(AppConfigService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
27
frontend/bec_atlas/src/app/app-config.service.ts
Normal file
27
frontend/bec_atlas/src/app/app-config.service.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
export interface AppConfig {
|
||||||
|
baseUrl?: string;
|
||||||
|
}
|
||||||
|
@Injectable()
|
||||||
|
export class AppConfigService {
|
||||||
|
private appConfig: object | undefined = {};
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
async loadAppConfig(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.appConfig = await firstValueFrom(
|
||||||
|
this.http.get('/assets/config.json')
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('No config provided, applying defaults');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfig(): AppConfig {
|
||||||
|
return this.appConfig as AppConfig;
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<!-- <app-dashboard></app-dashboard> -->
|
<!-- <app-dashboard></app-dashboard> -->
|
||||||
<!-- <app-gridstack-test></app-gridstack-test> -->
|
<!-- <app-gridstack-test></app-gridstack-test> -->
|
||||||
<app-device-box [device]="'samx'" [signal_name]="'samx'"></app-device-box>
|
<!-- <app-device-box [device]="'samx'" [signal_name]="'samx'"></app-device-box>
|
||||||
<app-device-box [device]="'samy'" [signal_name]="'samy'"></app-device-box>
|
<app-device-box [device]="'samy'" [signal_name]="'samy'"></app-device-box>
|
||||||
<app-queue-table></app-queue-table>
|
<app-queue-table></app-queue-table> -->
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
|
@ -14,8 +14,6 @@ import { QueueTableComponent } from './queue-table/queue-table.component';
|
|||||||
DashboardComponent,
|
DashboardComponent,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GridStackTestComponent,
|
GridStackTestComponent,
|
||||||
DeviceBoxComponent,
|
|
||||||
QueueTableComponent,
|
|
||||||
],
|
],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
|
@ -1,17 +1,34 @@
|
|||||||
import {
|
import {
|
||||||
|
APP_INITIALIZER,
|
||||||
ApplicationConfig,
|
ApplicationConfig,
|
||||||
provideEnvironmentInitializer,
|
|
||||||
provideZoneChangeDetection,
|
provideZoneChangeDetection,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { GridstackComponent } from 'gridstack/dist/angular';
|
|
||||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
|
import { AppConfigService } from './app-config.service';
|
||||||
|
import {
|
||||||
|
provideHttpClient,
|
||||||
|
withInterceptorsFromDi,
|
||||||
|
} from '@angular/common/http';
|
||||||
|
|
||||||
|
const appConfigInitializerFn = (appConfig: AppConfigService) => {
|
||||||
|
return () => appConfig.loadAppConfig();
|
||||||
|
};
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes), provideAnimationsAsync(),
|
provideRouter(routes),
|
||||||
|
provideAnimationsAsync(),
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
AppConfigService,
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: appConfigInitializerFn,
|
||||||
|
deps: [AppConfigService],
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
import { LoginComponent } from './login/login.component';
|
||||||
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
|
||||||
export const routes: Routes = [];
|
export const routes: Routes = [
|
||||||
|
{ path: '', redirectTo: 'login', pathMatch: 'full' },
|
||||||
|
{ path: 'login', component: LoginComponent },
|
||||||
|
{ path: 'dashboard', component: DashboardComponent },
|
||||||
|
{ path: '**', redirectTo: 'login' }
|
||||||
|
];
|
||||||
|
16
frontend/bec_atlas/src/app/core/auth.service.spec.ts
Normal file
16
frontend/bec_atlas/src/app/core/auth.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
describe('AuthService', () => {
|
||||||
|
let service: AuthService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
39
frontend/bec_atlas/src/app/core/auth.service.ts
Normal file
39
frontend/bec_atlas/src/app/core/auth.service.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { shareReplay, timeout } from 'rxjs/operators';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { AuthDataService } from './remote-data.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
forceReload = false;
|
||||||
|
constructor(private dataService: AuthDataService) {}
|
||||||
|
|
||||||
|
login(principal: string, password: string) {
|
||||||
|
return this.dataService.login(principal, password).pipe(
|
||||||
|
timeout(3000),
|
||||||
|
tap((res) => {
|
||||||
|
this.setSession(res);
|
||||||
|
}),
|
||||||
|
shareReplay()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSession(authResult: string) {
|
||||||
|
console.log(authResult);
|
||||||
|
// it would be good to get an expiration date for the token...
|
||||||
|
localStorage.setItem('id_token', authResult);
|
||||||
|
localStorage.setItem('id_session', this.getRandomId());
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
localStorage.removeItem('id_token');
|
||||||
|
localStorage.removeItem('id_session');
|
||||||
|
this.forceReload = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomId() {
|
||||||
|
return Math.floor(Math.random() * 1000 + 1).toString();
|
||||||
|
}
|
||||||
|
}
|
3
frontend/bec_atlas/src/app/core/model/auth.ts
Normal file
3
frontend/bec_atlas/src/app/core/model/auth.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface AuthResponse {
|
||||||
|
token: string;
|
||||||
|
}
|
@ -27,13 +27,13 @@ export class RedisConnectorService {
|
|||||||
auth: {
|
auth: {
|
||||||
user: 'john_doe',
|
user: 'john_doe',
|
||||||
token: '1234',
|
token: '1234',
|
||||||
deployment: '674739bc344eabfbabcff8bd',
|
deployment: '678aa8d4875568640bd92176',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.onAny((event, ...args) => {
|
// this.socket.onAny((event, ...args) => {
|
||||||
console.log('Received event:', event, 'with data:', args);
|
// // console.log('Received event:', event, 'with data:', args);
|
||||||
});
|
// });
|
||||||
|
|
||||||
this.socket.on('connect', () => {
|
this.socket.on('connect', () => {
|
||||||
console.log('Connected to WebSocket server');
|
console.log('Connected to WebSocket server');
|
||||||
@ -41,7 +41,7 @@ export class RedisConnectorService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('message', (data: any) => {
|
this.socket.on('message', (data: any) => {
|
||||||
console.log('Received message:', data);
|
// console.log('Received message:', data);
|
||||||
const dataObj = JSON.parse(data);
|
const dataObj = JSON.parse(data);
|
||||||
const endpoint_signal = this.signals.get(dataObj.endpoint_request);
|
const endpoint_signal = this.signals.get(dataObj.endpoint_request);
|
||||||
if (endpoint_signal) {
|
if (endpoint_signal) {
|
||||||
|
@ -40,4 +40,16 @@ export class MessageEndpoints {
|
|||||||
};
|
};
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns Endpoint for device monitor 2d
|
||||||
|
*/
|
||||||
|
static device_monitor_2d(device: string): EndpointInfo {
|
||||||
|
const out: EndpointInfo = {
|
||||||
|
endpoint: 'device_monitor_2d',
|
||||||
|
args: [device],
|
||||||
|
};
|
||||||
|
return out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
16
frontend/bec_atlas/src/app/core/remote-data.service.spec.ts
Normal file
16
frontend/bec_atlas/src/app/core/remote-data.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { RemoteDataService } from './remote-data.service';
|
||||||
|
|
||||||
|
describe('RemoteDataService', () => {
|
||||||
|
let service: RemoteDataService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(RemoteDataService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
75
frontend/bec_atlas/src/app/core/remote-data.service.ts
Normal file
75
frontend/bec_atlas/src/app/core/remote-data.service.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ServerSettingsService } from '../server-settings.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class RemoteDataService {
|
||||||
|
constructor(
|
||||||
|
private httpClient: HttpClient,
|
||||||
|
private serverSettings: ServerSettingsService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base method for making a POST request to the server
|
||||||
|
* @param path path to the endpoint
|
||||||
|
* @param payload payload to send
|
||||||
|
* @param headers additional headers
|
||||||
|
* @returns response from the server
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
protected post<T>(path: string, payload: any, headers: HttpHeaders) {
|
||||||
|
return this.httpClient.post<T>(
|
||||||
|
this.serverSettings.getServerAddress() + path,
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base method for making a GET request to the server
|
||||||
|
* @param path path to the endpoint
|
||||||
|
* @param params query parameters
|
||||||
|
* @param headers additional headers
|
||||||
|
* @returns response from the server
|
||||||
|
*/
|
||||||
|
protected get<T>(
|
||||||
|
path: string,
|
||||||
|
params: { [key: string]: string },
|
||||||
|
headers: HttpHeaders
|
||||||
|
) {
|
||||||
|
return this.httpClient.get<T>(
|
||||||
|
this.serverSettings.getServerAddress() + path,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AuthDataService extends RemoteDataService {
|
||||||
|
/**
|
||||||
|
* Method for logging into BEC
|
||||||
|
* @param principal username or email
|
||||||
|
* @param password password
|
||||||
|
* @returns response from the server with the token
|
||||||
|
* @throws HttpErrorResponse if the request fails
|
||||||
|
* @throws TimeoutError if the request takes too long
|
||||||
|
*/
|
||||||
|
login(username: string, password: string) {
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.set('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
return this.post<string>(
|
||||||
|
'user/login',
|
||||||
|
{ username: username, password: password },
|
||||||
|
headers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,27 @@
|
|||||||
<div>
|
<mat-sidenav-container class="sidenav-container">
|
||||||
<div class="gridster-container-toolbar">
|
<mat-sidenav mode="side" opened>
|
||||||
<gridster [options]="toolbarOptions">
|
<button mat-button class="menu-item">
|
||||||
<gridster-item [item]="item" *ngFor="let item of dashboard; let i = index" class="gridster-item">
|
<mat-icon>account_circle</mat-icon>
|
||||||
{{ item }}
|
</button>
|
||||||
|
<button mat-button class="menu-item">
|
||||||
</gridster-item>
|
<mat-icon>home</mat-icon>
|
||||||
</gridster>
|
<span class="menu-text">Home</span>
|
||||||
</div>
|
</button>
|
||||||
|
<button mat-button class="menu-item">
|
||||||
<div class="gridster-container">
|
<mat-icon>settings</mat-icon>
|
||||||
<gridster [options]="optionsEdit">
|
<span class="menu-text">Settings</span>
|
||||||
<gridster-item [item]="item" *ngFor="let item of dashboard; let i = index" class="gridster-item">
|
</button>
|
||||||
{{ item }}
|
<button mat-button class="menu-item">
|
||||||
|
<mat-icon>help</mat-icon>
|
||||||
</gridster-item>
|
<span class="menu-text">Help</span>
|
||||||
</gridster>
|
</button>
|
||||||
</div>
|
</mat-sidenav>
|
||||||
</div>
|
|
||||||
|
<mat-sidenav-content>
|
||||||
|
<div class="content">
|
||||||
|
<app-device-box [device]="'samx'" [signal_name]="'samx'"></app-device-box>
|
||||||
|
<app-device-box [device]="'samy'" [signal_name]="'samy'"></app-device-box>
|
||||||
|
<app-queue-table></app-queue-table>
|
||||||
|
</div>
|
||||||
|
</mat-sidenav-content>
|
||||||
|
</mat-sidenav-container>
|
||||||
|
@ -1,61 +1,49 @@
|
|||||||
.gridster-container {
|
.sidenav-container {
|
||||||
height: calc(100vh - 120px);
|
height: 100vh;
|
||||||
background-color: rgb(37, 94, 75);
|
display: flex;
|
||||||
}
|
|
||||||
.gridster-container-toolbar {
|
|
||||||
height: 50px;
|
|
||||||
background-color: aqua;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gridster-container-toolbar .gridster-item {
|
mat-sidenav {
|
||||||
background-color: aqua;
|
width: 200px;
|
||||||
|
background: var(--mat-sys-primary);
|
||||||
|
color: var(--mat-sys-on-primary);
|
||||||
|
border-radius: 0px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .gridster {
|
.sidenav.collapsed {
|
||||||
background-color: #848484;
|
width: 60px;
|
||||||
padding: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .gridster-edit{
|
.menu-items {
|
||||||
background-color:#ca000093;
|
display: flex;
|
||||||
/* margin-top: 80px; */
|
flex-direction: column;
|
||||||
|
padding: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .gridster{
|
.menu-item {
|
||||||
background-color:#ca000093;
|
width: 100%;
|
||||||
/* margin-top: 80px; */
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
::ng-deep .gridster-edit{
|
justify-content: flex-start;
|
||||||
background-color:#ca000093;
|
font-size: 1rem;
|
||||||
/* margin-top: 80px; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gridster-item{
|
.menu-item.hovered {
|
||||||
padding: 10px 15px;
|
background-color: var(--mat-sys-secondary-container);
|
||||||
background-color: #848484;
|
|
||||||
color: white;
|
|
||||||
border-radius: 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// .gridster-container{
|
.menu-item mat-icon {
|
||||||
// /* top: 50px; */
|
color: var(--mat-sys-on-primary);
|
||||||
// padding-bottom: 100px;
|
margin-right: 8px;
|
||||||
// padding-right: 20px;
|
|
||||||
// padding-left: 20px;
|
|
||||||
|
|
||||||
// height: calc(100vh - 120px);
|
|
||||||
// width: 100wh;
|
|
||||||
// overflow: hidden;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.dashboard-item{
|
|
||||||
height:inherit;
|
|
||||||
overflow: inherit;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-edit{
|
.menu-item .menu-text {
|
||||||
float: right;
|
transition: opacity 0.3s ease;
|
||||||
height: 36px;
|
color: var(--mat-sys-on-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mat-sidenav-content {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
@ -1,145 +1,35 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { DeviceBoxComponent } from '../device-box/device-box.component';
|
||||||
|
import { QueueTableComponent } from '../queue-table/queue-table.component';
|
||||||
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
CompactType,
|
|
||||||
DisplayGrid,
|
|
||||||
GridsterComponent,
|
|
||||||
GridsterConfig,
|
|
||||||
GridsterItem,
|
|
||||||
GridsterItemComponent,
|
|
||||||
GridType,
|
|
||||||
} from 'angular-gridster2';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
|
imports: [
|
||||||
|
DeviceBoxComponent,
|
||||||
|
CommonModule,
|
||||||
|
QueueTableComponent,
|
||||||
|
MatSidenavModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatButtonModule,
|
||||||
|
],
|
||||||
templateUrl: './dashboard.component.html',
|
templateUrl: './dashboard.component.html',
|
||||||
styleUrl: './dashboard.component.scss',
|
styleUrl: './dashboard.component.scss',
|
||||||
imports: [CommonModule, GridsterItemComponent, GridsterComponent],
|
|
||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit {
|
export class DashboardComponent {
|
||||||
dashboard: Array<GridsterItem>;
|
// isScreenSmall = false;
|
||||||
|
|
||||||
options: GridsterConfig = {
|
constructor(private breakpointObserver: BreakpointObserver) {}
|
||||||
gridType: GridType.Fit,
|
|
||||||
compactType: CompactType.None,
|
|
||||||
margin: 1,
|
|
||||||
outerMargin: true,
|
|
||||||
outerMarginTop: null,
|
|
||||||
outerMarginRight: null,
|
|
||||||
outerMarginBottom: null,
|
|
||||||
outerMarginLeft: null,
|
|
||||||
useTransformPositioning: true,
|
|
||||||
mobileBreakpoint: 640,
|
|
||||||
minCols: 40,
|
|
||||||
maxCols: 40,
|
|
||||||
minRows: 20,
|
|
||||||
maxRows: 20,
|
|
||||||
minColWidth: 300,
|
|
||||||
maxItemCols: 100,
|
|
||||||
minItemCols: 1,
|
|
||||||
maxItemRows: 100,
|
|
||||||
minItemRows: 1,
|
|
||||||
maxItemArea: 2500,
|
|
||||||
minItemArea: 1,
|
|
||||||
defaultItemCols: 1,
|
|
||||||
defaultItemRows: 1,
|
|
||||||
// fixedColWidth: 105,
|
|
||||||
// fixedRowHeight: 105,
|
|
||||||
keepFixedHeightInMobile: false,
|
|
||||||
keepFixedWidthInMobile: false,
|
|
||||||
scrollSensitivity: 50,
|
|
||||||
scrollSpeed: 20,
|
|
||||||
enableEmptyCellClick: false,
|
|
||||||
enableEmptyCellContextMenu: false,
|
|
||||||
enableEmptyCellDrop: false,
|
|
||||||
enableEmptyCellDrag: false,
|
|
||||||
enableOccupiedCellDrop: false,
|
|
||||||
emptyCellDragMaxCols: 50,
|
|
||||||
emptyCellDragMaxRows: 50,
|
|
||||||
ignoreMarginInRow: false,
|
|
||||||
draggable: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
resizable: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
swap: true,
|
|
||||||
pushItems: true,
|
|
||||||
disablePushOnDrag: false,
|
|
||||||
disablePushOnResize: false,
|
|
||||||
pushDirections: { north: true, east: true, south: true, west: true },
|
|
||||||
pushResizeItems: false,
|
|
||||||
displayGrid: DisplayGrid.None,
|
|
||||||
disableWindowResize: false,
|
|
||||||
disableWarnings: false,
|
|
||||||
scrollToNewItems: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
optionsEdit: GridsterConfig;
|
|
||||||
toolbarOptions: GridsterConfig;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.dashboard = [];
|
|
||||||
this.optionsEdit = JSON.parse(JSON.stringify(this.options));
|
|
||||||
this.toolbarOptions = JSON.parse(JSON.stringify(this.options));
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.optionsEdit = JSON.parse(JSON.stringify(this.options)); // seriously??? I cannot believe that's the only way to perform a deep copy of an object
|
// this.breakpointObserver
|
||||||
this.optionsEdit.draggable = { enabled: true };
|
// .observe([Breakpoints.Small, Breakpoints.XSmall])
|
||||||
this.optionsEdit.resizable = { enabled: true };
|
// .subscribe((result) => {
|
||||||
this.optionsEdit.displayGrid = DisplayGrid.Always;
|
// this.isScreenSmall = result.matches;
|
||||||
this.toolbarOptions.minCols = 40;
|
// });
|
||||||
this.toolbarOptions.maxCols = 40;
|
|
||||||
this.toolbarOptions.minRows = 1;
|
|
||||||
this.toolbarOptions.maxRows = 1;
|
|
||||||
|
|
||||||
this.dashboard = [
|
|
||||||
{ cols: 2, rows: 1, y: 0, x: 0 },
|
|
||||||
{ cols: 2, rows: 2, y: 0, x: 2, hasContent: true },
|
|
||||||
{ cols: 1, rows: 1, y: 0, x: 4 },
|
|
||||||
{ cols: 1, rows: 1, y: 2, x: 5 },
|
|
||||||
{ cols: 1, rows: 1, y: 1, x: 0 },
|
|
||||||
{ cols: 1, rows: 1, y: 1, x: 0 },
|
|
||||||
{
|
|
||||||
cols: 2,
|
|
||||||
rows: 2,
|
|
||||||
y: 3,
|
|
||||||
x: 5,
|
|
||||||
minItemRows: 2,
|
|
||||||
minItemCols: 2,
|
|
||||||
label: 'Min rows & cols = 2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cols: 2,
|
|
||||||
rows: 2,
|
|
||||||
y: 2,
|
|
||||||
x: 0,
|
|
||||||
maxItemRows: 2,
|
|
||||||
maxItemCols: 2,
|
|
||||||
label: 'Max rows & cols = 2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cols: 2,
|
|
||||||
rows: 1,
|
|
||||||
y: 2,
|
|
||||||
x: 2,
|
|
||||||
dragEnabled: true,
|
|
||||||
resizeEnabled: true,
|
|
||||||
label: 'Drag&Resize Enabled',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cols: 1,
|
|
||||||
rows: 1,
|
|
||||||
y: 2,
|
|
||||||
x: 4,
|
|
||||||
dragEnabled: false,
|
|
||||||
resizeEnabled: false,
|
|
||||||
label: 'Drag&Resize Disabled',
|
|
||||||
},
|
|
||||||
{ cols: 1, rows: 1, y: 2, x: 6 },
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log('DashboardComponent initialized');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
63
frontend/bec_atlas/src/app/login/login.component.html
Normal file
63
frontend/bec_atlas/src/app/login/login.component.html
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<div class="background-image"></div>
|
||||||
|
<div class="login-form-flex">
|
||||||
|
<mat-card class="mat-card">
|
||||||
|
<mat-card-title>
|
||||||
|
<!-- <img src="assets/scilog_logo.png" alt="BEC" width="120" height="120"> -->
|
||||||
|
</mat-card-title>
|
||||||
|
<mat-card-content class="content-container">
|
||||||
|
<mat-tab-group
|
||||||
|
mat-stretch-tabs="false"
|
||||||
|
mat-align-tabs="center"
|
||||||
|
fitInkBarToContent
|
||||||
|
class="tab-container"
|
||||||
|
dynamicHeight
|
||||||
|
>
|
||||||
|
<mat-tab label="Admin">
|
||||||
|
<ng-template mat-tab-label>
|
||||||
|
<label
|
||||||
|
for="adminInput"
|
||||||
|
matTooltip="Login with functional or admin accounts."
|
||||||
|
matTooltipClass="example-tooltip-red1"
|
||||||
|
[matTooltipPosition]="'above'"
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
</label>
|
||||||
|
</ng-template>
|
||||||
|
<form [formGroup]="form" (keyup.enter)="login()" tabindex="0">
|
||||||
|
<mat-form-field appearance="outline" class="mat-form-field">
|
||||||
|
<mat-label>User name</mat-label>
|
||||||
|
<input matInput [type]="'text'" formControlName="email" />
|
||||||
|
</mat-form-field>
|
||||||
|
<br />
|
||||||
|
<mat-form-field appearance="outline" class="mat-form-field">
|
||||||
|
<mat-label>Enter your password</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[type]="hide ? 'password' : 'text'"
|
||||||
|
formControlName="password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
(mousedown)="hide = !hide"
|
||||||
|
[attr.aria-label]="'Hide password'"
|
||||||
|
[attr.aria-pressed]="hide"
|
||||||
|
tabindex="1"
|
||||||
|
>
|
||||||
|
<mat-icon>{{
|
||||||
|
hide ? "visibility_off" : "visibility"
|
||||||
|
}}</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
<div class="errorText">
|
||||||
|
{{ loginMessage }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<mat-card-actions class="mat-card-actions">
|
||||||
|
<button mat-raised-button (click)="login()">Login</button>
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
36
frontend/bec_atlas/src/app/login/login.component.scss
Normal file
36
frontend/bec_atlas/src/app/login/login.component.scss
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// @import "../styles/colors";
|
||||||
|
|
||||||
|
.login-form-flex {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
background-image: url("../../assets/psi_background.jpg");
|
||||||
|
filter: blur(4px);
|
||||||
|
-webkit-filter: blur(4px);
|
||||||
|
height: 100vh;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container {
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
23
frontend/bec_atlas/src/app/login/login.component.spec.ts
Normal file
23
frontend/bec_atlas/src/app/login/login.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
describe('LoginComponent', () => {
|
||||||
|
let component: LoginComponent;
|
||||||
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [LoginComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
87
frontend/bec_atlas/src/app/login/login.component.ts
Normal file
87
frontend/bec_atlas/src/app/login/login.component.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { MatIcon } from '@angular/material/icon';
|
||||||
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import {
|
||||||
|
UntypedFormBuilder,
|
||||||
|
UntypedFormGroup,
|
||||||
|
Validators,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import { AuthService } from '../core/auth.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { AppConfig, AppConfigService } from '../app-config.service';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
imports: [
|
||||||
|
MatCardModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatTabsModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatIcon,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrl: './login.component.scss',
|
||||||
|
})
|
||||||
|
export class LoginComponent {
|
||||||
|
hide = true;
|
||||||
|
form: UntypedFormGroup;
|
||||||
|
loginMessage = ' ';
|
||||||
|
appConfig!: AppConfig;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private appConfigService: AppConfigService,
|
||||||
|
private fb: UntypedFormBuilder,
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
this.appConfig = this.appConfigService.getConfig();
|
||||||
|
this.form = this.fb.group({
|
||||||
|
email: ['', Validators.required],
|
||||||
|
password: ['', Validators.required],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.authService.forceReload) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async login() {
|
||||||
|
const val = this.form.value;
|
||||||
|
if (val.email && val.password) {
|
||||||
|
try {
|
||||||
|
const data = await firstValueFrom(
|
||||||
|
this.authService.login(val.email, val.password)
|
||||||
|
);
|
||||||
|
console.log('User is logged in');
|
||||||
|
this.router.navigateByUrl('/dashboard');
|
||||||
|
} catch (error: unknown) {
|
||||||
|
switch ((error as HttpErrorResponse).statusText) {
|
||||||
|
case 'Unknown Error':
|
||||||
|
this.loginMessage = 'Authentication failed.';
|
||||||
|
return;
|
||||||
|
case 'Unauthorized':
|
||||||
|
this.loginMessage =
|
||||||
|
'User name / email or password are not correct.';
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
this.loginMessage = 'Authentication failed.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +1,34 @@
|
|||||||
<mat-card>
|
<mat-card>
|
||||||
<mat-toolbar color="primary">
|
<mat-toolbar color="primary"> Queue Table </mat-toolbar>
|
||||||
Queue Table
|
|
||||||
</mat-toolbar>
|
|
||||||
|
|
||||||
<table mat-table [dataSource]="tableData()" class="mat-elevation-z8">
|
<table mat-table [dataSource]="tableData()" class="mat-elevation-z8">
|
||||||
<!-- Queue ID Column -->
|
<!-- Queue ID Column -->
|
||||||
<ng-container matColumnDef="queue_id">
|
<ng-container matColumnDef="queue_id">
|
||||||
<th mat-header-cell *matHeaderCellDef> Queue ID </th>
|
<th mat-header-cell *matHeaderCellDef>Queue ID</th>
|
||||||
<td mat-cell *matCellDef="let element"> {{ element.queue_id }} </td>
|
<td mat-cell *matCellDef="let element">{{ element.queue_id }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Scan ID Column -->
|
<!-- Scan ID Column -->
|
||||||
<ng-container matColumnDef="scan_id">
|
<ng-container matColumnDef="scan_id">
|
||||||
<th mat-header-cell *matHeaderCellDef> Scan ID </th>
|
<th mat-header-cell *matHeaderCellDef>Scan ID</th>
|
||||||
<td mat-cell *matCellDef="let element"> {{ element.scan_id }} </td>
|
<td mat-cell *matCellDef="let element">{{ element.scan_id }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Scan Number Column -->
|
<!-- Scan Number Column -->
|
||||||
<ng-container matColumnDef="scan_number">
|
<ng-container matColumnDef="scan_number">
|
||||||
<th mat-header-cell *matHeaderCellDef> Scan Number </th>
|
<th mat-header-cell *matHeaderCellDef>Scan Number</th>
|
||||||
<td mat-cell *matCellDef="let element"> {{ element.scan_number }} </td>
|
<td mat-cell *matCellDef="let element">{{ element.scan_number }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Status Column -->
|
<!-- Status Column -->
|
||||||
<ng-container matColumnDef="status">
|
<ng-container matColumnDef="status">
|
||||||
<th mat-header-cell *matHeaderCellDef> Status </th>
|
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||||
<td mat-cell *matCellDef="let element"> {{ element.status }} </td>
|
|
||||||
|
<td mat-cell *matCellDef="let element">{{ element.status }}</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<!-- Header Row -->
|
<!-- Header Row -->
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
<!-- Data Rows -->
|
<!-- Data Rows -->
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||||
</table>
|
</table>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
|
|
@ -4,10 +4,11 @@ import { MatTableModule } from '@angular/material/table';
|
|||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
import { RedisConnectorService } from '../core/redis-connector.service';
|
import { RedisConnectorService } from '../core/redis-connector.service';
|
||||||
import { MessageEndpoints } from '../core/redis_endpoints';
|
import { MessageEndpoints } from '../core/redis_endpoints';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-queue-table',
|
selector: 'app-queue-table',
|
||||||
imports: [MatCardModule, MatTableModule, MatToolbarModule],
|
imports: [MatCardModule, MatTableModule, MatToolbarModule, CommonModule],
|
||||||
templateUrl: './queue-table.component.html',
|
templateUrl: './queue-table.component.html',
|
||||||
styleUrl: './queue-table.component.scss',
|
styleUrl: './queue-table.component.scss',
|
||||||
})
|
})
|
||||||
|
16
frontend/bec_atlas/src/app/server-settings.service.spec.ts
Normal file
16
frontend/bec_atlas/src/app/server-settings.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ServerSettingsService } from './server-settings.service';
|
||||||
|
|
||||||
|
describe('ServerSettingsService', () => {
|
||||||
|
let service: ServerSettingsService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(ServerSettingsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
23
frontend/bec_atlas/src/app/server-settings.service.ts
Normal file
23
frontend/bec_atlas/src/app/server-settings.service.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AppConfigService } from './app-config.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ServerSettingsService {
|
||||||
|
constructor(private appConfigService: AppConfigService) {}
|
||||||
|
|
||||||
|
getServerAddress() {
|
||||||
|
return (
|
||||||
|
this.appConfigService.getConfig().baseUrl ?? 'http://localhost/api/v1/'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSocketAddress() {
|
||||||
|
const baseUrl =
|
||||||
|
this.appConfigService.getConfig().baseUrl ?? 'http://localhost/api/v1/';
|
||||||
|
if (!baseUrl.startsWith('http'))
|
||||||
|
throw new Error('BaseURL must use the http or https protocol');
|
||||||
|
return `ws${baseUrl.substring(4)}`;
|
||||||
|
}
|
||||||
|
}
|
BIN
frontend/bec_atlas/src/assets/psi_background.jpg
Normal file
BIN
frontend/bec_atlas/src/assets/psi_background.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 763 KiB |
Reference in New Issue
Block a user