Reviewed-on: #27
This commit is contained in:
commit
9796711966
@ -11,3 +11,4 @@ Jinja2==3.1.5
|
||||
python-keycloak==5.3.1
|
||||
python-multipart==0.0.20
|
||||
websockets==15.0
|
||||
sqlparse==0.5.3
|
||||
|
@ -9,18 +9,28 @@ class Keycloak:
|
||||
client: Optional[KeycloakOpenID] = None
|
||||
admin: Optional[KeycloakAdmin] = None
|
||||
|
||||
url: Optional[str] = None
|
||||
client_id: Optional[str] = None
|
||||
realm: Optional[str] = None
|
||||
__client_secret: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def init(cls):
|
||||
cls.url = Environment.get("KEYCLOAK_URL", str)
|
||||
cls.client_id = Environment.get("KEYCLOAK_CLIENT_ID", str)
|
||||
cls.realm = Environment.get("KEYCLOAK_REALM", str)
|
||||
cls.__client_secret = Environment.get("KEYCLOAK_CLIENT_SECRET", str)
|
||||
|
||||
cls.client = KeycloakOpenID(
|
||||
server_url=Environment.get("KEYCLOAK_URL", str),
|
||||
client_id=Environment.get("KEYCLOAK_CLIENT_ID", str),
|
||||
realm_name=Environment.get("KEYCLOAK_REALM", str),
|
||||
client_secret_key=Environment.get("KEYCLOAK_CLIENT_SECRET", str),
|
||||
server_url=cls.url,
|
||||
client_id=cls.client_id,
|
||||
realm_name=cls.realm,
|
||||
client_secret_key=cls.__client_secret,
|
||||
)
|
||||
connection = KeycloakOpenIDConnection(
|
||||
server_url=Environment.get("KEYCLOAK_URL", str),
|
||||
client_id=Environment.get("KEYCLOAK_CLIENT_ID", str),
|
||||
realm_name=Environment.get("KEYCLOAK_REALM", str),
|
||||
client_secret_key=Environment.get("KEYCLOAK_CLIENT_SECRET", str),
|
||||
server_url=cls.url,
|
||||
client_id=cls.client_id,
|
||||
realm_name=cls.realm,
|
||||
client_secret_key=cls.__client_secret,
|
||||
)
|
||||
cls.admin = KeycloakAdmin(connection=connection)
|
||||
|
@ -51,6 +51,10 @@ class RouteUserExtension:
|
||||
if request is None:
|
||||
return None
|
||||
|
||||
dev_user = await cls.get_dev_user()
|
||||
if dev_user is not None:
|
||||
return dev_user
|
||||
|
||||
user_id = cls._get_user_id_from_token(request)
|
||||
if not user_id:
|
||||
return None
|
||||
@ -63,6 +67,15 @@ class RouteUserExtension:
|
||||
if request is None:
|
||||
return None
|
||||
|
||||
if "Authorization" not in request.headers:
|
||||
return None
|
||||
|
||||
if len(request.headers.get("Authorization").split()) < 2:
|
||||
return None
|
||||
|
||||
if request.headers.get("Authorization").split()[0] != "DEV-User":
|
||||
return None
|
||||
|
||||
return await userDao.find_by_keycloak_id(cls.get_token(request))
|
||||
|
||||
@classmethod
|
||||
|
72
api/src/api/routes/invitation.py
Normal file
72
api/src/api/routes/invitation.py
Normal file
@ -0,0 +1,72 @@
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import RedirectResponse, JSONResponse
|
||||
|
||||
from api.auth.keycloak_client import Keycloak
|
||||
from api.route import Route
|
||||
from core.environment import Environment
|
||||
from core.logger import Logger
|
||||
from data.schemas.administration.user import User
|
||||
from data.schemas.administration.user_dao import userDao
|
||||
from data.schemas.public.user_invitation import UserInvitation
|
||||
from data.schemas.public.user_invitation_dao import userInvitationDao
|
||||
from data.schemas.public.user_space_user import UserSpaceUser
|
||||
from data.schemas.public.user_space_user_dao import userSpaceUserDao
|
||||
|
||||
BasePath = f"/api/invitation"
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
def _redirect_to_client():
|
||||
client_urls = Environment.get("CLIENT_URLS", str)
|
||||
if client_urls is None:
|
||||
return JSONResponse({"message": "Invitation accepted."})
|
||||
|
||||
return RedirectResponse(client_urls.split(",")[0], status_code=303)
|
||||
|
||||
|
||||
@Route.get(f"{BasePath}/accept/{{invitation_id:path}}")
|
||||
async def accept_invitation(request: Request):
|
||||
invitation_id = request.path_params["invitation_id"]
|
||||
|
||||
user_invitation = await userInvitationDao.find_single_by(
|
||||
[{UserInvitation.invitation: invitation_id}]
|
||||
)
|
||||
if user_invitation is None:
|
||||
return JSONResponse({"error": "Invitation not found."}, 404)
|
||||
|
||||
if user_invitation.deleted:
|
||||
return _redirect_to_client()
|
||||
|
||||
keycloak_user = Keycloak.admin.get_users({"email": user_invitation.email})
|
||||
if len(keycloak_user) == 0:
|
||||
redirect_uri = f"{str(request.base_url)}api/invitation/accept/{invitation_id}"
|
||||
registration_url = f"{Keycloak.url}/realms/{Keycloak.realm}/protocol/openid-connect/auth?prompt=create&client_id={Keycloak.client_id}&response_type=code&scope=openid&redirect_uri={redirect_uri}"
|
||||
return RedirectResponse(registration_url, status_code=303)
|
||||
|
||||
if user_invitation.email not in [
|
||||
x.email for x in await userDao.find_by([{User.deleted: False}])
|
||||
]:
|
||||
user_id = await userDao.create(User(0, keycloak_user[0]["id"]))
|
||||
await userSpaceUserDao.create(
|
||||
UserSpaceUser(0, user_invitation.user_space_id, user_id)
|
||||
)
|
||||
|
||||
await userInvitationDao.delete(user_invitation)
|
||||
|
||||
return _redirect_to_client()
|
||||
|
||||
|
||||
@Route.get(f"{BasePath}/user-space/accept/{{invitation_id:path}}")
|
||||
async def accept_user_space_invitation(request: Request):
|
||||
invitation_id = request.path_params["invitation_id"]
|
||||
|
||||
user_space_user = await userSpaceUserDao.find_single_by(
|
||||
[{UserSpaceUser.invitation: invitation_id}, {UserSpaceUser.deleted: False}]
|
||||
)
|
||||
if user_space_user is None:
|
||||
return JSONResponse({"error": "Invitation not found or already accepted."}, 404)
|
||||
|
||||
user_space_user.invitation = None
|
||||
await userSpaceUserDao.update(user_space_user)
|
||||
|
||||
return _redirect_to_client()
|
@ -1,5 +1,5 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Type, Union
|
||||
from typing import Type, Union, Any
|
||||
|
||||
from api_graphql.abc.query_abc import QueryABC
|
||||
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
|
||||
|
@ -174,9 +174,9 @@ class QueryABC(ObjectType):
|
||||
kwargs["sort"] = None
|
||||
|
||||
if iscoroutinefunction(field.resolver):
|
||||
collection = await field.collection_resolver(*args)
|
||||
collection = await field.resolver(*args)
|
||||
else:
|
||||
collection = field.collection_resolver(*args)
|
||||
collection = field.resolver(*args)
|
||||
|
||||
return self._resolve_collection(
|
||||
collection,
|
||||
@ -240,6 +240,18 @@ class QueryABC(ObjectType):
|
||||
):
|
||||
await self._require_any_permission(field.require_any_permission)
|
||||
|
||||
if isinstance(field, MutationField):
|
||||
if field.require_any is not None:
|
||||
await self._require_any(
|
||||
None,
|
||||
*field.require_any,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
result = await resolver(*args, **kwargs)
|
||||
return result
|
||||
|
||||
result = await resolver(*args, **kwargs)
|
||||
|
||||
if field.require_any is not None:
|
||||
|
@ -15,7 +15,7 @@ class CollectionField(FieldABC):
|
||||
require_any_permission: list[Permissions] = None,
|
||||
require_any: TRequireAny = None,
|
||||
public: bool = False,
|
||||
collection_resolver: Callable = None,
|
||||
resolver: Callable = None,
|
||||
filter_type: Type[CollectionFilterABC] = None,
|
||||
sort_type: Type[T] = None,
|
||||
):
|
||||
@ -23,13 +23,13 @@ class CollectionField(FieldABC):
|
||||
self._name = name
|
||||
|
||||
self._public = public
|
||||
self._collection_resolver = collection_resolver
|
||||
self._resolver = resolver
|
||||
self._filter_type = filter_type
|
||||
self._sort_type = sort_type
|
||||
|
||||
@property
|
||||
def collection_resolver(self) -> Optional[Callable]:
|
||||
return self._collection_resolver
|
||||
def resolver(self) -> Optional[Callable]:
|
||||
return self._resolver
|
||||
|
||||
@property
|
||||
def filter_type(
|
||||
|
@ -11,13 +11,13 @@ class CollectionFieldBuilder(FieldBuilderABC):
|
||||
def __init__(self, name: str):
|
||||
FieldBuilderABC.__init__(self, name)
|
||||
|
||||
self._collection_resolver = None
|
||||
self._resolver = None
|
||||
self._filter_type = None
|
||||
self._sort_type = None
|
||||
|
||||
def with_collection_resolver(self, collection_resolver: Callable) -> Self:
|
||||
assert collection_resolver is not None, "collection_resolver cannot be None"
|
||||
self._collection_resolver = collection_resolver
|
||||
def with_resolver(self, resolver: Callable) -> Self:
|
||||
assert resolver is not None, "resolver cannot be None"
|
||||
self._resolver = resolver
|
||||
return self
|
||||
|
||||
def with_filter(self, filter_type: Type[CollectionFilterABC]) -> Self:
|
||||
@ -31,16 +31,14 @@ class CollectionFieldBuilder(FieldBuilderABC):
|
||||
return self
|
||||
|
||||
def build(self) -> CollectionField:
|
||||
assert (
|
||||
self._collection_resolver is not None
|
||||
), "collection_resolver cannot be None"
|
||||
assert self._resolver is not None, "resolver cannot be None"
|
||||
|
||||
return CollectionField(
|
||||
self._name,
|
||||
self._require_any_permission,
|
||||
self._require_any,
|
||||
self._public,
|
||||
self._collection_resolver,
|
||||
self._resolver,
|
||||
self._filter_type,
|
||||
self._sort_type,
|
||||
)
|
||||
|
@ -60,7 +60,7 @@ class MutationFieldBuilder(FieldBuilderABC):
|
||||
|
||||
def with_input(
|
||||
self,
|
||||
input_type: Type[Union[InputABC, str, int, bool]],
|
||||
input_type: Type[Union[InputABC, str, int, bool, list]],
|
||||
input_key: str = "input",
|
||||
) -> Self:
|
||||
self._input_type = input_type
|
||||
|
@ -1,18 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from api_graphql.abc.db_model_collection_filter_abc import DbModelCollectionFilterABC
|
||||
from api_graphql.abc.filter.string_filter import StringCollectionFilter
|
||||
from api_graphql.abc.collection_filter_abc import FilterOperator
|
||||
from core.typing import T
|
||||
|
||||
|
||||
class ApiKeyCollectionFilter(DbModelCollectionFilterABC):
|
||||
def __init__(
|
||||
self,
|
||||
obj: dict,
|
||||
op: Optional[FilterOperator] = None,
|
||||
source_value: Optional[T] = None,
|
||||
):
|
||||
DbModelCollectionFilterABC.__init__(self, obj, op, source_value)
|
||||
|
||||
self.add_field("identifier", StringCollectionFilter)
|
@ -1,21 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from api_graphql.abc.db_model_collection_filter_abc import DbModelCollectionFilterABC
|
||||
from api_graphql.abc.filter.string_filter import StringCollectionFilter
|
||||
from api_graphql.abc.collection_filter_abc import FilterOperator
|
||||
from core.typing import T
|
||||
|
||||
|
||||
class IpListFilter(DbModelCollectionFilterABC):
|
||||
def __init__(
|
||||
self,
|
||||
obj: dict,
|
||||
op: Optional[FilterOperator] = None,
|
||||
source_value: Optional[T] = None,
|
||||
):
|
||||
DbModelCollectionFilterABC.__init__(self, obj, op, source_value)
|
||||
|
||||
self.add_field("ip", StringCollectionFilter)
|
||||
self.add_field("description", StringCollectionFilter)
|
||||
self.add_field("mac", StringCollectionFilter)
|
||||
self.add_field("dns", StringCollectionFilter)
|
@ -1,20 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from api_graphql.abc.db_model_collection_filter_abc import DbModelCollectionFilterABC
|
||||
from api_graphql.abc.filter.bool_filter import BoolCollectionFilter
|
||||
from api_graphql.abc.filter.string_filter import StringCollectionFilter
|
||||
from api_graphql.abc.collection_filter_abc import FilterOperator
|
||||
from core.typing import T
|
||||
|
||||
|
||||
class NewsFilter(DbModelCollectionFilterABC):
|
||||
def __init__(
|
||||
self,
|
||||
obj: dict,
|
||||
op: Optional[FilterOperator] = None,
|
||||
source_value: Optional[T] = None,
|
||||
):
|
||||
DbModelCollectionFilterABC.__init__(self, obj, op, source_value)
|
||||
|
||||
self.add_field("title", StringCollectionFilter)
|
||||
self.add_field("published", BoolCollectionFilter)
|
@ -1,19 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from api_graphql.abc.db_model_collection_filter_abc import DbModelCollectionFilterABC
|
||||
from api_graphql.abc.filter.string_filter import StringCollectionFilter
|
||||
from api_graphql.abc.collection_filter_abc import FilterOperator
|
||||
from core.typing import T
|
||||
|
||||
|
||||
class PermissionFilter(DbModelCollectionFilterABC):
|
||||
def __init__(
|
||||
self,
|
||||
obj: dict,
|
||||
op: Optional[FilterOperator] = None,
|
||||
source_value: Optional[T] = None,
|
||||
):
|
||||
DbModelCollectionFilterABC.__init__(self, obj, op, source_value)
|
||||
|
||||
self.add_field("name", StringCollectionFilter)
|
||||
self.add_field("description", StringCollectionFilter)
|
@ -1,19 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from api_graphql.abc.db_model_collection_filter_abc import DbModelCollectionFilterABC
|
||||
from api_graphql.abc.filter.string_filter import StringCollectionFilter
|
||||
from api_graphql.abc.collection_filter_abc import FilterOperator
|
||||
from core.typing import T
|
||||
|
||||
|
||||
class RoleFilter(DbModelCollectionFilterABC):
|
||||
def __init__(
|
||||
self,
|
||||
obj: dict,
|
||||
op: Optional[FilterOperator] = None,
|
||||
source_value: Optional[T] = None,
|
||||
):
|
||||
DbModelCollectionFilterABC.__init__(self, obj, op, source_value)
|
||||
|
||||
self.add_field("name", StringCollectionFilter)
|
||||
self.add_field("description", StringCollectionFilter)
|
@ -1,20 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from api_graphql.abc.db_model_collection_filter_abc import DbModelCollectionFilterABC
|
||||
from api_graphql.abc.filter.string_filter import StringCollectionFilter
|
||||
from api_graphql.abc.collection_filter_abc import FilterOperator
|
||||
from core.typing import T
|
||||
|
||||
|
||||
class UserFilter(DbModelCollectionFilterABC):
|
||||
def __init__(
|
||||
self,
|
||||
obj: dict,
|
||||
op: Optional[FilterOperator] = None,
|
||||
source_value: Optional[T] = None,
|
||||
):
|
||||
DbModelCollectionFilterABC.__init__(self, obj, op, source_value)
|
||||
|
||||
self.add_field("keycloakId", StringCollectionFilter)
|
||||
self.add_field("username", StringCollectionFilter)
|
||||
self.add_field("email", StringCollectionFilter)
|
@ -1,5 +1,6 @@
|
||||
from api_graphql.abc.db_model_filter_abc import DbModelFilterABC
|
||||
from api_graphql.abc.filter.string_filter import StringFilter
|
||||
from api_graphql.filter.user_space_filter import UserSpaceFilter
|
||||
|
||||
|
||||
class GroupFilter(DbModelFilterABC):
|
||||
@ -11,6 +12,7 @@ class GroupFilter(DbModelFilterABC):
|
||||
|
||||
self.add_field("name", StringFilter)
|
||||
self.add_field("description", StringFilter)
|
||||
self.add_field("userSpace", UserSpaceFilter, "userspace")
|
||||
|
||||
self.add_field("isNull", bool)
|
||||
self.add_field("isNotNull", bool)
|
||||
|
@ -2,6 +2,7 @@ from api_graphql.abc.db_model_filter_abc import DbModelFilterABC
|
||||
from api_graphql.abc.filter.string_filter import StringFilter
|
||||
from api_graphql.filter.domain_filter import DomainFilter
|
||||
from api_graphql.filter.group_filter import GroupFilter
|
||||
from api_graphql.filter.user_space_filter import UserSpaceFilter
|
||||
|
||||
|
||||
class ShortUrlFilter(DbModelFilterABC):
|
||||
@ -17,3 +18,5 @@ class ShortUrlFilter(DbModelFilterABC):
|
||||
|
||||
self.add_field("group", GroupFilter)
|
||||
self.add_field("domain", DomainFilter)
|
||||
|
||||
self.add_field("userSpace", UserSpaceFilter, "userspace")
|
||||
|
17
api/src/api_graphql/filter/user_space_filter.py
Normal file
17
api/src/api_graphql/filter/user_space_filter.py
Normal file
@ -0,0 +1,17 @@
|
||||
from api_graphql.abc.db_model_filter_abc import DbModelFilterABC
|
||||
from api_graphql.abc.filter.string_filter import StringFilter
|
||||
from api_graphql.filter.user_filter import UserFilter
|
||||
|
||||
|
||||
class UserSpaceFilter(DbModelFilterABC):
|
||||
def __init__(
|
||||
self,
|
||||
obj: dict,
|
||||
):
|
||||
DbModelFilterABC.__init__(self, obj)
|
||||
|
||||
self.add_field("name", StringFilter)
|
||||
self.add_field("owner", UserFilter)
|
||||
|
||||
self.add_field("isNull", bool)
|
||||
self.add_field("isNotNull", bool)
|
@ -8,6 +8,8 @@ type GroupHistory implements DbHistoryModel {
|
||||
id: Int
|
||||
name: String
|
||||
|
||||
userSpace: UserSpace
|
||||
|
||||
shortUrls: [ShortUrl]
|
||||
roles: [Role]
|
||||
|
||||
@ -21,6 +23,8 @@ type Group implements DbModel {
|
||||
id: Int
|
||||
name: String
|
||||
|
||||
userSpace: UserSpace
|
||||
|
||||
shortUrls: [ShortUrl]
|
||||
roles: [Role]
|
||||
|
||||
@ -55,6 +59,8 @@ input GroupFilter {
|
||||
id: IntFilter
|
||||
name: StringFilter
|
||||
|
||||
userSpace: UserSpaceFilter
|
||||
|
||||
fuzzy: GroupFuzzy
|
||||
|
||||
deleted: BooleanFilter
|
||||
@ -74,6 +80,7 @@ type GroupMutation {
|
||||
}
|
||||
|
||||
input GroupCreateInput {
|
||||
userSpaceId: Int!
|
||||
name: String!
|
||||
roles: [Int]
|
||||
}
|
||||
|
@ -4,8 +4,10 @@ type Mutation {
|
||||
user: UserMutation
|
||||
role: RoleMutation
|
||||
|
||||
group: GroupMutation
|
||||
domain: DomainMutation
|
||||
|
||||
userSpace: UserSpaceMutation
|
||||
group: GroupMutation
|
||||
shortUrl: ShortUrlMutation
|
||||
|
||||
setting: SettingMutation
|
||||
|
@ -12,6 +12,9 @@ type Query {
|
||||
notExistingUsersFromKeycloak: [KeycloakUser]
|
||||
|
||||
domains(filter: [DomainFilter], sort: [DomainSort], skip: Int, take: Int): DomainResult
|
||||
|
||||
assignedUserSpaces: UserSpaceResult
|
||||
userSpaces(filter: [UserSpaceFilter], sort: [UserSpaceSort], skip: Int, take: Int): UserSpaceResult
|
||||
groups(filter: [GroupFilter], sort: [GroupSort], skip: Int, take: Int): GroupResult
|
||||
shortUrls(filter: [ShortUrlFilter], sort: [ShortUrlSort], skip: Int, take: Int): ShortUrlResult
|
||||
|
||||
|
@ -14,6 +14,7 @@ type ShortUrlHistory implements DbHistoryModel {
|
||||
|
||||
group: Group
|
||||
domain: Domain
|
||||
userSpace: UserSpace
|
||||
|
||||
deleted: Boolean
|
||||
editor: String
|
||||
@ -27,9 +28,11 @@ type ShortUrl implements DbModel {
|
||||
targetUrl: String
|
||||
description: String
|
||||
visits: Int
|
||||
loadingScreen: Boolean
|
||||
|
||||
group: Group
|
||||
domain: Domain
|
||||
loadingScreen: Boolean
|
||||
userSpace: UserSpace
|
||||
|
||||
deleted: Boolean
|
||||
editor: User
|
||||
@ -68,9 +71,12 @@ input ShortUrlFilter {
|
||||
targetUrl: StringFilter
|
||||
description: StringFilter
|
||||
loadingScreen: BooleanFilter
|
||||
|
||||
group: GroupFilter
|
||||
domain: DomainFilter
|
||||
|
||||
userSpace: UserSpaceFilter
|
||||
|
||||
fuzzy: ShortUrlFuzzy
|
||||
|
||||
deleted: BooleanFilter
|
||||
@ -88,6 +94,7 @@ type ShortUrlMutation {
|
||||
}
|
||||
|
||||
input ShortUrlCreateInput {
|
||||
userSpaceId: Int!
|
||||
shortUrl: String!
|
||||
targetUrl: String!
|
||||
description: String
|
||||
|
@ -12,6 +12,7 @@ type Subscription {
|
||||
userLogout: SubscriptionChange
|
||||
|
||||
domainChange: SubscriptionChange
|
||||
userSpaceChange: SubscriptionChange
|
||||
groupChange: SubscriptionChange
|
||||
shortUrlChange: SubscriptionChange
|
||||
}
|
90
api/src/api_graphql/graphql/user_space.gql
Normal file
90
api/src/api_graphql/graphql/user_space.gql
Normal file
@ -0,0 +1,90 @@
|
||||
type UserSpaceResult {
|
||||
totalCount: Int
|
||||
count: Int
|
||||
nodes: [UserSpace]
|
||||
}
|
||||
|
||||
type UserSpaceHistory implements DbHistoryModel {
|
||||
id: Int
|
||||
name: String
|
||||
owner: User
|
||||
|
||||
users: [User]
|
||||
|
||||
deleted: Boolean
|
||||
editor: String
|
||||
created: String
|
||||
updated: String
|
||||
}
|
||||
|
||||
type UserSpace implements DbModel {
|
||||
id: Int
|
||||
name: String
|
||||
owner: User
|
||||
|
||||
users: [User]
|
||||
|
||||
deleted: Boolean
|
||||
editor: User
|
||||
created: String
|
||||
updated: String
|
||||
history: [UserSpaceHistory]
|
||||
}
|
||||
|
||||
input UserSpaceSort {
|
||||
id: SortOrder
|
||||
name: SortOrder
|
||||
owner: SortOrder
|
||||
|
||||
deleted: SortOrder
|
||||
editorId: SortOrder
|
||||
created: SortOrder
|
||||
updated: SortOrder
|
||||
}
|
||||
|
||||
enum UserSpaceFuzzyFields {
|
||||
name
|
||||
owner
|
||||
}
|
||||
|
||||
input UserSpaceFuzzy {
|
||||
fields: [UserSpaceFuzzyFields]
|
||||
term: String
|
||||
threshold: Int
|
||||
}
|
||||
|
||||
input UserSpaceFilter {
|
||||
id: IntFilter
|
||||
name: StringFilter
|
||||
owner: UserFilter
|
||||
|
||||
fuzzy: UserSpaceFuzzy
|
||||
|
||||
deleted: BooleanFilter
|
||||
editor: IntFilter
|
||||
created: DateFilter
|
||||
updated: DateFilter
|
||||
|
||||
isNull: Boolean
|
||||
isNotNull: Boolean
|
||||
}
|
||||
|
||||
type UserSpaceMutation {
|
||||
create(input: UserSpaceCreateInput!): UserSpace
|
||||
update(input: UserSpaceUpdateInput!): UserSpace
|
||||
delete(id: Int!): Boolean
|
||||
restore(id: Int!): Boolean
|
||||
|
||||
inviteUsers(id: Int!, emails: [String!]!): Boolean
|
||||
}
|
||||
|
||||
input UserSpaceCreateInput {
|
||||
name: String!
|
||||
users: [Int]
|
||||
}
|
||||
|
||||
input UserSpaceUpdateInput {
|
||||
id: Int!
|
||||
name: String
|
||||
users: [Int]
|
||||
}
|
@ -6,9 +6,14 @@ class GroupCreateInput(InputABC):
|
||||
def __init__(self, src: dict):
|
||||
InputABC.__init__(self, src)
|
||||
|
||||
self._user_space_id = self.option("userSpaceId", int, required=True)
|
||||
self._name = self.option("name", str, required=True)
|
||||
self._roles = self.option("roles", list[int])
|
||||
|
||||
@property
|
||||
def user_space_id(self) -> int:
|
||||
return self._user_space_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
@ -8,6 +8,7 @@ class ShortUrlCreateInput(InputABC):
|
||||
def __init__(self, src: dict):
|
||||
InputABC.__init__(self, src)
|
||||
|
||||
self._user_space_id = self.option("userSpaceId", int, required=True)
|
||||
self._short_url = self.option("shortUrl", str, required=True)
|
||||
self._target_url = self.option("targetUrl", str, required=True)
|
||||
self._description = self.option("description", str)
|
||||
@ -15,6 +16,10 @@ class ShortUrlCreateInput(InputABC):
|
||||
self._domain_id = self.option("domainId", int)
|
||||
self._loading_screen = self.option("loadingScreen", bool)
|
||||
|
||||
@property
|
||||
def user_space_id(self) -> int:
|
||||
return self._user_space_id
|
||||
|
||||
@property
|
||||
def short_url(self) -> str:
|
||||
return self._short_url
|
||||
|
18
api/src/api_graphql/input/user_space_create_input.py
Normal file
18
api/src/api_graphql/input/user_space_create_input.py
Normal file
@ -0,0 +1,18 @@
|
||||
from api_graphql.abc.input_abc import InputABC
|
||||
|
||||
|
||||
class UserSpaceCreateInput(InputABC):
|
||||
|
||||
def __init__(self, src: dict):
|
||||
InputABC.__init__(self, src)
|
||||
|
||||
self._name = self.option("name", str, required=True)
|
||||
self._users = self.option("users", list[int])
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def users(self) -> list[int]:
|
||||
return self._users
|
23
api/src/api_graphql/input/user_space_update_input.py
Normal file
23
api/src/api_graphql/input/user_space_update_input.py
Normal file
@ -0,0 +1,23 @@
|
||||
from api_graphql.abc.input_abc import InputABC
|
||||
|
||||
|
||||
class UserSpaceUpdateInput(InputABC):
|
||||
|
||||
def __init__(self, src: dict):
|
||||
InputABC.__init__(self, src)
|
||||
|
||||
self._id = self.option("id", int, required=True)
|
||||
self._name = self.option("name", str)
|
||||
self._users = self.option("users", list[int])
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def users(self) -> list[int]:
|
||||
return self._users
|
@ -1,5 +1,6 @@
|
||||
from api_graphql.abc.mutation_abc import MutationABC
|
||||
from api_graphql.require_any_resolvers import by_user_setup_mutation
|
||||
from api_graphql.require_any_resolvers import has_assigned_user_spaces
|
||||
from api_graphql.service.query_context import QueryContext
|
||||
from service.permission.permissions_enum import Permissions
|
||||
|
||||
|
||||
@ -43,6 +44,18 @@ class Mutation(MutationABC):
|
||||
Permissions.domains_delete,
|
||||
],
|
||||
)
|
||||
self.add_mutation_type(
|
||||
"userSpace",
|
||||
"UserSpace",
|
||||
require_any=(
|
||||
[
|
||||
Permissions.user_spaces_create,
|
||||
Permissions.user_spaces_update,
|
||||
Permissions.user_spaces_delete,
|
||||
],
|
||||
[lambda ctx: True],
|
||||
),
|
||||
)
|
||||
self.add_mutation_type(
|
||||
"group",
|
||||
"Group",
|
||||
@ -52,7 +65,7 @@ class Mutation(MutationABC):
|
||||
Permissions.groups_update,
|
||||
Permissions.groups_delete,
|
||||
],
|
||||
[by_user_setup_mutation],
|
||||
[has_assigned_user_spaces],
|
||||
),
|
||||
)
|
||||
self.add_mutation_type(
|
||||
@ -64,7 +77,7 @@ class Mutation(MutationABC):
|
||||
Permissions.short_urls_update,
|
||||
Permissions.short_urls_delete,
|
||||
],
|
||||
[by_user_setup_mutation],
|
||||
[has_assigned_user_spaces],
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -1,14 +1,12 @@
|
||||
from typing import Optional
|
||||
|
||||
from api.route import Route
|
||||
from api_graphql.abc.mutation_abc import MutationABC
|
||||
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
|
||||
from api_graphql.input.group_create_input import GroupCreateInput
|
||||
from api_graphql.input.group_update_input import GroupUpdateInput
|
||||
from api_graphql.require_any_resolvers import by_user_setup_mutation
|
||||
from core.configuration.feature_flags import FeatureFlags
|
||||
from core.configuration.feature_flags_enum import FeatureFlagsEnum
|
||||
from api_graphql.require_any_resolvers import has_assigned_user_spaces
|
||||
from core.logger import APILogger
|
||||
from core.string import first_to_lower
|
||||
from data.schemas.public.group import Group
|
||||
from data.schemas.public.group_dao import groupDao
|
||||
from data.schemas.public.group_role_assignment import GroupRoleAssignment
|
||||
@ -26,26 +24,38 @@ class GroupMutation(MutationABC):
|
||||
MutationFieldBuilder("create")
|
||||
.with_resolver(self.resolve_create)
|
||||
.with_input(GroupCreateInput)
|
||||
.with_require_any([Permissions.groups_create], [by_user_setup_mutation])
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any([Permissions.groups_create], [has_assigned_user_spaces])
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("update")
|
||||
.with_resolver(self.resolve_update)
|
||||
.with_input(GroupUpdateInput)
|
||||
.with_require_any([Permissions.groups_update], [by_user_setup_mutation])
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any([Permissions.groups_update], [has_assigned_user_spaces])
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("delete")
|
||||
.with_resolver(self.resolve_delete)
|
||||
.with_require_any([Permissions.groups_delete], [by_user_setup_mutation])
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any([Permissions.groups_delete], [has_assigned_user_spaces])
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("restore")
|
||||
.with_resolver(self.resolve_restore)
|
||||
.with_require_any([Permissions.groups_delete], [by_user_setup_mutation])
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any([Permissions.groups_delete], [has_assigned_user_spaces])
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@ -87,12 +97,9 @@ class GroupMutation(MutationABC):
|
||||
group = Group(
|
||||
0,
|
||||
obj.name,
|
||||
(
|
||||
(await Route.get_user()).id
|
||||
if await FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup)
|
||||
else None
|
||||
),
|
||||
obj.user_space_id,
|
||||
)
|
||||
|
||||
gid = await groupDao.create(group)
|
||||
|
||||
await cls._handle_group_role_assignments(gid, obj.roles)
|
||||
@ -107,9 +114,7 @@ class GroupMutation(MutationABC):
|
||||
raise ValueError(f"Group with id {obj.id} not found")
|
||||
|
||||
if obj.name is not None:
|
||||
already_exists = await groupDao.find_by(
|
||||
{Group.name: obj.name, Group.id: {"ne": obj.id}}
|
||||
)
|
||||
already_exists = await groupDao.find_by({Group.name: obj.name})
|
||||
if len(already_exists) > 0:
|
||||
raise ValueError(f"Group {obj.name} already exists")
|
||||
|
||||
|
@ -3,10 +3,9 @@ from api_graphql.abc.mutation_abc import MutationABC
|
||||
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
|
||||
from api_graphql.input.short_url_create_input import ShortUrlCreateInput
|
||||
from api_graphql.input.short_url_update_input import ShortUrlUpdateInput
|
||||
from api_graphql.require_any_resolvers import by_user_setup_mutation
|
||||
from core.configuration.feature_flags import FeatureFlags
|
||||
from core.configuration.feature_flags_enum import FeatureFlagsEnum
|
||||
from api_graphql.require_any_resolvers import has_assigned_user_spaces
|
||||
from core.logger import APILogger
|
||||
from core.string import first_to_lower
|
||||
from data.schemas.public.domain_dao import domainDao
|
||||
from data.schemas.public.group_dao import groupDao
|
||||
from data.schemas.public.short_url import ShortUrl
|
||||
@ -26,31 +25,54 @@ class ShortUrlMutation(MutationABC):
|
||||
MutationFieldBuilder("create")
|
||||
.with_resolver(self.resolve_create)
|
||||
.with_input(ShortUrlCreateInput)
|
||||
.with_require_any([Permissions.short_urls_create], [by_user_setup_mutation])
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any(
|
||||
[Permissions.short_urls_create], [has_assigned_user_spaces]
|
||||
)
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("update")
|
||||
.with_resolver(self.resolve_update)
|
||||
.with_input(ShortUrlUpdateInput)
|
||||
.with_require_any([Permissions.short_urls_update], [by_user_setup_mutation])
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any(
|
||||
[Permissions.short_urls_update], [has_assigned_user_spaces]
|
||||
)
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("delete")
|
||||
.with_resolver(self.resolve_delete)
|
||||
.with_require_any([Permissions.short_urls_delete], [by_user_setup_mutation])
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any(
|
||||
[Permissions.short_urls_delete], [has_assigned_user_spaces]
|
||||
)
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("restore")
|
||||
.with_resolver(self.resolve_restore)
|
||||
.with_require_any([Permissions.short_urls_delete], [by_user_setup_mutation])
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any(
|
||||
[Permissions.short_urls_delete], [has_assigned_user_spaces]
|
||||
)
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("trackVisit")
|
||||
.with_resolver(self.resolve_track_visit)
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any_permission([Permissions.short_urls_update])
|
||||
)
|
||||
|
||||
@ -70,11 +92,7 @@ class ShortUrlMutation(MutationABC):
|
||||
obj.group_id,
|
||||
obj.domain_id,
|
||||
obj.loading_screen,
|
||||
(
|
||||
(await Route.get_user()).id
|
||||
if await FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup)
|
||||
else None
|
||||
),
|
||||
obj.user_space_id,
|
||||
)
|
||||
nid = await shortUrlDao.create(short_url)
|
||||
return await shortUrlDao.get_by_id(nid)
|
||||
|
@ -22,12 +22,12 @@ class UserSettingMutation(MutationABC):
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_input(UserSettingInput, "input")
|
||||
.with_require_any([], [self._x])
|
||||
.with_require_any([], [self._check_user])
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _x(ctx):
|
||||
return ctx.data.user_id == (await Route.get_user()).id
|
||||
async def _check_user(ctx):
|
||||
return ctx.user.id == (await Route.get_user()).id
|
||||
|
||||
@staticmethod
|
||||
async def resolve_change(obj: UserSettingInput, *_):
|
||||
|
221
api/src/api_graphql/mutations/user_space_mutation.py
Normal file
221
api/src/api_graphql/mutations/user_space_mutation.py
Normal file
@ -0,0 +1,221 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from api.middleware.request import get_request
|
||||
from api.route import Route
|
||||
from api_graphql.abc.mutation_abc import MutationABC
|
||||
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
|
||||
from api_graphql.input.user_space_create_input import UserSpaceCreateInput
|
||||
from api_graphql.input.user_space_update_input import UserSpaceUpdateInput
|
||||
from api_graphql.service.query_context import QueryContext
|
||||
from core.logger import APILogger
|
||||
from core.string import first_to_lower
|
||||
from data.schemas.administration.user_dao import userDao
|
||||
from data.schemas.public.user_invitation import UserInvitation
|
||||
from data.schemas.public.user_invitation_dao import userInvitationDao
|
||||
from data.schemas.public.user_space import UserSpace
|
||||
from data.schemas.public.user_space_dao import userSpaceDao
|
||||
from data.schemas.public.user_space_user import UserSpaceUser
|
||||
from data.schemas.public.user_space_user_dao import userSpaceUserDao
|
||||
from service.mail_service import mailService
|
||||
from service.permission.permissions_enum import Permissions
|
||||
|
||||
logger = APILogger(__name__)
|
||||
|
||||
|
||||
class UserSpaceMutation(MutationABC):
|
||||
def __init__(self):
|
||||
MutationABC.__init__(self, "UserSpace")
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("create")
|
||||
.with_resolver(self.resolve_create)
|
||||
.with_input(UserSpaceCreateInput)
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("update")
|
||||
.with_resolver(self.resolve_update)
|
||||
.with_input(UserSpaceUpdateInput)
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any(
|
||||
[Permissions.user_spaces_update],
|
||||
[self._resolve_input_user_space_assigned],
|
||||
)
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("delete")
|
||||
.with_resolver(self.resolve_delete)
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any(
|
||||
[Permissions.user_spaces_delete],
|
||||
[self._resolve_input_user_space_assigned],
|
||||
)
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("restore")
|
||||
.with_resolver(self.resolve_restore)
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any_permission([Permissions.user_spaces_delete])
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("inviteUsers")
|
||||
.with_resolver(self.resolve_invite_users)
|
||||
.with_input(list, "emails")
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any(
|
||||
[Permissions.user_spaces_update],
|
||||
[self._resolve_input_user_space_assigned],
|
||||
)
|
||||
)
|
||||
|
||||
async def resolve_create(self, obj: UserSpaceCreateInput, *_):
|
||||
logger.debug(f"create user_space: {obj.__dict__}")
|
||||
|
||||
already_exists = await userSpaceDao.find_by({UserSpace.name: obj.name})
|
||||
if len(already_exists) > 0:
|
||||
raise ValueError(f"UserSpace {obj.name} already exists")
|
||||
|
||||
user_space = UserSpace(0, obj.name, (await Route.get_user()).id)
|
||||
gid = await userSpaceDao.create(user_space)
|
||||
|
||||
await self._resolve_assignments(
|
||||
obj.get("users", []),
|
||||
user_space,
|
||||
UserSpaceUser.user_space_id,
|
||||
UserSpaceUser.user_id,
|
||||
userSpaceDao,
|
||||
userSpaceUserDao,
|
||||
UserSpaceUser,
|
||||
userDao,
|
||||
)
|
||||
|
||||
return await userSpaceDao.get_by_id(gid)
|
||||
|
||||
async def resolve_update(self, obj: UserSpaceUpdateInput, *_):
|
||||
logger.debug(f"update user_space: {input}")
|
||||
|
||||
if await userSpaceDao.find_by_id(obj.id) is None:
|
||||
raise ValueError(f"UserSpace with id {obj.id} not found")
|
||||
|
||||
user_space = await userSpaceDao.get_by_id(obj.id)
|
||||
if obj.name is not None:
|
||||
already_exists = await userSpaceDao.find_by({UserSpace.name: obj.name})
|
||||
if len(already_exists) > 0:
|
||||
raise ValueError(f"UserSpace {obj.name} already exists")
|
||||
|
||||
user_space.name = obj.name
|
||||
await userSpaceDao.update(user_space)
|
||||
|
||||
await self._resolve_assignments(
|
||||
obj.get("users", []),
|
||||
user_space,
|
||||
UserSpaceUser.user_space_id,
|
||||
UserSpaceUser.user_id,
|
||||
userSpaceDao,
|
||||
userSpaceUserDao,
|
||||
UserSpaceUser,
|
||||
userDao,
|
||||
)
|
||||
|
||||
return await userSpaceDao.get_by_id(obj.id)
|
||||
|
||||
@staticmethod
|
||||
async def resolve_delete(*_, id: str):
|
||||
logger.debug(f"delete user_space: {id}")
|
||||
user_space = await userSpaceDao.get_by_id(id)
|
||||
await userSpaceDao.delete(user_space)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def resolve_restore(*_, id: str):
|
||||
logger.debug(f"restore user_space: {id}")
|
||||
user_space = await userSpaceDao.get_by_id(id)
|
||||
await userSpaceDao.restore(user_space)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def resolve_invite_users(emails: list[str], *_, id: str):
|
||||
logger.debug(f"invite users to user_space: {id} with emails: {emails}")
|
||||
|
||||
user_space = await userSpaceDao.get_by_id(id)
|
||||
user_space_user_emails = [
|
||||
x.email for x in await userSpaceDao.get_users(user_space.id)
|
||||
]
|
||||
|
||||
email_invitations = {}
|
||||
|
||||
base_url = str(get_request().base_url)
|
||||
for email in emails:
|
||||
if email in user_space_user_emails:
|
||||
continue
|
||||
user = await userDao.find_single_by({"email": {"equal": email}})
|
||||
|
||||
invitation = uuid4()
|
||||
if user is None:
|
||||
user_invitation = await userInvitationDao.find_by(
|
||||
[
|
||||
{UserInvitation.email: email},
|
||||
{UserInvitation.user_space_id: user_space.id},
|
||||
]
|
||||
)
|
||||
if len(user_invitation) > 0:
|
||||
continue
|
||||
|
||||
await userInvitationDao.create(
|
||||
UserInvitation(
|
||||
0,
|
||||
email,
|
||||
str(invitation),
|
||||
user_space.id,
|
||||
)
|
||||
)
|
||||
email_invitations[email] = (
|
||||
f"{base_url}api/invitation/accept/{invitation}"
|
||||
)
|
||||
continue
|
||||
|
||||
email_invitations[email] = (
|
||||
f"{base_url}api/invitation/user-space/accept/{invitation}"
|
||||
)
|
||||
await userSpaceUserDao.create(
|
||||
UserSpaceUser(0, user_space.id, user.id, str(invitation))
|
||||
)
|
||||
|
||||
for email, invitation in email_invitations.items():
|
||||
logger.debug(f"Invitation for {email}: {invitation}")
|
||||
mailService.send(
|
||||
email,
|
||||
"You're invited to join Open-Redirect",
|
||||
f"Hi,\n\nYou’ve been invited to join Open-Redirect, a secure collaboration platform for your team. "
|
||||
f"To complete your registration, please use the following invitation code:\n\n"
|
||||
f"{invitation}\n\n"
|
||||
"If you weren’t expecting this invitation, you can safely ignore this message.\n\n"
|
||||
"Thanks,\n"
|
||||
"The Open-Redirect Team",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def _resolve_input_user_space_assigned(ctx: QueryContext):
|
||||
check_dict = ctx.kwargs
|
||||
if "input" in ctx.kwargs:
|
||||
check_dict = ctx.kwargs["input"]
|
||||
|
||||
return "id" in check_dict and check_dict["id"] in [
|
||||
x.id for x in await userSpaceDao.get_assigned_by_user_id(ctx.user.id)
|
||||
]
|
@ -1,6 +1,6 @@
|
||||
from api_graphql.abc.db_history_model_query_abc import DbHistoryModelQueryABC
|
||||
from api_graphql.field.resolver_field_builder import ResolverFieldBuilder
|
||||
from api_graphql.require_any_resolvers import by_assignment_resolver
|
||||
from api_graphql.require_any_resolvers import by_group_assignment_resolver
|
||||
from data.schemas.public.group import Group
|
||||
from data.schemas.public.group_dao import groupDao
|
||||
from data.schemas.public.group_role_assignment_dao import groupRoleAssignmentDao
|
||||
@ -14,6 +14,7 @@ class GroupHistoryQuery(DbHistoryModelQueryABC):
|
||||
DbHistoryModelQueryABC.__init__(self, "Group")
|
||||
|
||||
self.set_field("name", lambda x, *_: x.name)
|
||||
self.set_field("userSpace", lambda x, *_: x.user_space)
|
||||
self.field(
|
||||
ResolverFieldBuilder("shortUrls")
|
||||
.with_resolver(self._get_urls)
|
||||
@ -21,7 +22,7 @@ class GroupHistoryQuery(DbHistoryModelQueryABC):
|
||||
[
|
||||
Permissions.groups,
|
||||
],
|
||||
[by_assignment_resolver],
|
||||
[by_group_assignment_resolver],
|
||||
)
|
||||
)
|
||||
self.set_field(
|
||||
|
@ -1,6 +1,7 @@
|
||||
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
|
||||
from api_graphql.field.resolver_field_builder import ResolverFieldBuilder
|
||||
from api_graphql.require_any_resolvers import by_assignment_resolver
|
||||
from api_graphql.require_any_resolvers import by_group_assignment_resolver
|
||||
|
||||
from data.schemas.public.group import Group
|
||||
from data.schemas.public.group_dao import groupDao
|
||||
from data.schemas.public.group_role_assignment_dao import groupRoleAssignmentDao
|
||||
@ -14,6 +15,7 @@ class GroupQuery(DbModelQueryABC):
|
||||
DbModelQueryABC.__init__(self, "Group", groupDao, with_history=True)
|
||||
|
||||
self.set_field("name", lambda x, *_: x.name)
|
||||
self.set_field("userSpace", lambda x, *_: x.user_space)
|
||||
self.field(
|
||||
ResolverFieldBuilder("shortUrls")
|
||||
.with_resolver(self._get_urls)
|
||||
@ -21,7 +23,7 @@ class GroupQuery(DbModelQueryABC):
|
||||
[
|
||||
Permissions.groups,
|
||||
],
|
||||
[by_assignment_resolver],
|
||||
[by_group_assignment_resolver],
|
||||
)
|
||||
)
|
||||
self.set_field("roles", self._get_roles)
|
||||
|
@ -8,7 +8,10 @@ class ShortUrlQuery(DbHistoryModelQueryABC):
|
||||
self.set_field("shortUrl", lambda x, *_: x.short_url)
|
||||
self.set_field("targetUrl", lambda x, *_: x.target_url)
|
||||
self.set_field("description", lambda x, *_: x.description)
|
||||
self.set_field("group", lambda x, *_: x.group)
|
||||
self.set_field("domain", lambda x, *_: x.domain)
|
||||
self.set_field("visits", lambda x, *_: x.visit_count)
|
||||
self.set_field("loadingScreen", lambda x, *_: x.loading_screen)
|
||||
|
||||
self.set_field("group", lambda x, *_: x.group)
|
||||
self.set_field("domain", lambda x, *_: x.domain)
|
||||
|
||||
self.set_field("userSpace", lambda x, *_: x.user_space)
|
||||
|
@ -9,7 +9,10 @@ class ShortUrlQuery(DbModelQueryABC):
|
||||
self.set_field("shortUrl", lambda x, *_: x.short_url)
|
||||
self.set_field("targetUrl", lambda x, *_: x.target_url)
|
||||
self.set_field("description", lambda x, *_: x.description)
|
||||
self.set_field("group", lambda x, *_: x.group)
|
||||
self.set_field("domain", lambda x, *_: x.domain)
|
||||
self.set_field("visits", lambda x, *_: x.visit_count)
|
||||
self.set_field("loadingScreen", lambda x, *_: x.loading_screen)
|
||||
|
||||
self.set_field("group", lambda x, *_: x.group)
|
||||
self.set_field("domain", lambda x, *_: x.domain)
|
||||
|
||||
self.set_field("userSpace", lambda x, *_: x.user_space)
|
||||
|
22
api/src/api_graphql/queries/user_space_history_query.py
Normal file
22
api/src/api_graphql/queries/user_space_history_query.py
Normal file
@ -0,0 +1,22 @@
|
||||
from api_graphql.abc.db_history_model_query_abc import DbHistoryModelQueryABC
|
||||
from data.schemas.administration.user_dao import userDao
|
||||
from data.schemas.public.user_space_user_dao import userSpaceUserDao
|
||||
|
||||
|
||||
class UserSpaceHistoryQuery(DbHistoryModelQueryABC):
|
||||
def __init__(self):
|
||||
DbHistoryModelQueryABC.__init__(self, "UserSpace")
|
||||
|
||||
self.set_field("name", lambda x, *_: x.name)
|
||||
self.set_field("owner", lambda x, *_: x.owner)
|
||||
self.set_field(
|
||||
"users",
|
||||
lambda x, *_: self._resolve_foreign_history(
|
||||
x.updated,
|
||||
x.id,
|
||||
userSpaceUserDao,
|
||||
userDao,
|
||||
lambda y: y.user_id,
|
||||
obj_key="userspaceid",
|
||||
),
|
||||
)
|
21
api/src/api_graphql/queries/user_space_query.py
Normal file
21
api/src/api_graphql/queries/user_space_query.py
Normal file
@ -0,0 +1,21 @@
|
||||
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
|
||||
from data.schemas.public.group_dao import groupDao
|
||||
from data.schemas.public.short_url_dao import shortUrlDao
|
||||
from data.schemas.public.user_space import UserSpace
|
||||
from data.schemas.public.user_space_dao import userSpaceDao
|
||||
|
||||
|
||||
class UserSpaceQuery(DbModelQueryABC):
|
||||
def __init__(self):
|
||||
DbModelQueryABC.__init__(self, "UserSpace", userSpaceDao, with_history=True)
|
||||
|
||||
self.set_field("name", lambda x, *_: x.name)
|
||||
self.set_field("owner", lambda x, *_: x.owner)
|
||||
self.set_field("users", self._get_users)
|
||||
|
||||
self.set_history_reference_dao(groupDao, "userspaceid")
|
||||
self.set_history_reference_dao(shortUrlDao, "userspaceid")
|
||||
|
||||
@staticmethod
|
||||
async def _get_users(space: UserSpace, *_):
|
||||
return await userSpaceDao.get_users(space.id, with_invitations=False)
|
@ -2,6 +2,7 @@ from api.auth.keycloak_client import Keycloak
|
||||
from api.route import Route
|
||||
from api_graphql.abc.query_abc import QueryABC
|
||||
from api_graphql.abc.sort_abc import Sort
|
||||
from api_graphql.field.collection_field_builder import CollectionFieldBuilder
|
||||
from api_graphql.field.dao_field_builder import DaoFieldBuilder
|
||||
from api_graphql.field.resolver_field_builder import ResolverFieldBuilder
|
||||
from api_graphql.filter.api_key_filter import ApiKeyFilter
|
||||
@ -11,12 +12,12 @@ from api_graphql.filter.permission_filter import PermissionFilter
|
||||
from api_graphql.filter.role_filter import RoleFilter
|
||||
from api_graphql.filter.short_url_filter import ShortUrlFilter
|
||||
from api_graphql.filter.user_filter import UserFilter
|
||||
from api_graphql.filter.user_space_filter import UserSpaceFilter
|
||||
from api_graphql.require_any_resolvers import (
|
||||
by_assignment_resolver,
|
||||
by_user_setup_resolver,
|
||||
by_group_assignment_resolver,
|
||||
by_user_space_assignment_resolver,
|
||||
has_assigned_user_spaces,
|
||||
)
|
||||
from core.configuration.feature_flags import FeatureFlags
|
||||
from core.configuration.feature_flags_enum import FeatureFlagsEnum
|
||||
from data.schemas.administration.api_key import ApiKey
|
||||
from data.schemas.administration.api_key_dao import apiKeyDao
|
||||
from data.schemas.administration.user import User
|
||||
@ -33,6 +34,8 @@ from data.schemas.public.short_url import ShortUrl
|
||||
from data.schemas.public.short_url_dao import shortUrlDao
|
||||
from data.schemas.public.user_setting import UserSetting
|
||||
from data.schemas.public.user_setting_dao import userSettingDao
|
||||
from data.schemas.public.user_space import UserSpace
|
||||
from data.schemas.public.user_space_dao import userSpaceDao
|
||||
from data.schemas.system.feature_flag_dao import featureFlagDao
|
||||
from data.schemas.system.setting_dao import settingDao
|
||||
from service.permission.permissions_enum import Permissions
|
||||
@ -56,19 +59,21 @@ class Query(QueryABC):
|
||||
.with_filter(PermissionFilter)
|
||||
.with_sort(Sort[Permission])
|
||||
)
|
||||
|
||||
self.field(
|
||||
DaoFieldBuilder("roles")
|
||||
.with_dao(roleDao)
|
||||
.with_filter(RoleFilter)
|
||||
.with_sort(Sort[Role])
|
||||
.with_require_any_permission(
|
||||
.with_require_any(
|
||||
[
|
||||
Permissions.roles,
|
||||
Permissions.users_create,
|
||||
Permissions.users_update,
|
||||
Permissions.groups_create,
|
||||
Permissions.groups_update,
|
||||
]
|
||||
],
|
||||
[has_assigned_user_spaces],
|
||||
)
|
||||
)
|
||||
|
||||
@ -106,16 +111,36 @@ class Query(QueryABC):
|
||||
.with_dao(domainDao)
|
||||
.with_filter(DomainFilter)
|
||||
.with_sort(Sort[Domain])
|
||||
.with_require_any_permission(
|
||||
.with_require_any(
|
||||
[
|
||||
Permissions.domains,
|
||||
Permissions.short_urls_create,
|
||||
Permissions.short_urls_update,
|
||||
]
|
||||
Permissions.domains_create,
|
||||
Permissions.domains_update,
|
||||
],
|
||||
[has_assigned_user_spaces],
|
||||
)
|
||||
)
|
||||
|
||||
group_field = (
|
||||
self.field(
|
||||
CollectionFieldBuilder("assignedUserSpaces").with_resolver(
|
||||
self._resolve_assigned_user_spaces
|
||||
)
|
||||
)
|
||||
|
||||
self.field(
|
||||
DaoFieldBuilder("userSpaces")
|
||||
.with_dao(userSpaceDao)
|
||||
.with_filter(UserSpaceFilter)
|
||||
.with_sort(Sort[UserSpace])
|
||||
.with_require_any(
|
||||
[
|
||||
Permissions.user_spaces,
|
||||
],
|
||||
[lambda ctx: all(x.owner_id == ctx.user.id for x in ctx.data.nodes)],
|
||||
)
|
||||
)
|
||||
|
||||
self.field(
|
||||
DaoFieldBuilder("groups")
|
||||
.with_dao(groupDao)
|
||||
.with_filter(GroupFilter)
|
||||
@ -126,35 +151,21 @@ class Query(QueryABC):
|
||||
Permissions.short_urls_create,
|
||||
Permissions.short_urls_update,
|
||||
],
|
||||
[by_assignment_resolver, by_user_setup_resolver],
|
||||
[by_user_space_assignment_resolver, by_group_assignment_resolver],
|
||||
)
|
||||
)
|
||||
|
||||
if FeatureFlags.get_default(FeatureFlagsEnum.per_user_setup):
|
||||
group_field = group_field.with_default_filter(
|
||||
self._resolve_default_user_filter
|
||||
)
|
||||
|
||||
self.field(group_field)
|
||||
|
||||
short_url_field = (
|
||||
self.field(
|
||||
DaoFieldBuilder("shortUrls")
|
||||
.with_dao(shortUrlDao)
|
||||
.with_filter(ShortUrlFilter)
|
||||
.with_sort(Sort[ShortUrl])
|
||||
.with_require_any(
|
||||
[Permissions.short_urls],
|
||||
[by_assignment_resolver, by_user_setup_resolver],
|
||||
[by_user_space_assignment_resolver, by_group_assignment_resolver],
|
||||
)
|
||||
)
|
||||
|
||||
if FeatureFlags.get_default(FeatureFlagsEnum.per_user_setup):
|
||||
short_url_field = short_url_field.with_default_filter(
|
||||
self._resolve_default_user_filter
|
||||
)
|
||||
|
||||
self.field(short_url_field)
|
||||
|
||||
self.field(
|
||||
ResolverFieldBuilder("settings")
|
||||
.with_resolver(self._resolve_settings)
|
||||
@ -230,3 +241,11 @@ class Query(QueryABC):
|
||||
@staticmethod
|
||||
async def _resolve_default_user_filter(*args, **kwargs) -> dict:
|
||||
return {"user": {"id": {"equal": (await Route.get_user()).id}}}
|
||||
|
||||
@staticmethod
|
||||
async def _resolve_assigned_user_spaces(*args, **kwargs):
|
||||
user = await Route.get_user()
|
||||
if user is None:
|
||||
return []
|
||||
|
||||
return await userSpaceDao.get_assigned_by_user_id(user.id)
|
||||
|
@ -1,12 +1,36 @@
|
||||
from api_graphql.service.collection_result import CollectionResult
|
||||
from api_graphql.service.query_context import QueryContext
|
||||
from core.configuration.feature_flags import FeatureFlags
|
||||
from core.configuration.feature_flags_enum import FeatureFlagsEnum
|
||||
from data.schemas.public.group_dao import groupDao
|
||||
from data.schemas.public.user_space_dao import userSpaceDao
|
||||
from data.schemas.public.user_space_user_dao import userSpaceUserDao
|
||||
from service.permission.permissions_enum import Permissions
|
||||
|
||||
|
||||
async def by_assignment_resolver(ctx: QueryContext) -> bool:
|
||||
async def by_user_space_assignment_resolver(ctx: QueryContext) -> bool:
|
||||
if not isinstance(ctx.data, CollectionResult):
|
||||
return False
|
||||
|
||||
if len(ctx.data.nodes) == 0:
|
||||
return True
|
||||
|
||||
user = ctx.user
|
||||
|
||||
assigned_user_space_ids = {
|
||||
us.user_space_id for us in await userSpaceUserDao.get_by_user_id(user.id)
|
||||
}
|
||||
|
||||
for node in ctx.data.nodes:
|
||||
user_space = await node.user_space
|
||||
if user_space is None:
|
||||
continue
|
||||
|
||||
if user_space.owner_id == user.id or user_space.id in assigned_user_space_ids:
|
||||
return True
|
||||
|
||||
return len(ctx.data.nodes) == 0
|
||||
|
||||
|
||||
async def by_group_assignment_resolver(ctx: QueryContext) -> bool:
|
||||
if not isinstance(ctx.data, CollectionResult):
|
||||
return False
|
||||
|
||||
@ -29,15 +53,6 @@ async def by_assignment_resolver(ctx: QueryContext) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def by_user_setup_resolver(ctx: QueryContext) -> bool:
|
||||
if not isinstance(ctx.data, CollectionResult):
|
||||
return False
|
||||
|
||||
if not FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup):
|
||||
return False
|
||||
|
||||
return all(x.user_id == ctx.user.id for x in ctx.data.nodes)
|
||||
|
||||
|
||||
async def by_user_setup_mutation(ctx: QueryContext) -> bool:
|
||||
return await FeatureFlags.has_feature(FeatureFlagsEnum.per_user_setup)
|
||||
async def has_assigned_user_spaces(ctx: QueryContext):
|
||||
user_spaces = await userSpaceDao.get_assigned_by_user_id(ctx.user.id)
|
||||
return len(user_spaces) > 0
|
||||
|
@ -1,5 +1,10 @@
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from api.broadcast import broadcast
|
||||
from api.route import Route
|
||||
from api_graphql.abc.subscription_abc import SubscriptionABC
|
||||
from api_graphql.field.subscription_field_builder import SubscriptionFieldBuilder
|
||||
from api_graphql.require_any_resolvers import has_assigned_user_spaces
|
||||
from service.permission.permissions_enum import Permissions
|
||||
|
||||
|
||||
@ -49,9 +54,16 @@ class Subscription(SubscriptionABC):
|
||||
.with_public(True)
|
||||
)
|
||||
|
||||
async def _user_logout_generator(*args, **kwargs) -> AsyncGenerator[str, None]:
|
||||
async with broadcast.subscribe(channel="userLogout") as subscriber:
|
||||
async for message in subscriber:
|
||||
if message.message == (await Route.get_user()).id:
|
||||
yield message
|
||||
|
||||
self.subscribe(
|
||||
SubscriptionFieldBuilder("userLogout")
|
||||
.with_resolver(lambda message, *_: message.message)
|
||||
.with_generator(_user_logout_generator)
|
||||
.with_public(True)
|
||||
)
|
||||
|
||||
@ -60,13 +72,18 @@ class Subscription(SubscriptionABC):
|
||||
.with_resolver(lambda message, *_: message.message)
|
||||
.with_require_any_permission([Permissions.domains])
|
||||
)
|
||||
self.subscribe(
|
||||
SubscriptionFieldBuilder("userSpaceChange")
|
||||
.with_resolver(lambda message, *_: message.message)
|
||||
.with_public(True)
|
||||
)
|
||||
self.subscribe(
|
||||
SubscriptionFieldBuilder("groupChange")
|
||||
.with_resolver(lambda message, *_: message.message)
|
||||
.with_require_any_permission([Permissions.groups])
|
||||
.with_require_any([Permissions.groups], [has_assigned_user_spaces])
|
||||
)
|
||||
self.subscribe(
|
||||
SubscriptionFieldBuilder("shortUrlChange")
|
||||
.with_resolver(lambda message, *_: message.message)
|
||||
.with_require_any_permission([Permissions.short_urls])
|
||||
.with_require_any([Permissions.short_urls], [has_assigned_user_spaces])
|
||||
)
|
||||
|
@ -9,14 +9,9 @@ class FeatureFlags:
|
||||
_flags = {
|
||||
FeatureFlagsEnum.version_endpoint.value: True, # 15.01.2025
|
||||
FeatureFlagsEnum.technical_demo_banner.value: False, # 18.04.2025
|
||||
FeatureFlagsEnum.per_user_setup.value: Environment.get(
|
||||
"PER_USER_SETUP", bool, False
|
||||
), # 18.04.2025
|
||||
}
|
||||
|
||||
_overwrite_flags = [
|
||||
FeatureFlagsEnum.per_user_setup.value,
|
||||
]
|
||||
_overwrite_flags = []
|
||||
|
||||
@staticmethod
|
||||
def overwrite_flag(key: str):
|
||||
|
@ -4,4 +4,3 @@ from enum import Enum
|
||||
class FeatureFlagsEnum(Enum):
|
||||
version_endpoint = "VersionEndpoint"
|
||||
technical_demo_banner = "TechnicalDemoBanner"
|
||||
per_user_setup = "PerUserSetup"
|
||||
|
@ -37,6 +37,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
||||
self.__db_names: dict[str, str] = {}
|
||||
self.__foreign_tables: dict[str, tuple[str, str]] = {}
|
||||
self.__foreign_table_keys: dict[str, str] = {}
|
||||
self.__foreign_dao: dict[str, "DataAccessObjectABC"] = {}
|
||||
|
||||
self.__date_attributes: set[str] = set()
|
||||
self.__ignored_attributes: set[str] = set()
|
||||
@ -108,6 +109,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
||||
primary_attr: Attribute,
|
||||
foreign_attr: Attribute,
|
||||
table_name: str,
|
||||
reference_dao: "DataAccessObjectABC" = None,
|
||||
):
|
||||
"""
|
||||
Add a reference to another table for the given attribute
|
||||
@ -115,6 +117,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
||||
:param str primary_attr: Name of the primary key in the foreign object
|
||||
:param str foreign_attr: Name of the foreign key in the object
|
||||
:param str table_name: Name of the table to reference
|
||||
:param DataAccessObjectABC reference_dao: The data access object for the referenced table
|
||||
:return:
|
||||
"""
|
||||
if isinstance(attr, property):
|
||||
@ -131,6 +134,9 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
||||
foreign_attr = foreign_attr.lower().replace("_", "")
|
||||
|
||||
self.__foreign_table_keys[attr] = foreign_attr
|
||||
if reference_dao is not None:
|
||||
self.__foreign_dao[attr] = reference_dao
|
||||
|
||||
if table_name == self._table_name:
|
||||
return
|
||||
|
||||
@ -622,7 +628,27 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
||||
external_table_deps.append(external_field)
|
||||
parse_node(value, f"{external_field}.{key}")
|
||||
elif parent_key in self.__foreign_table_keys:
|
||||
parse_node({key: value}, self.__foreign_table_keys[parent_key])
|
||||
if key in operators:
|
||||
parse_node({key: value}, self.__foreign_table_keys[parent_key])
|
||||
continue
|
||||
|
||||
if parent_key in self.__foreign_dao:
|
||||
foreign_dao = self.__foreign_dao[parent_key]
|
||||
if key in foreign_dao.__foreign_tables:
|
||||
x = f"{self.__foreign_tables[parent_key][0]}.{foreign_dao.__foreign_table_keys[key]}"
|
||||
parse_node(
|
||||
value,
|
||||
f"{self.__foreign_tables[parent_key][0]}.{foreign_dao.__foreign_table_keys[key]}",
|
||||
)
|
||||
continue
|
||||
|
||||
if parent_key in self.__foreign_tables:
|
||||
parse_node(
|
||||
value, f"{self.__foreign_tables[parent_key][0]}.{key}"
|
||||
)
|
||||
continue
|
||||
|
||||
parse_node({parent_key: value})
|
||||
elif key in operators:
|
||||
operator = operators[key]
|
||||
if key == "contains" or key == "notContains":
|
||||
@ -633,6 +659,23 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
||||
value = f"{value}%"
|
||||
elif key == "endsWith":
|
||||
value = f"%{value}"
|
||||
elif key == "isNull" or key == "isNotNull":
|
||||
is_null_value = (
|
||||
value.get("equal", None)
|
||||
if isinstance(value, dict)
|
||||
else value
|
||||
)
|
||||
|
||||
if is_null_value is None:
|
||||
operator = operators[key]
|
||||
elif (key == "isNull" and is_null_value) or (
|
||||
key == "isNotNull" and not is_null_value
|
||||
):
|
||||
operator = "IS NULL"
|
||||
else:
|
||||
operator = "IS NOT NULL"
|
||||
|
||||
conditions.append((parent_key, operator, None))
|
||||
elif (key == "equal" or key == "notEqual") and value is None:
|
||||
operator = operators["isNull"]
|
||||
|
||||
@ -641,6 +684,8 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
|
||||
elif isinstance(value, dict):
|
||||
if key in self.__foreign_table_keys:
|
||||
parse_node(value, key)
|
||||
elif key in self.__db_names and parent_key is not None:
|
||||
parse_node({f"{parent_key}": value})
|
||||
elif key in self.__db_names:
|
||||
parse_node(value, self.__db_names[key])
|
||||
else:
|
||||
|
@ -1,5 +1,6 @@
|
||||
from typing import Optional, Any
|
||||
|
||||
import sqlparse
|
||||
from psycopg import sql
|
||||
from psycopg_pool import AsyncConnectionPool, PoolTimeout
|
||||
|
||||
@ -44,7 +45,9 @@ class PostgresPool:
|
||||
@staticmethod
|
||||
async def _exec_sql(cursor: Any, query: str, args=None, multi=True):
|
||||
if multi:
|
||||
queries = query.split(";")
|
||||
queries = [
|
||||
str(stmt).strip() for stmt in sqlparse.parse(query) if str(stmt).strip()
|
||||
]
|
||||
for q in queries:
|
||||
if q.strip() == "":
|
||||
continue
|
||||
|
@ -3,11 +3,12 @@ from typing import Optional
|
||||
|
||||
from async_property import async_property
|
||||
|
||||
from core.database.abc.db_join_model_abc import DbJoinModelABC
|
||||
from core.typing import SerialId
|
||||
from core.database.abc.db_model_abc import DbModelABC
|
||||
|
||||
|
||||
class RoleUser(DbModelABC):
|
||||
class RoleUser(DbJoinModelABC):
|
||||
def __init__(
|
||||
self,
|
||||
id: SerialId,
|
||||
@ -18,7 +19,9 @@ class RoleUser(DbModelABC):
|
||||
created: Optional[datetime] = None,
|
||||
updated: Optional[datetime] = None,
|
||||
):
|
||||
DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
|
||||
DbJoinModelABC.__init__(
|
||||
self, id, role_id, user_id, deleted, editor_id, created, updated
|
||||
)
|
||||
self._role_id = role_id
|
||||
self._user_id = user_id
|
||||
|
||||
|
@ -12,7 +12,7 @@ class Group(DbModelABC):
|
||||
self,
|
||||
id: SerialId,
|
||||
name: str,
|
||||
user_id: Optional[SerialId] = None,
|
||||
user_space_id: Optional[SerialId] = None,
|
||||
deleted: bool = False,
|
||||
editor_id: Optional[SerialId] = None,
|
||||
created: Optional[datetime] = None,
|
||||
@ -20,7 +20,7 @@ class Group(DbModelABC):
|
||||
):
|
||||
DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
|
||||
self._name = name
|
||||
self._user_id = user_id
|
||||
self._user_space_id = user_space_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@ -31,15 +31,14 @@ class Group(DbModelABC):
|
||||
self._name = value
|
||||
|
||||
@property
|
||||
def user_id(self) -> Optional[SerialId]:
|
||||
return self._user_id
|
||||
def user_space_id(self) -> Optional[SerialId]:
|
||||
return self._user_space_id
|
||||
|
||||
@async_property
|
||||
async def user(self):
|
||||
if self._user_id is None:
|
||||
async def user_space(self):
|
||||
if self._user_space_id is None:
|
||||
return None
|
||||
|
||||
from data.schemas.administration.user_dao import userDao
|
||||
from data.schemas.public.user_space_dao import userSpaceDao
|
||||
|
||||
user = await userDao.get_by_id(self.user_id)
|
||||
return user
|
||||
return await userSpaceDao.get_by_id(self._user_space_id)
|
||||
|
@ -11,8 +11,8 @@ class GroupDao(DbModelDaoABC[Group]):
|
||||
DbModelDaoABC.__init__(self, __name__, Group, "public.groups")
|
||||
self.attribute(Group.name, str)
|
||||
|
||||
self.attribute(Group.user_id, int)
|
||||
self.reference("user", "id", Group.user_id, "administration.users")
|
||||
self.attribute(Group.user_space_id, int)
|
||||
self.reference("userspace", "id", Group.user_space_id, "public.user_spaces")
|
||||
|
||||
async def get_by_name(self, name: str) -> Group:
|
||||
result = await self._db.select_map(
|
||||
|
@ -18,7 +18,7 @@ class ShortUrl(DbModelABC):
|
||||
group_id: Optional[SerialId],
|
||||
domain_id: Optional[SerialId],
|
||||
loading_screen: Optional[str] = None,
|
||||
user_id: Optional[SerialId] = None,
|
||||
user_space_id: Optional[SerialId] = None,
|
||||
deleted: bool = False,
|
||||
editor_id: Optional[SerialId] = None,
|
||||
created: Optional[datetime] = None,
|
||||
@ -35,7 +35,7 @@ class ShortUrl(DbModelABC):
|
||||
loading_screen = False
|
||||
self._loading_screen = loading_screen
|
||||
|
||||
self._user_id = user_id
|
||||
self._user_space_id = user_space_id
|
||||
|
||||
@property
|
||||
def short_url(self) -> str:
|
||||
@ -110,18 +110,17 @@ class ShortUrl(DbModelABC):
|
||||
self._loading_screen = value
|
||||
|
||||
@property
|
||||
def user_id(self) -> Optional[SerialId]:
|
||||
return self._user_id
|
||||
def user_space_id(self) -> Optional[SerialId]:
|
||||
return self._user_space_id
|
||||
|
||||
@async_property
|
||||
async def user(self):
|
||||
if self._user_id is None:
|
||||
async def user_space(self):
|
||||
if self._user_space_id is None:
|
||||
return None
|
||||
|
||||
from data.schemas.administration.user_dao import userDao
|
||||
from data.schemas.public.user_space_dao import userSpaceDao
|
||||
|
||||
user = await userDao.get_by_id(self.user_id)
|
||||
return user
|
||||
return await userSpaceDao.get_by_id(self._user_space_id)
|
||||
|
||||
def to_dto(self) -> dict:
|
||||
return {
|
||||
|
@ -13,13 +13,17 @@ class ShortUrlDao(DbModelDaoABC[ShortUrl]):
|
||||
self.attribute(ShortUrl.target_url, str)
|
||||
self.attribute(ShortUrl.description, str)
|
||||
self.attribute(ShortUrl.group_id, int)
|
||||
self.reference("group", "id", ShortUrl.group_id, "public.groups")
|
||||
from data.schemas.public.group_dao import groupDao
|
||||
|
||||
self.reference("group", "id", ShortUrl.group_id, "public.groups", groupDao)
|
||||
self.attribute(ShortUrl.domain_id, int)
|
||||
self.reference("domain", "id", ShortUrl.domain_id, "public.domains")
|
||||
from data.schemas.public.domain_dao import domainDao
|
||||
|
||||
self.reference("domain", "id", ShortUrl.domain_id, "public.domains", domainDao)
|
||||
self.attribute(ShortUrl.loading_screen, bool)
|
||||
|
||||
self.attribute(ShortUrl.user_id, int)
|
||||
self.reference("user", "id", ShortUrl.user_id, "administration.users")
|
||||
self.attribute(ShortUrl.user_space_id, int)
|
||||
self.reference("userspace", "id", ShortUrl.user_space_id, "public.user_spaces")
|
||||
|
||||
|
||||
shortUrlDao = ShortUrlDao()
|
||||
|
@ -15,7 +15,9 @@ class ShortUrlVisitDao(DbModelDaoABC[ShortUrlVisit]):
|
||||
|
||||
async def count_by_id(self, fid: int) -> int:
|
||||
result = await self._db.select_map(
|
||||
f"SELECT COUNT(*) FROM {self._table_name} WHERE shortUrlId = {fid}"
|
||||
f"""SELECT COUNT(*) FROM {self._table_name}
|
||||
WHERE shortUrlId = {fid}
|
||||
AND deleted = FALSE"""
|
||||
)
|
||||
if result is None or len(result) == 0:
|
||||
return 0
|
||||
|
51
api/src/data/schemas/public/user_invitation.py
Normal file
51
api/src/data/schemas/public/user_invitation.py
Normal file
@ -0,0 +1,51 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from async_property import async_property
|
||||
|
||||
from core.database.abc.db_model_abc import DbModelABC
|
||||
from core.typing import SerialId
|
||||
|
||||
|
||||
class UserInvitation(DbModelABC):
|
||||
def __init__(
|
||||
self,
|
||||
id: SerialId,
|
||||
email: str,
|
||||
invitation: str,
|
||||
user_space_id: SerialId,
|
||||
deleted: bool = False,
|
||||
editor_id: Optional[SerialId] = None,
|
||||
created: Optional[datetime] = None,
|
||||
updated: Optional[datetime] = None,
|
||||
):
|
||||
DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
|
||||
self._email = email
|
||||
self._user_space_id = user_space_id
|
||||
self._invitation = invitation
|
||||
|
||||
@property
|
||||
def email(self) -> str:
|
||||
return self._email
|
||||
|
||||
@email.setter
|
||||
def email(self, value: str):
|
||||
self._email = value
|
||||
|
||||
@property
|
||||
def invitation(self) -> Optional[str]:
|
||||
return self._invitation
|
||||
|
||||
@invitation.setter
|
||||
def invitation(self, value: Optional[str]):
|
||||
self._invitation = value
|
||||
|
||||
@property
|
||||
def user_space_id(self) -> SerialId:
|
||||
return self._user_space_id
|
||||
|
||||
@async_property
|
||||
async def user_space(self):
|
||||
from data.schemas.public.user_space_dao import userSpaceDao
|
||||
|
||||
return await userSpaceDao.get_by_id(self._user_space_id)
|
16
api/src/data/schemas/public/user_invitation_dao.py
Normal file
16
api/src/data/schemas/public/user_invitation_dao.py
Normal file
@ -0,0 +1,16 @@
|
||||
from core.database.abc.db_model_dao_abc import DbModelDaoABC
|
||||
from data.schemas.public.user_invitation import UserInvitation
|
||||
|
||||
|
||||
class UserInvitationDao(DbModelDaoABC[UserInvitation]):
|
||||
|
||||
def __init__(self):
|
||||
DbModelDaoABC.__init__(
|
||||
self, __name__, UserInvitation, "public.user_invitations"
|
||||
)
|
||||
self.attribute(UserInvitation.email, str)
|
||||
self.attribute(UserInvitation.invitation, str)
|
||||
self.attribute(UserInvitation.user_space_id, int)
|
||||
|
||||
|
||||
userInvitationDao = UserInvitationDao()
|
45
api/src/data/schemas/public/user_space.py
Normal file
45
api/src/data/schemas/public/user_space.py
Normal file
@ -0,0 +1,45 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from async_property import async_property
|
||||
|
||||
from core.database.abc.db_model_abc import DbModelABC
|
||||
from core.typing import SerialId
|
||||
|
||||
|
||||
class UserSpace(DbModelABC):
|
||||
def __init__(
|
||||
self,
|
||||
id: SerialId,
|
||||
name: str,
|
||||
owner_id: SerialId,
|
||||
deleted: bool = False,
|
||||
editor_id: Optional[SerialId] = None,
|
||||
created: Optional[datetime] = None,
|
||||
updated: Optional[datetime] = None,
|
||||
):
|
||||
DbModelABC.__init__(self, id, deleted, editor_id, created, updated)
|
||||
self._name = name
|
||||
self._owner_id = owner_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, value: str):
|
||||
self._name = value
|
||||
|
||||
@property
|
||||
def owner_id(self) -> SerialId:
|
||||
return self._owner_id
|
||||
|
||||
@owner_id.setter
|
||||
def owner_id(self, value: SerialId):
|
||||
self._owner_id = value
|
||||
|
||||
@async_property
|
||||
async def owner(self):
|
||||
from data.schemas.administration.user_dao import userDao
|
||||
|
||||
return await userDao.get_by_id(self._owner_id)
|
50
api/src/data/schemas/public/user_space_dao.py
Normal file
50
api/src/data/schemas/public/user_space_dao.py
Normal file
@ -0,0 +1,50 @@
|
||||
from core.database.abc.db_model_dao_abc import DbModelDaoABC
|
||||
from core.logger import DBLogger
|
||||
from data.schemas.public.user_space import UserSpace
|
||||
|
||||
logger = DBLogger(__name__)
|
||||
|
||||
|
||||
class UserSpaceDao(DbModelDaoABC[UserSpace]):
|
||||
def __init__(self):
|
||||
DbModelDaoABC.__init__(self, __name__, UserSpace, "public.user_spaces")
|
||||
self.attribute(UserSpace.name, str)
|
||||
self.attribute(UserSpace.owner_id, int)
|
||||
|
||||
async def get_by_name(self, name: str) -> UserSpace:
|
||||
result = await self._db.select_map(
|
||||
f"SELECT * FROM {self._table_name} WHERE Name = '{name}'"
|
||||
)
|
||||
return self.to_object(result[0])
|
||||
|
||||
async def get_users(self, user_space_id: int, with_invitations=True) -> list:
|
||||
result = await self._db.select_map(
|
||||
f"""
|
||||
SELECT u.*
|
||||
FROM administration.users u
|
||||
JOIN public.user_spaces_users usu ON u.id = usu.userId
|
||||
WHERE usu.userSpaceId = {user_space_id}
|
||||
AND usu.deleted = FALSE
|
||||
{'' if with_invitations else 'AND usu.invitation IS NULL'}
|
||||
"""
|
||||
)
|
||||
|
||||
from data.schemas.administration.user_dao import userDao
|
||||
|
||||
return [userDao.to_object(x) for x in result]
|
||||
|
||||
async def get_assigned_by_user_id(self, user_id: int):
|
||||
result = await self._db.select_map(
|
||||
f"""
|
||||
SELECT DISTINCT us.*
|
||||
FROM public.user_spaces us
|
||||
LEFT JOIN public.user_spaces_users usu ON us.id = usu.userSpaceId
|
||||
WHERE (usu.userId = {user_id} OR us.ownerId = {user_id})
|
||||
AND us.deleted = FALSE;
|
||||
"""
|
||||
)
|
||||
|
||||
return [self.to_object(x) for x in result]
|
||||
|
||||
|
||||
userSpaceDao = UserSpaceDao()
|
56
api/src/data/schemas/public/user_space_user.py
Normal file
56
api/src/data/schemas/public/user_space_user.py
Normal file
@ -0,0 +1,56 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from async_property import async_property
|
||||
|
||||
from core.database.abc.db_join_model_abc import DbJoinModelABC
|
||||
from core.database.abc.db_model_abc import DbModelABC
|
||||
from core.typing import SerialId
|
||||
|
||||
|
||||
class UserSpaceUser(DbJoinModelABC):
|
||||
def __init__(
|
||||
self,
|
||||
id: SerialId,
|
||||
user_space_id: SerialId,
|
||||
user_id: SerialId,
|
||||
invitation: Optional[str] = None,
|
||||
deleted: bool = False,
|
||||
editor_id: Optional[SerialId] = None,
|
||||
created: Optional[datetime] = None,
|
||||
updated: Optional[datetime] = None,
|
||||
):
|
||||
DbJoinModelABC.__init__(
|
||||
self, id, user_space_id, user_id, deleted, editor_id, created, updated
|
||||
)
|
||||
self._user_space_id = user_space_id
|
||||
self._user_id = user_id
|
||||
self._invitation = invitation
|
||||
|
||||
@property
|
||||
def user_space_id(self) -> SerialId:
|
||||
return self._user_space_id
|
||||
|
||||
@async_property
|
||||
async def user_space(self):
|
||||
from data.schemas.public.user_space_dao import userSpaceDao
|
||||
|
||||
return await userSpaceDao.get_by_id(self._user_space_id)
|
||||
|
||||
@property
|
||||
def user_id(self) -> SerialId:
|
||||
return self._user_id
|
||||
|
||||
@async_property
|
||||
async def user(self):
|
||||
from data.schemas.administration.user_dao import userDao
|
||||
|
||||
return await userDao.get_by_id(self._user_id)
|
||||
|
||||
@property
|
||||
def invitation(self) -> Optional[str]:
|
||||
return self._invitation
|
||||
|
||||
@invitation.setter
|
||||
def invitation(self, value: Optional[str]):
|
||||
self._invitation = value
|
39
api/src/data/schemas/public/user_space_user_dao.py
Normal file
39
api/src/data/schemas/public/user_space_user_dao.py
Normal file
@ -0,0 +1,39 @@
|
||||
from core.database.abc.db_model_dao_abc import DbModelDaoABC
|
||||
from core.logger import DBLogger
|
||||
from data.schemas.public.user_space_user import UserSpaceUser
|
||||
|
||||
logger = DBLogger(__name__)
|
||||
|
||||
|
||||
class UserSpaceUserDao(DbModelDaoABC[UserSpaceUser]):
|
||||
def __init__(self):
|
||||
DbModelDaoABC.__init__(
|
||||
self, __name__, UserSpaceUser, "public.user_spaces_users"
|
||||
)
|
||||
self.attribute(UserSpaceUser.user_space_id, int)
|
||||
self.attribute(UserSpaceUser.user_id, int)
|
||||
self.attribute(UserSpaceUser.invitation, str)
|
||||
|
||||
async def get_by_user_space_id(
|
||||
self, user_space_id: str, with_deleted=False
|
||||
) -> list[UserSpaceUser]:
|
||||
f = [
|
||||
{UserSpaceUser.user_space_id: user_space_id},
|
||||
{UserSpaceUser.invitation: None},
|
||||
]
|
||||
if not with_deleted:
|
||||
f.append({UserSpaceUser.deleted: False})
|
||||
|
||||
return await self.find_by(f)
|
||||
|
||||
async def get_by_user_id(
|
||||
self, user_id: str, with_deleted=False
|
||||
) -> list[UserSpaceUser]:
|
||||
f = [{UserSpaceUser.user_id: user_id}, {UserSpaceUser.invitation: None}]
|
||||
if not with_deleted:
|
||||
f.append({UserSpaceUser.deleted: False})
|
||||
|
||||
return await self.find_by(f)
|
||||
|
||||
|
||||
userSpaceUserDao = UserSpaceUserDao()
|
79
api/src/data/scripts/2025-04-30-21-50-user-spaces2.sql
Normal file
79
api/src/data/scripts/2025-04-30-21-50-user-spaces2.sql
Normal file
@ -0,0 +1,79 @@
|
||||
CREATE TABLE IF NOT EXISTS public.user_spaces
|
||||
(
|
||||
Id SERIAL PRIMARY KEY,
|
||||
Name VARCHAR(255) NOT NULL,
|
||||
OwnerId INT NOT NULL REFERENCES administration.users (Id),
|
||||
-- for history
|
||||
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
EditorId INT NULL REFERENCES administration.users (Id),
|
||||
Created timestamptz NOT NULL DEFAULT NOW(),
|
||||
Updated timestamptz NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_spaces_history
|
||||
(
|
||||
LIKE public.user_spaces
|
||||
);
|
||||
|
||||
CREATE TRIGGER user_spaces_history_trigger
|
||||
BEFORE INSERT OR UPDATE OR DELETE
|
||||
ON public.user_spaces
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.history_trigger_function();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_spaces_users
|
||||
(
|
||||
Id SERIAL PRIMARY KEY,
|
||||
UserSpaceId INT NOT NULL REFERENCES public.user_spaces (Id),
|
||||
UserId INT NOT NULL REFERENCES administration.users (Id),
|
||||
-- for history
|
||||
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
EditorId INT NULL REFERENCES administration.users (Id),
|
||||
Created timestamptz NOT NULL DEFAULT NOW(),
|
||||
Updated timestamptz NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_spaces_users_history
|
||||
(
|
||||
LIKE public.user_spaces_users
|
||||
);
|
||||
|
||||
CREATE TRIGGER user_spaces_users_history_trigger
|
||||
BEFORE INSERT OR UPDATE OR DELETE
|
||||
ON public.user_spaces_users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.history_trigger_function();
|
||||
|
||||
-- add user spaces
|
||||
INSERT INTO public.user_spaces (Name, OwnerId)
|
||||
VALUES ('Default', (SELECT id FROM administration.users ORDER BY id LIMIT 1));
|
||||
|
||||
INSERT INTO public.user_spaces_users (UserSpaceId, UserId)
|
||||
SELECT 1 AS UserSpaceId, u.id AS UserId
|
||||
FROM administration.users u
|
||||
WHERE u.deleted = FALSE;
|
||||
|
||||
-- change other tables
|
||||
ALTER TABLE public.groups
|
||||
ADD COLUMN IF NOT EXISTS UserSpaceId INT NOT NULL REFERENCES public.user_spaces (Id) default 1;
|
||||
|
||||
ALTER TABLE public.groups
|
||||
DROP COLUMN IF EXISTS UserId;
|
||||
|
||||
ALTER TABLE public.groups_history
|
||||
ADD COLUMN IF NOT EXISTS UserSpaceId INT NOT NULL REFERENCES public.user_spaces (Id) default 1;
|
||||
|
||||
ALTER TABLE public.groups_history
|
||||
DROP COLUMN IF EXISTS UserId;
|
||||
|
||||
ALTER TABLE public.short_urls
|
||||
ADD COLUMN IF NOT EXISTS UserSpaceId INT NOT NULL REFERENCES public.user_spaces (Id) default 1;
|
||||
|
||||
ALTER TABLE public.short_urls
|
||||
DROP COLUMN IF EXISTS UserId;
|
||||
|
||||
ALTER TABLE public.short_urls_history
|
||||
ADD COLUMN IF NOT EXISTS UserSpaceId INT NOT NULL REFERENCES public.user_spaces (Id) default 1;
|
||||
|
||||
ALTER TABLE public.short_urls_history
|
||||
DROP COLUMN IF EXISTS UserId;
|
@ -0,0 +1,5 @@
|
||||
ALTER TABLE public.user_spaces_users
|
||||
ADD COLUMN invitation UUID DEFAULT NULL;
|
||||
|
||||
ALTER TABLE public.user_spaces_users_history
|
||||
ADD COLUMN invitation UUID DEFAULT NULL;
|
@ -0,0 +1,23 @@
|
||||
CREATE TABLE public.user_invitations
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
invitation UUID NOT NULL,
|
||||
userSpaceId INT NOT NULL REFERENCES public.user_spaces (Id),
|
||||
-- for history
|
||||
Deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
EditorId INT NULL REFERENCES administration.users (Id),
|
||||
Created timestamptz NOT NULL DEFAULT NOW(),
|
||||
Updated timestamptz NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE public.user_invitations_history
|
||||
(
|
||||
LIKE public.user_invitations
|
||||
);
|
||||
|
||||
CREATE TRIGGER user_invitations_history_trigger
|
||||
BEFORE INSERT OR UPDATE OR DELETE
|
||||
ON public.user_invitations
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.history_trigger_function();
|
@ -0,0 +1,10 @@
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS user_invitations_unique_email
|
||||
ON public.user_invitations (email)
|
||||
WHERE email != 'ANONYMOUS';
|
||||
|
||||
ALTER TABLE public.user_invitations
|
||||
ADD CONSTRAINT user_invitations_unique_invitation UNIQUE (invitation);
|
||||
|
||||
ALTER TABLE public.user_spaces_users
|
||||
ADD CONSTRAINT user_spaces_users_unique_invitation UNIQUE (invitation),
|
||||
ADD CONSTRAINT user_spaces_users_unique_entry UNIQUE (userspaceid, userid);
|
@ -83,7 +83,26 @@ def _find_short_url_by_path(path: str) -> Optional[dict]:
|
||||
json={
|
||||
"query": f"""
|
||||
query getShortUrlByPath($path: String!) {{
|
||||
shortUrls(filter: {{ shortUrl: {{ equal: $path }}, deleted: {{ equal: false }}, group: {{ deleted: {{ equal: false }} }} }}) {{
|
||||
shortUrls(filter: [{{ shortUrl: {{ equal: $path }} }}, {{ deleted: {{ equal: false }} }}, {{ group: {{ deleted: {{ equal: false }} }} }}]) {{
|
||||
nodes {{
|
||||
id
|
||||
shortUrl
|
||||
targetUrl
|
||||
description
|
||||
group {{
|
||||
id
|
||||
name
|
||||
}}
|
||||
domain {{
|
||||
id
|
||||
name
|
||||
}}
|
||||
loadingScreen
|
||||
deleted
|
||||
}}
|
||||
}}
|
||||
|
||||
shortUrlsWithoutGroup: shortUrls(filter: [{{ shortUrl: {{ equal: $path }} }}, {{ deleted: {{ equal: false }} }}, {{ group: {{ isNull: true }} }}]) {{
|
||||
nodes {{
|
||||
id
|
||||
shortUrl
|
||||
@ -115,14 +134,18 @@ def _find_short_url_by_path(path: str) -> Optional[dict]:
|
||||
"data" not in data
|
||||
or "shortUrls" not in data["data"]
|
||||
or "nodes" not in data["data"]["shortUrls"]
|
||||
or "nodes" not in data["data"]["shortUrlsWithoutGroup"]
|
||||
):
|
||||
return None
|
||||
|
||||
data = data["data"]["shortUrls"]["nodes"]
|
||||
if len(data) == 0:
|
||||
nodes = [
|
||||
*data["data"]["shortUrls"]["nodes"],
|
||||
*data["data"]["shortUrlsWithoutGroup"]["nodes"],
|
||||
]
|
||||
if len(nodes) == 0:
|
||||
return None
|
||||
|
||||
return data[0]
|
||||
return nodes[0]
|
||||
|
||||
|
||||
async def _handle_short_url(request: Request, short_url: dict):
|
||||
@ -145,7 +168,7 @@ async def _track_visit(r: Request, short_url: dict):
|
||||
f"{api_url}/graphql",
|
||||
json={
|
||||
"query": f"""
|
||||
mutation trackShortUrlVisit($id: ID!, $agent: String) {{
|
||||
mutation trackShortUrlVisit($id: Int!, $agent: String) {{
|
||||
shortUrl {{
|
||||
trackVisit(id: $id, agent: $agent)
|
||||
}}
|
||||
|
@ -9,6 +9,7 @@ from core.database.abc.db_model_dao_abc import DbModelDaoABC
|
||||
from core.logger import Logger
|
||||
from core.string import first_to_lower
|
||||
from data.schemas.administration.user_dao import userDao
|
||||
from data.schemas.public.user_invitation_dao import userInvitationDao
|
||||
|
||||
logger = Logger("DataPrivacy")
|
||||
|
||||
@ -77,7 +78,16 @@ class DataPrivacyService:
|
||||
keycloak_id = user.keycloak_id
|
||||
|
||||
# Anonymize internal data
|
||||
user_invitations = await userInvitationDao.find_by(
|
||||
[{"email": {"equal": user.email}}]
|
||||
)
|
||||
for user_invitation in user_invitations:
|
||||
user_invitation.email = "ANONYMOUS"
|
||||
user_invitation.deleted = True
|
||||
await userInvitationDao.update(user_invitation)
|
||||
|
||||
await user.anonymize()
|
||||
await userDao.delete(user)
|
||||
|
||||
# Anonymize external data
|
||||
try:
|
||||
|
46
api/src/service/mail_service.py
Normal file
46
api/src/service/mail_service.py
Normal file
@ -0,0 +1,46 @@
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from core.environment import Environment
|
||||
from core.logger import Logger
|
||||
|
||||
logger = Logger("MailService")
|
||||
|
||||
|
||||
class MailService:
|
||||
|
||||
def __init__(self):
|
||||
self._server = Environment.get("MAIL_SERVER", str)
|
||||
self._port = Environment.get("MAIL_PORT", int)
|
||||
self._user = Environment.get("MAIL_USER", str)
|
||||
self._password = Environment.get("MAIL_PASSWORD", str)
|
||||
self._from = Environment.get("MAIL_FROM", str)
|
||||
|
||||
if not all([self._server, self._port, self._user, self._password, self._from]):
|
||||
raise ValueError("Mail server configuration is incomplete.")
|
||||
|
||||
def send(self, to: str, subject: str, body: str):
|
||||
logger.debug(f"Preparing to send email to: {to}, subject: {subject}")
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = self._from
|
||||
msg["To"] = to
|
||||
msg["Subject"] = subject
|
||||
|
||||
msg.attach(MIMEText(body, "plain"))
|
||||
|
||||
try:
|
||||
logger.info(f"Connecting to mail server: {self._server}:{self._port}")
|
||||
with smtplib.SMTP(self._server, self._port) as server:
|
||||
server.starttls()
|
||||
logger.debug("Starting TLS encryption")
|
||||
server.login(self._user, self._password)
|
||||
logger.info(f"Logged in as: {self._user}")
|
||||
server.sendmail(self._from, to, msg.as_string())
|
||||
logger.info(f"Email successfully sent to: {to}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send email to {to}", e)
|
||||
|
||||
|
||||
mailService = MailService()
|
@ -26,6 +26,18 @@ class Permissions(Enum):
|
||||
settings = "settings"
|
||||
settings_update = "settings.update"
|
||||
|
||||
# domains
|
||||
domains = "domains"
|
||||
domains_create = "domains.create"
|
||||
domains_update = "domains.update"
|
||||
domains_delete = "domains.delete"
|
||||
|
||||
# user spaces
|
||||
user_spaces = "user_spaces"
|
||||
user_spaces_create = "user_spaces.create"
|
||||
user_spaces_update = "user_spaces.update"
|
||||
user_spaces_delete = "user_spaces.delete"
|
||||
|
||||
"""
|
||||
Permissions
|
||||
"""
|
||||
@ -38,12 +50,6 @@ class Permissions(Enum):
|
||||
"""
|
||||
Public
|
||||
"""
|
||||
# domains
|
||||
domains = "domains"
|
||||
domains_create = "domains.create"
|
||||
domains_update = "domains.update"
|
||||
domains_delete = "domains.delete"
|
||||
|
||||
# groups
|
||||
groups = "groups"
|
||||
groups_create = "groups.create"
|
||||
|
@ -1 +0,0 @@
|
||||
<p>home works!</p>
|
@ -26,8 +26,12 @@ export abstract class FormPageBase<
|
||||
protected filterService = inject(FilterService);
|
||||
protected dataService = inject(PageDataService) as S;
|
||||
|
||||
protected constructor(idKey: string = 'id') {
|
||||
private backRoute: string = '..';
|
||||
|
||||
protected constructor(idKey: string = 'id', backRoute: string = '..') {
|
||||
const id = this.route.snapshot.params[idKey];
|
||||
this.backRoute = backRoute;
|
||||
|
||||
this.validateRoute(id);
|
||||
|
||||
this.buildForm();
|
||||
@ -46,7 +50,7 @@ export abstract class FormPageBase<
|
||||
}
|
||||
|
||||
close() {
|
||||
const backRoute = this.nodeId ? '../..' : '..';
|
||||
const backRoute = this.nodeId ? `${this.backRoute}/..` : this.backRoute;
|
||||
|
||||
this.router.navigate([backRoute], { relativeTo: this.route }).then(() => {
|
||||
this.filterService.onLoad.emit();
|
||||
|
@ -4,7 +4,7 @@ import { Logger } from 'src/app/service/logger.service';
|
||||
import { ToastService } from 'src/app/service/toast.service';
|
||||
import { AuthService } from 'src/app/service/auth.service';
|
||||
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||
import { FeatureFlagService } from 'src/app/service/feature-flag.service';
|
||||
import { SidebarService } from 'src/app/service/sidebar.service';
|
||||
|
||||
const log = new Logger('PermissionGuard');
|
||||
|
||||
@ -16,16 +16,18 @@ export class PermissionGuard {
|
||||
private router: Router,
|
||||
private toast: ToastService,
|
||||
private auth: AuthService,
|
||||
private features: FeatureFlagService
|
||||
private sidebar: SidebarService
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
|
||||
const permissions = route.data['permissions'] as PermissionsEnum[];
|
||||
const checkByPerUserSetup = route.data['checkByPerUserSetup'] as boolean;
|
||||
const isInUserSpace = route.data['isInUserSpace'] as boolean;
|
||||
|
||||
const isPerUserSetup = await this.features.get('PerUserSetup');
|
||||
if (checkByPerUserSetup && isPerUserSetup) {
|
||||
return true;
|
||||
if (isInUserSpace) {
|
||||
return (
|
||||
this.sidebar.selectedUserSpace$.value !== undefined &&
|
||||
this.sidebar.selectedUserSpace$.value !== null
|
||||
);
|
||||
}
|
||||
|
||||
if (!permissions || permissions.length === 0) {
|
||||
|
@ -13,6 +13,7 @@ export function initializeKeycloak(
|
||||
},
|
||||
initOptions: {
|
||||
onLoad: 'check-sso',
|
||||
checkLoginIframe: false,
|
||||
},
|
||||
enableBearerInterceptor: false,
|
||||
});
|
||||
|
@ -20,6 +20,7 @@ export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
return next(req);
|
||||
}
|
||||
|
||||
const auth = inject(AuthService);
|
||||
return from(keycloak.getToken()).pipe(
|
||||
switchMap(token => {
|
||||
if (!token) {
|
||||
@ -46,7 +47,6 @@ export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
);
|
||||
}),
|
||||
catchError(() => {
|
||||
const auth = inject(AuthService);
|
||||
auth.logout().then();
|
||||
return next(req);
|
||||
})
|
||||
|
20
web/src/app/model/entities/user-space.ts
Normal file
20
web/src/app/model/entities/user-space.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { User } from 'src/app/model/auth/user';
|
||||
import { DbModelWithHistory } from 'src/app/model/entities/db-model';
|
||||
|
||||
export interface UserSpace extends DbModelWithHistory {
|
||||
id: number;
|
||||
name: string;
|
||||
owner?: User;
|
||||
users?: User[];
|
||||
}
|
||||
|
||||
export interface UserSpaceCreateInput {
|
||||
name?: string;
|
||||
users?: number[];
|
||||
}
|
||||
|
||||
export interface UserSpaceUpdateInput {
|
||||
id: number;
|
||||
name?: string;
|
||||
users?: number[];
|
||||
}
|
@ -7,13 +7,9 @@ import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'domains',
|
||||
loadChildren: () =>
|
||||
import('src/app/modules/admin/domains/domains.module').then(
|
||||
m => m.DomainsModule
|
||||
),
|
||||
canActivate: [PermissionGuard],
|
||||
data: { permissions: [PermissionsEnum.domains] },
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'urls',
|
||||
},
|
||||
{
|
||||
path: 'groups',
|
||||
@ -22,7 +18,7 @@ const routes: Routes = [
|
||||
m => m.GroupsModule
|
||||
),
|
||||
canActivate: [PermissionGuard],
|
||||
data: { permissions: [PermissionsEnum.groups], checkByPerUserSetup: true },
|
||||
data: { permissions: [PermissionsEnum.groups], isInUserSpace: true },
|
||||
},
|
||||
{
|
||||
path: 'urls',
|
||||
@ -36,9 +32,16 @@ const routes: Routes = [
|
||||
PermissionsEnum.shortUrls,
|
||||
PermissionsEnum.shortUrlsByAssignment,
|
||||
],
|
||||
checkByPerUserSetup: true,
|
||||
isInUserSpace: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'rooms',
|
||||
loadChildren: () =>
|
||||
import('src/app/modules/admin/user-spaces/user-spaces.module').then(
|
||||
m => m.UserSpacesModule
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'administration',
|
||||
loadChildren: () =>
|
||||
|
@ -11,6 +11,15 @@ const routes: Routes = [
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'users',
|
||||
},
|
||||
{
|
||||
path: 'domains',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'src/app/modules/admin/administration/domains/domains.module'
|
||||
).then(m => m.DomainsModule),
|
||||
canActivate: [PermissionGuard],
|
||||
data: { permissions: [PermissionsEnum.domains] },
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
title: 'Users | Maxlan',
|
||||
|
@ -4,11 +4,11 @@ import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { PermissionGuard } from 'src/app/core/guard/permission.guard';
|
||||
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||
import { DomainsPage } from 'src/app/modules/admin/domains/domains.page';
|
||||
import { DomainFormPageComponent } from 'src/app/modules/admin/domains/form-page/domain-form-page.component';
|
||||
import { DomainsDataService } from 'src/app/modules/admin/domains/domains.data.service';
|
||||
import { DomainsColumns } from 'src/app/modules/admin/domains/domains.columns';
|
||||
import { HistoryComponent } from 'src/app/modules/admin/domains/history/history.component';
|
||||
import { DomainsPage } from 'src/app/modules/admin/administration/domains/domains.page';
|
||||
import { DomainFormPageComponent } from 'src/app/modules/admin/administration/domains/form-page/domain-form-page.component';
|
||||
import { DomainsDataService } from 'src/app/modules/admin/administration/domains/domains.data.service';
|
||||
import { DomainsColumns } from 'src/app/modules/admin/administration/domains/domains.columns';
|
||||
import { HistoryComponent } from 'src/app/modules/admin/administration/domains/history/history.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
@ -3,9 +3,9 @@ import { PageBase } from 'src/app/core/base/page-base';
|
||||
import { ToastService } from 'src/app/service/toast.service';
|
||||
import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service';
|
||||
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||
import { DomainsDataService } from 'src/app/modules/admin/domains/domains.data.service';
|
||||
import { DomainsColumns } from 'src/app/modules/admin/domains/domains.columns';
|
||||
import { Domain } from 'src/app/model/entities/domain';
|
||||
import { DomainsColumns } from 'src/app/modules/admin/administration/domains/domains.columns';
|
||||
import { DomainsDataService } from 'src/app/modules/admin/administration/domains/domains.data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-domains',
|
@ -7,7 +7,7 @@ import {
|
||||
DomainCreateInput,
|
||||
DomainUpdateInput,
|
||||
} from 'src/app/model/entities/domain';
|
||||
import { DomainsDataService } from 'src/app/modules/admin/domains/domains.data.service';
|
||||
import { DomainsDataService } from 'src/app/modules/admin/administration/domains/domains.data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-domain-form-page',
|
@ -14,7 +14,7 @@ import { PageDataService } from 'src/app/core/base/page.data.service';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { PageColumns } from 'src/app/core/base/page.columns';
|
||||
import { MockPageColumns } from 'src/app/modules/shared/test/page.columns.mock';
|
||||
import { DomainsDataService } from 'src/app/modules/admin/domains/domains.data.service';
|
||||
import { DomainsDataService } from 'src/app/modules/admin/administration/domains/domains.data.service';
|
||||
|
||||
describe('HistoryComponent', () => {
|
||||
let component: HistoryComponent<unknown>;
|
@ -25,12 +25,14 @@ export class UsersColumns extends PageColumns<User> {
|
||||
translationKey: 'user.username',
|
||||
type: 'text',
|
||||
value: (row: User) => row.username,
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
translationKey: 'user.email',
|
||||
type: 'text',
|
||||
value: (row: User) => row.email,
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
|
@ -26,6 +26,8 @@ export class UsersPage extends PageBase<User, UsersDataService, UsersColumns> {
|
||||
});
|
||||
}
|
||||
|
||||
async onInit(): Promise<void> {}
|
||||
|
||||
load(silent?: boolean): void {
|
||||
if (!silent) this.loading = true;
|
||||
this.dataService
|
||||
|
@ -27,14 +27,13 @@
|
||||
type="text"
|
||||
formControlName="name"/>
|
||||
</div>
|
||||
<div *ngIf="!isPerUserSetup" class="divider"></div>
|
||||
<div class="divider"></div>
|
||||
<p-multiSelect
|
||||
*ngIf="!isPerUserSetup"
|
||||
[options]="roles"
|
||||
formControlName="roles"
|
||||
optionLabel="name"
|
||||
placeholder="{{ 'user.assign_roles' | translate }}"
|
||||
display="chip"
|
||||
[showClear]="true" />
|
||||
[showClear]="true"/>
|
||||
</ng-template>
|
||||
</app-form-page>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ToastService } from 'src/app/service/toast.service';
|
||||
import { FormPageBase } from 'src/app/core/base/form-page-base';
|
||||
@ -17,17 +17,13 @@ import { FeatureFlagService } from 'src/app/service/feature-flag.service';
|
||||
templateUrl: './group-form-page.component.html',
|
||||
styleUrl: './group-form-page.component.scss',
|
||||
})
|
||||
export class GroupFormPageComponent
|
||||
extends FormPageBase<
|
||||
Group,
|
||||
GroupCreateInput,
|
||||
GroupUpdateInput,
|
||||
GroupsDataService
|
||||
>
|
||||
implements OnInit
|
||||
{
|
||||
export class GroupFormPageComponent extends FormPageBase<
|
||||
Group,
|
||||
GroupCreateInput,
|
||||
GroupUpdateInput,
|
||||
GroupsDataService
|
||||
> {
|
||||
roles: Role[] = [];
|
||||
isPerUserSetup = true;
|
||||
|
||||
constructor(
|
||||
private features: FeatureFlagService,
|
||||
@ -35,15 +31,9 @@ export class GroupFormPageComponent
|
||||
private cds: CommonDataService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isPerUserSetup = await this.features.get('PerUserSetup');
|
||||
if (!this.isPerUserSetup) {
|
||||
this.cds.getAllRoles().subscribe(roles => {
|
||||
this.roles = roles;
|
||||
});
|
||||
}
|
||||
this.cds.getAllRoles().subscribe(roles => {
|
||||
this.roles = roles;
|
||||
});
|
||||
|
||||
if (!this.nodeId) {
|
||||
this.node = this.new();
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
GroupUpdateInput,
|
||||
} from 'src/app/model/entities/group';
|
||||
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
|
||||
import { SidebarService } from 'src/app/service/sidebar.service';
|
||||
|
||||
@Injectable()
|
||||
export class GroupsDataService
|
||||
@ -35,7 +36,8 @@ export class GroupsDataService
|
||||
{
|
||||
constructor(
|
||||
private spinner: SpinnerService,
|
||||
private apollo: Apollo
|
||||
private apollo: Apollo,
|
||||
private sidebarService: SidebarService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@ -74,7 +76,14 @@ export class GroupsDataService
|
||||
${DB_MODEL_FRAGMENT}
|
||||
`,
|
||||
variables: {
|
||||
filter: filter,
|
||||
filter: [
|
||||
{
|
||||
userSpace: {
|
||||
id: { equal: this.sidebarService.selectedUserSpace$.value?.id },
|
||||
},
|
||||
},
|
||||
...(filter ?? []),
|
||||
],
|
||||
sort: sort,
|
||||
skip: skip,
|
||||
take: take,
|
||||
@ -186,6 +195,7 @@ export class GroupsDataService
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
userSpaceId: this.sidebarService.selectedUserSpace$.value?.id,
|
||||
name: object.name,
|
||||
roles: object.roles?.map(x => x.id),
|
||||
},
|
||||
|
@ -22,7 +22,7 @@ const routes: Routes = [
|
||||
canActivate: [PermissionGuard],
|
||||
data: {
|
||||
permissions: [PermissionsEnum.groupsCreate],
|
||||
checkByPerUserSetup: true,
|
||||
isInUserSpace: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -31,7 +31,7 @@ const routes: Routes = [
|
||||
canActivate: [PermissionGuard],
|
||||
data: {
|
||||
permissions: [PermissionsEnum.groupsUpdate],
|
||||
checkByPerUserSetup: true,
|
||||
isInUserSpace: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -40,7 +40,7 @@ const routes: Routes = [
|
||||
canActivate: [PermissionGuard],
|
||||
data: {
|
||||
permissions: [PermissionsEnum.groups],
|
||||
checkByPerUserSetup: true,
|
||||
isInUserSpace: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -1,68 +1,41 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { PageBase } from 'src/app/core/base/page-base';
|
||||
import { ToastService } from 'src/app/service/toast.service';
|
||||
import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service';
|
||||
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||
import { Group } from 'src/app/model/entities/group';
|
||||
import { GroupsDataService } from 'src/app/modules/admin/groups/groups.data.service';
|
||||
import { GroupsColumns } from 'src/app/modules/admin/groups/groups.columns';
|
||||
import { AuthService } from 'src/app/service/auth.service';
|
||||
import { ConfigService } from 'src/app/service/config.service';
|
||||
import { FeatureFlagService } from 'src/app/service/feature-flag.service';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { SidebarService } from 'src/app/service/sidebar.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-groups',
|
||||
templateUrl: './groups.page.html',
|
||||
styleUrl: './groups.page.scss',
|
||||
})
|
||||
export class GroupsPage
|
||||
extends PageBase<Group, GroupsDataService, GroupsColumns>
|
||||
implements OnInit
|
||||
{
|
||||
export class GroupsPage extends PageBase<
|
||||
Group,
|
||||
GroupsDataService,
|
||||
GroupsColumns
|
||||
> {
|
||||
constructor(
|
||||
private toast: ToastService,
|
||||
private confirmation: ConfirmationDialogService,
|
||||
private auth: AuthService,
|
||||
private config: ConfigService,
|
||||
private features: FeatureFlagService
|
||||
private sidebar: SidebarService
|
||||
) {
|
||||
super(true);
|
||||
}
|
||||
super(true, {
|
||||
read: [],
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [],
|
||||
restore: [],
|
||||
});
|
||||
|
||||
async ngOnInit() {
|
||||
this.requiredPermissions = {
|
||||
read: (await this.features.get('PerUserSetup'))
|
||||
? []
|
||||
: [PermissionsEnum.groups],
|
||||
create: (await this.features.get('PerUserSetup'))
|
||||
? []
|
||||
: (await this.auth.hasAnyPermissionLazy(
|
||||
this.requiredPermissions.create ?? []
|
||||
))
|
||||
? (this.requiredPermissions.create ?? [])
|
||||
: [],
|
||||
update: (await this.features.get('PerUserSetup'))
|
||||
? []
|
||||
: (await this.auth.hasAnyPermissionLazy(
|
||||
this.requiredPermissions.update ?? []
|
||||
))
|
||||
? (this.requiredPermissions.update ?? [])
|
||||
: [],
|
||||
delete: (await this.features.get('PerUserSetup'))
|
||||
? []
|
||||
: (await this.auth.hasAnyPermissionLazy(
|
||||
this.requiredPermissions.delete ?? []
|
||||
))
|
||||
? (this.requiredPermissions.delete ?? [])
|
||||
: [],
|
||||
restore: (await this.features.get('PerUserSetup'))
|
||||
? []
|
||||
: (await this.auth.hasAnyPermissionLazy(
|
||||
this.requiredPermissions.restore ?? []
|
||||
))
|
||||
? (this.requiredPermissions.restore ?? [])
|
||||
: [],
|
||||
};
|
||||
this.sidebar.selectedUserSpace$
|
||||
.pipe(takeUntil(this.unsubscribe$))
|
||||
.subscribe(() => {
|
||||
this.load(true);
|
||||
});
|
||||
}
|
||||
|
||||
load(silent?: boolean): void {
|
||||
|
@ -65,7 +65,7 @@
|
||||
></p-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-page-input" *ngIf="!isPerUserSetup">
|
||||
<div class="form-page-input">
|
||||
<p class="label">{{ 'common.domain' | translate }}</p>
|
||||
<div
|
||||
class="value">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ToastService } from 'src/app/service/toast.service';
|
||||
import { FormPageBase } from 'src/app/core/base/form-page-base';
|
||||
@ -10,45 +10,29 @@ import {
|
||||
import { ShortUrlsDataService } from 'src/app/modules/admin/short-urls/short-urls.data.service';
|
||||
import { Group } from 'src/app/model/entities/group';
|
||||
import { Domain } from 'src/app/model/entities/domain';
|
||||
import { FeatureFlagService } from 'src/app/service/feature-flag.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-short-url-form-page',
|
||||
templateUrl: './short-url-form-page.component.html',
|
||||
styleUrl: './short-url-form-page.component.scss',
|
||||
})
|
||||
export class ShortUrlFormPageComponent
|
||||
extends FormPageBase<
|
||||
ShortUrl,
|
||||
ShortUrlCreateInput,
|
||||
ShortUrlUpdateInput,
|
||||
ShortUrlsDataService
|
||||
>
|
||||
implements OnInit
|
||||
{
|
||||
export class ShortUrlFormPageComponent extends FormPageBase<
|
||||
ShortUrl,
|
||||
ShortUrlCreateInput,
|
||||
ShortUrlUpdateInput,
|
||||
ShortUrlsDataService
|
||||
> {
|
||||
groups: Group[] = [];
|
||||
domains: Domain[] = [];
|
||||
|
||||
isPerUserSetup = true;
|
||||
|
||||
constructor(
|
||||
private features: FeatureFlagService,
|
||||
private toast: ToastService
|
||||
) {
|
||||
constructor(private toast: ToastService) {
|
||||
super();
|
||||
this.dataService.getAllGroups().subscribe(groups => {
|
||||
this.dataService.getAllAvailableGroups().subscribe(groups => {
|
||||
this.groups = groups;
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isPerUserSetup = await this.features.get('PerUserSetup');
|
||||
|
||||
if (!this.isPerUserSetup) {
|
||||
this.dataService.getAllDomains().subscribe(domains => {
|
||||
this.domains = domains;
|
||||
});
|
||||
}
|
||||
this.dataService.getAllDomains().subscribe(domains => {
|
||||
this.domains = domains;
|
||||
});
|
||||
|
||||
if (!this.nodeId) {
|
||||
this.node = this.new();
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable, Provider } from '@angular/core';
|
||||
import { forkJoin, merge, Observable } from 'rxjs';
|
||||
import { merge, Observable } from 'rxjs';
|
||||
import {
|
||||
Create,
|
||||
Delete,
|
||||
@ -25,6 +25,7 @@ import {
|
||||
import { Group } from 'src/app/model/entities/group';
|
||||
import { Domain } from 'src/app/model/entities/domain';
|
||||
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
|
||||
import { SidebarService } from 'src/app/service/sidebar.service';
|
||||
|
||||
@Injectable()
|
||||
export class ShortUrlsDataService
|
||||
@ -37,7 +38,8 @@ export class ShortUrlsDataService
|
||||
{
|
||||
constructor(
|
||||
private spinner: SpinnerService,
|
||||
private apollo: Apollo
|
||||
private apollo: Apollo,
|
||||
private sidebarService: SidebarService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@ -48,88 +50,113 @@ export class ShortUrlsDataService
|
||||
skip?: number | undefined,
|
||||
take?: number | undefined
|
||||
): Observable<QueryResult<ShortUrl>> {
|
||||
const query1 = this.apollo.query<{ shortUrls: QueryResult<ShortUrl> }>({
|
||||
query: gql`
|
||||
query getShortUrls($filter: [ShortUrlFilter], $sort: [ShortUrlSort]) {
|
||||
shortUrls(filter: $filter, sort: $sort) {
|
||||
nodes {
|
||||
id
|
||||
shortUrl
|
||||
targetUrl
|
||||
description
|
||||
loadingScreen
|
||||
visits
|
||||
group {
|
||||
return this.apollo
|
||||
.query<{
|
||||
shortUrls: QueryResult<ShortUrl>;
|
||||
shortUrlsWithoutGroup: QueryResult<ShortUrl>;
|
||||
}>({
|
||||
query: gql`
|
||||
query getShortUrls(
|
||||
$filter: [ShortUrlFilter]
|
||||
$filter2: [ShortUrlFilter]
|
||||
$sort: [ShortUrlSort]
|
||||
) {
|
||||
shortUrls(filter: $filter, sort: $sort) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
}
|
||||
domain {
|
||||
id
|
||||
name
|
||||
}
|
||||
shortUrl
|
||||
targetUrl
|
||||
description
|
||||
loadingScreen
|
||||
visits
|
||||
group {
|
||||
id
|
||||
name
|
||||
}
|
||||
domain {
|
||||
id
|
||||
name
|
||||
}
|
||||
|
||||
...DB_MODEL
|
||||
...DB_MODEL
|
||||
}
|
||||
}
|
||||
|
||||
shortUrlsWithoutGroup: shortUrls(filter: $filter2, sort: $sort) {
|
||||
nodes {
|
||||
id
|
||||
shortUrl
|
||||
targetUrl
|
||||
description
|
||||
loadingScreen
|
||||
visits
|
||||
group {
|
||||
id
|
||||
name
|
||||
}
|
||||
domain {
|
||||
id
|
||||
name
|
||||
}
|
||||
|
||||
...DB_MODEL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${DB_MODEL_FRAGMENT}
|
||||
`,
|
||||
variables: {
|
||||
filter: [{ group: { deleted: { equal: false } } }, ...(filter ?? [])],
|
||||
sort: [{ id: SortOrder.DESC }, ...(sort ?? [])],
|
||||
skip,
|
||||
take,
|
||||
},
|
||||
});
|
||||
|
||||
const query2 = this.apollo.query<{ shortUrls: QueryResult<ShortUrl> }>({
|
||||
query: gql`
|
||||
query getShortUrls($filter: [ShortUrlFilter], $sort: [ShortUrlSort]) {
|
||||
shortUrls(filter: $filter, sort: $sort) {
|
||||
nodes {
|
||||
id
|
||||
shortUrl
|
||||
targetUrl
|
||||
description
|
||||
loadingScreen
|
||||
visits
|
||||
group {
|
||||
id
|
||||
name
|
||||
}
|
||||
domain {
|
||||
id
|
||||
name
|
||||
}
|
||||
|
||||
...DB_MODEL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${DB_MODEL_FRAGMENT}
|
||||
`,
|
||||
variables: {
|
||||
filter: [{ group: { isNull: true } }, ...(filter ?? [])],
|
||||
sort: [{ id: SortOrder.DESC }, ...(sort ?? [])],
|
||||
skip,
|
||||
take,
|
||||
},
|
||||
});
|
||||
|
||||
return forkJoin([query1, query2]).pipe(
|
||||
map(([result1, result2]) => {
|
||||
const nodes = [
|
||||
...result1.data.shortUrls.nodes,
|
||||
...result2.data.shortUrls.nodes,
|
||||
];
|
||||
const uniqueNodes = Array.from(
|
||||
new Map(nodes.map(node => [node.id, node])).values()
|
||||
);
|
||||
return { ...result1.data.shortUrls, nodes: uniqueNodes };
|
||||
${DB_MODEL_FRAGMENT}
|
||||
`,
|
||||
variables: {
|
||||
filter: [
|
||||
{
|
||||
userSpace: {
|
||||
id: { equal: this.sidebarService.selectedUserSpace$.value?.id },
|
||||
},
|
||||
},
|
||||
{ group: { deleted: { equal: false } } },
|
||||
{
|
||||
group: {
|
||||
userSpace: {
|
||||
id: {
|
||||
equal: this.sidebarService.selectedUserSpace$.value?.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...(filter ?? []),
|
||||
],
|
||||
filter2: [
|
||||
{
|
||||
userSpace: {
|
||||
id: { equal: this.sidebarService.selectedUserSpace$.value?.id },
|
||||
},
|
||||
},
|
||||
{
|
||||
group: {
|
||||
isNull: true,
|
||||
},
|
||||
},
|
||||
...(filter ?? []),
|
||||
],
|
||||
sort: [{ id: SortOrder.DESC }, ...(sort ?? [])],
|
||||
skip,
|
||||
take,
|
||||
},
|
||||
})
|
||||
);
|
||||
.pipe(
|
||||
map(x => {
|
||||
return {
|
||||
count: x.data.shortUrls.count + x.data.shortUrlsWithoutGroup.count,
|
||||
totalCount:
|
||||
x.data.shortUrls.totalCount +
|
||||
x.data.shortUrlsWithoutGroup.totalCount,
|
||||
nodes: [
|
||||
...x.data.shortUrls.nodes,
|
||||
...x.data.shortUrlsWithoutGroup.nodes,
|
||||
],
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
loadById(id: number): Observable<ShortUrl> {
|
||||
@ -263,6 +290,7 @@ export class ShortUrlsDataService
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
userSpaceId: this.sidebarService.selectedUserSpace$.value?.id,
|
||||
shortUrl: object.shortUrl,
|
||||
targetUrl: object.targetUrl,
|
||||
description: object.description,
|
||||
@ -365,12 +393,12 @@ export class ShortUrlsDataService
|
||||
.pipe(map(result => result.data?.shortUrl.restore ?? false));
|
||||
}
|
||||
|
||||
getAllGroups() {
|
||||
getAllAvailableGroups() {
|
||||
return this.apollo
|
||||
.query<{ groups: QueryResult<Group> }>({
|
||||
query: gql`
|
||||
query getGroups {
|
||||
groups {
|
||||
query getGroups($filter: [GroupFilter]) {
|
||||
groups(filter: $filter) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
@ -378,6 +406,16 @@ export class ShortUrlsDataService
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
filter: [
|
||||
{
|
||||
userSpace: {
|
||||
id: { equal: this.sidebarService.selectedUserSpace$.value?.id },
|
||||
},
|
||||
},
|
||||
{ deleted: { equal: false } },
|
||||
],
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
catchError(err => {
|
||||
|
@ -22,7 +22,7 @@ const routes: Routes = [
|
||||
canActivate: [PermissionGuard],
|
||||
data: {
|
||||
permissions: [PermissionsEnum.shortUrlsCreate],
|
||||
checkByPerUserSetup: true,
|
||||
isInUserSpace: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -31,7 +31,7 @@ const routes: Routes = [
|
||||
canActivate: [PermissionGuard],
|
||||
data: {
|
||||
permissions: [PermissionsEnum.shortUrlsUpdate],
|
||||
checkByPerUserSetup: true,
|
||||
isInUserSpace: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -40,7 +40,7 @@ const routes: Routes = [
|
||||
canActivate: [PermissionGuard],
|
||||
data: {
|
||||
permissions: [PermissionsEnum.shortUrls],
|
||||
checkByPerUserSetup: true,
|
||||
isInUserSpace: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user