Merge pull request 'feat: add roundcube service' (#119) from def/selfprivacy-rest-api:master into master

Reviewed-on: https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api/pulls/119
Reviewed-by: Inex Code <inex.code@selfprivacy.org>
This commit is contained in:
Inex Code 2024-07-15 16:45:46 +03:00
commit b510af725b
31 changed files with 253 additions and 76 deletions

View file

@ -1,8 +1,10 @@
"""API access status""" """API access status"""
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
import datetime import datetime
import typing import typing
import strawberry import strawberry
from strawberry.types import Info from strawberry.types import Info
from selfprivacy_api.actions.api_tokens import ( from selfprivacy_api.actions.api_tokens import (
get_api_tokens_with_caller_flag, get_api_tokens_with_caller_flag,

View file

@ -1,4 +1,5 @@
"""Backup""" """Backup"""
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
import typing import typing
import strawberry import strawberry

View file

@ -1,4 +1,5 @@
"""Common types and enums used by different types of queries.""" """Common types and enums used by different types of queries."""
from enum import Enum from enum import Enum
import datetime import datetime
import typing import typing

View file

@ -1,4 +1,5 @@
"""Jobs status""" """Jobs status"""
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
import strawberry import strawberry
from typing import List, Optional from typing import List, Optional

View file

@ -1,4 +1,5 @@
"""Enums representing different service providers.""" """Enums representing different service providers."""
from enum import Enum from enum import Enum
import strawberry import strawberry

View file

@ -1,4 +1,5 @@
"""Services status""" """Services status"""
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
import typing import typing
import strawberry import strawberry

View file

@ -1,4 +1,5 @@
"""Storage queries.""" """Storage queries."""
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
import typing import typing
import strawberry import strawberry
@ -18,9 +19,11 @@ class Storage:
"""Get list of volumes""" """Get list of volumes"""
return [ return [
StorageVolume( StorageVolume(
total_space=str(volume.fssize) total_space=(
str(volume.fssize)
if volume.fssize is not None if volume.fssize is not None
else str(volume.size), else str(volume.size)
),
free_space=str(volume.fsavail), free_space=str(volume.fsavail),
used_space=str(volume.fsused), used_space=str(volume.fsused),
root=volume.is_root(), root=volume.is_root(),

View file

@ -1,8 +1,10 @@
"""Common system information and settings""" """Common system information and settings"""
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
import os import os
import typing import typing
import strawberry import strawberry
from selfprivacy_api.graphql.common_types.dns import DnsRecord from selfprivacy_api.graphql.common_types.dns import DnsRecord
from selfprivacy_api.graphql.queries.common import Alert, Severity from selfprivacy_api.graphql.queries.common import Alert, Severity

View file

@ -1,4 +1,5 @@
"""Users""" """Users"""
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
import typing import typing
import strawberry import strawberry

View file

@ -14,10 +14,12 @@ from selfprivacy_api.migrations.write_token_to_redis import WriteTokenToRedis
from selfprivacy_api.migrations.check_for_system_rebuild_jobs import ( from selfprivacy_api.migrations.check_for_system_rebuild_jobs import (
CheckForSystemRebuildJobs, CheckForSystemRebuildJobs,
) )
from selfprivacy_api.migrations.add_roundcube import AddRoundcube
migrations = [ migrations = [
WriteTokenToRedis(), WriteTokenToRedis(),
CheckForSystemRebuildJobs(), CheckForSystemRebuildJobs(),
AddRoundcube(),
] ]

View file

@ -0,0 +1,36 @@
from selfprivacy_api.migrations.migration import Migration
from selfprivacy_api.services.flake_service_manager import FlakeServiceManager
from selfprivacy_api.utils import ReadUserData, WriteUserData
class AddRoundcube(Migration):
"""Adds the Roundcube if it is not present."""
def get_migration_name(self) -> str:
return "add_roundcube"
def get_migration_description(self) -> str:
return "Adds the Roundcube if it is not present."
def is_migration_needed(self) -> bool:
with FlakeServiceManager() as manager:
if "roundcube" not in manager.services:
return True
with ReadUserData() as data:
if "roundcube" not in data["modules"]:
return True
return False
def migrate(self) -> None:
with FlakeServiceManager() as manager:
if "roundcube" not in manager.services:
manager.services[
"roundcube"
] = "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git?ref=flakes&dir=sp-modules/roundcube"
with WriteUserData() as data:
if "roundcube" not in data["modules"]:
data["modules"]["roundcube"] = {
"enable": False,
"subdomain": "roundcube",
}

View file

@ -5,13 +5,13 @@ from selfprivacy_api.jobs import JobStatus, Jobs
class CheckForSystemRebuildJobs(Migration): class CheckForSystemRebuildJobs(Migration):
"""Check if there are unfinished system rebuild jobs and finish them""" """Check if there are unfinished system rebuild jobs and finish them"""
def get_migration_name(self): def get_migration_name(self) -> str:
return "check_for_system_rebuild_jobs" return "check_for_system_rebuild_jobs"
def get_migration_description(self): def get_migration_description(self) -> str:
return "Check if there are unfinished system rebuild jobs and finish them" return "Check if there are unfinished system rebuild jobs and finish them"
def is_migration_needed(self): def is_migration_needed(self) -> bool:
# Check if there are any unfinished system rebuild jobs # Check if there are any unfinished system rebuild jobs
for job in Jobs.get_jobs(): for job in Jobs.get_jobs():
if ( if (
@ -25,8 +25,9 @@ class CheckForSystemRebuildJobs(Migration):
JobStatus.RUNNING, JobStatus.RUNNING,
]: ]:
return True return True
return False
def migrate(self): def migrate(self) -> None:
# As the API is restarted, we assume that the jobs are finished # As the API is restarted, we assume that the jobs are finished
for job in Jobs.get_jobs(): for job in Jobs.get_jobs():
if ( if (

View file

@ -12,17 +12,17 @@ class Migration(ABC):
""" """
@abstractmethod @abstractmethod
def get_migration_name(self): def get_migration_name(self) -> str:
pass pass
@abstractmethod @abstractmethod
def get_migration_description(self): def get_migration_description(self) -> str:
pass pass
@abstractmethod @abstractmethod
def is_migration_needed(self): def is_migration_needed(self) -> bool:
pass pass
@abstractmethod @abstractmethod
def migrate(self): def migrate(self) -> None:
pass pass

View file

@ -15,10 +15,10 @@ from selfprivacy_api.utils import ReadUserData, UserDataFiles
class WriteTokenToRedis(Migration): class WriteTokenToRedis(Migration):
"""Load Json tokens into Redis""" """Load Json tokens into Redis"""
def get_migration_name(self): def get_migration_name(self) -> str:
return "write_token_to_redis" return "write_token_to_redis"
def get_migration_description(self): def get_migration_description(self) -> str:
return "Loads the initial token into redis token storage" return "Loads the initial token into redis token storage"
def is_repo_empty(self, repo: AbstractTokensRepository) -> bool: def is_repo_empty(self, repo: AbstractTokensRepository) -> bool:
@ -38,7 +38,7 @@ class WriteTokenToRedis(Migration):
print(e) print(e)
return None return None
def is_migration_needed(self): def is_migration_needed(self) -> bool:
try: try:
if self.get_token_from_json() is not None and self.is_repo_empty( if self.get_token_from_json() is not None and self.is_repo_empty(
RedisTokensRepository() RedisTokensRepository()
@ -47,8 +47,9 @@ class WriteTokenToRedis(Migration):
except Exception as e: except Exception as e:
print(e) print(e)
return False return False
return False
def migrate(self): def migrate(self) -> None:
# Write info about providers to userdata.json # Write info about providers to userdata.json
try: try:
token = self.get_token_from_json() token = self.get_token_from_json()

View file

@ -4,6 +4,7 @@ import typing
from selfprivacy_api.services.bitwarden import Bitwarden from selfprivacy_api.services.bitwarden import Bitwarden
from selfprivacy_api.services.forgejo import Forgejo from selfprivacy_api.services.forgejo import Forgejo
from selfprivacy_api.services.jitsimeet import JitsiMeet from selfprivacy_api.services.jitsimeet import JitsiMeet
from selfprivacy_api.services.roundcube import Roundcube
from selfprivacy_api.services.mailserver import MailServer from selfprivacy_api.services.mailserver import MailServer
from selfprivacy_api.services.nextcloud import Nextcloud from selfprivacy_api.services.nextcloud import Nextcloud
from selfprivacy_api.services.pleroma import Pleroma from selfprivacy_api.services.pleroma import Pleroma
@ -19,6 +20,7 @@ services: list[Service] = [
Pleroma(), Pleroma(),
Ocserv(), Ocserv(),
JitsiMeet(), JitsiMeet(),
Roundcube(),
] ]

View file

@ -37,14 +37,14 @@ class Bitwarden(Service):
def get_user() -> str: def get_user() -> str:
return "vaultwarden" return "vaultwarden"
@staticmethod @classmethod
def get_url() -> Optional[str]: def get_url(cls) -> Optional[str]:
"""Return service url.""" """Return service url."""
domain = get_domain() domain = get_domain()
return f"https://password.{domain}" return f"https://password.{domain}"
@staticmethod @classmethod
def get_subdomain() -> Optional[str]: def get_subdomain(cls) -> Optional[str]:
return "password" return "password"
@staticmethod @staticmethod

View file

@ -36,14 +36,14 @@ class Forgejo(Service):
"""Read SVG icon from file and return it as base64 encoded string.""" """Read SVG icon from file and return it as base64 encoded string."""
return base64.b64encode(FORGEJO_ICON.encode("utf-8")).decode("utf-8") return base64.b64encode(FORGEJO_ICON.encode("utf-8")).decode("utf-8")
@staticmethod @classmethod
def get_url() -> Optional[str]: def get_url(cls) -> Optional[str]:
"""Return service url.""" """Return service url."""
domain = get_domain() domain = get_domain()
return f"https://git.{domain}" return f"https://git.{domain}"
@staticmethod @classmethod
def get_subdomain() -> Optional[str]: def get_subdomain(cls) -> Optional[str]:
return "git" return "git"
@staticmethod @staticmethod

View file

@ -36,14 +36,14 @@ class JitsiMeet(Service):
"""Read SVG icon from file and return it as base64 encoded string.""" """Read SVG icon from file and return it as base64 encoded string."""
return base64.b64encode(JITSI_ICON.encode("utf-8")).decode("utf-8") return base64.b64encode(JITSI_ICON.encode("utf-8")).decode("utf-8")
@staticmethod @classmethod
def get_url() -> Optional[str]: def get_url(cls) -> Optional[str]:
"""Return service url.""" """Return service url."""
domain = get_domain() domain = get_domain()
return f"https://meet.{domain}" return f"https://meet.{domain}"
@staticmethod @classmethod
def get_subdomain() -> Optional[str]: def get_subdomain(cls) -> Optional[str]:
return "meet" return "meet"
@staticmethod @staticmethod

View file

@ -35,13 +35,13 @@ class MailServer(Service):
def get_user() -> str: def get_user() -> str:
return "virtualMail" return "virtualMail"
@staticmethod @classmethod
def get_url() -> Optional[str]: def get_url(cls) -> Optional[str]:
"""Return service url.""" """Return service url."""
return None return None
@staticmethod @classmethod
def get_subdomain() -> Optional[str]: def get_subdomain(cls) -> Optional[str]:
return None return None
@staticmethod @staticmethod

View file

@ -4,7 +4,6 @@ import subprocess
from typing import Optional, List from typing import Optional, List
from selfprivacy_api.utils import get_domain from selfprivacy_api.utils import get_domain
from selfprivacy_api.jobs import Job, Jobs
from selfprivacy_api.utils.systemd import get_service_status from selfprivacy_api.utils.systemd import get_service_status
from selfprivacy_api.services.service import Service, ServiceStatus from selfprivacy_api.services.service import Service, ServiceStatus
@ -35,14 +34,14 @@ class Nextcloud(Service):
"""Read SVG icon from file and return it as base64 encoded string.""" """Read SVG icon from file and return it as base64 encoded string."""
return base64.b64encode(NEXTCLOUD_ICON.encode("utf-8")).decode("utf-8") return base64.b64encode(NEXTCLOUD_ICON.encode("utf-8")).decode("utf-8")
@staticmethod @classmethod
def get_url() -> Optional[str]: def get_url(cls) -> Optional[str]:
"""Return service url.""" """Return service url."""
domain = get_domain() domain = get_domain()
return f"https://cloud.{domain}" return f"https://cloud.{domain}"
@staticmethod @classmethod
def get_subdomain() -> Optional[str]: def get_subdomain(cls) -> Optional[str]:
return "cloud" return "cloud"
@staticmethod @staticmethod

View file

@ -28,13 +28,13 @@ class Ocserv(Service):
def get_svg_icon() -> str: def get_svg_icon() -> str:
return base64.b64encode(OCSERV_ICON.encode("utf-8")).decode("utf-8") return base64.b64encode(OCSERV_ICON.encode("utf-8")).decode("utf-8")
@staticmethod @classmethod
def get_url() -> typing.Optional[str]: def get_url(cls) -> typing.Optional[str]:
"""Return service url.""" """Return service url."""
return None return None
@staticmethod @classmethod
def get_subdomain() -> typing.Optional[str]: def get_subdomain(cls) -> typing.Optional[str]:
return "vpn" return "vpn"
@staticmethod @staticmethod

View file

@ -31,14 +31,14 @@ class Pleroma(Service):
def get_svg_icon() -> str: def get_svg_icon() -> str:
return base64.b64encode(PLEROMA_ICON.encode("utf-8")).decode("utf-8") return base64.b64encode(PLEROMA_ICON.encode("utf-8")).decode("utf-8")
@staticmethod @classmethod
def get_url() -> Optional[str]: def get_url(cls) -> Optional[str]:
"""Return service url.""" """Return service url."""
domain = get_domain() domain = get_domain()
return f"https://social.{domain}" return f"https://social.{domain}"
@staticmethod @classmethod
def get_subdomain() -> Optional[str]: def get_subdomain(cls) -> Optional[str]:
return "social" return "social"
@staticmethod @staticmethod

View file

@ -0,0 +1,113 @@
"""Class representing Roundcube service"""
import base64
import subprocess
from typing import List, Optional
from selfprivacy_api.jobs import Job
from selfprivacy_api.utils.systemd import (
get_service_status_from_several_units,
)
from selfprivacy_api.services.service import Service, ServiceStatus
from selfprivacy_api.utils import ReadUserData, get_domain
from selfprivacy_api.utils.block_devices import BlockDevice
from selfprivacy_api.services.roundcube.icon import ROUNDCUBE_ICON
class Roundcube(Service):
"""Class representing roundcube service"""
@staticmethod
def get_id() -> str:
"""Return service id."""
return "roundcube"
@staticmethod
def get_display_name() -> str:
"""Return service display name."""
return "Roundcube"
@staticmethod
def get_description() -> str:
"""Return service description."""
return "Roundcube is an open source webmail software."
@staticmethod
def get_svg_icon() -> str:
"""Read SVG icon from file and return it as base64 encoded string."""
return base64.b64encode(ROUNDCUBE_ICON.encode("utf-8")).decode("utf-8")
@classmethod
def get_url(cls) -> Optional[str]:
"""Return service url."""
domain = get_domain()
subdomain = cls.get_subdomain()
return f"https://{subdomain}.{domain}"
@classmethod
def get_subdomain(cls) -> Optional[str]:
with ReadUserData() as data:
if "roundcube" in data["modules"]:
return data["modules"]["roundcube"]["subdomain"]
return "roundcube"
@staticmethod
def is_movable() -> bool:
return False
@staticmethod
def is_required() -> bool:
return False
@staticmethod
def can_be_backed_up() -> bool:
return False
@staticmethod
def get_backup_description() -> str:
return "Nothing to backup."
@staticmethod
def get_status() -> ServiceStatus:
return get_service_status_from_several_units(["phpfpm-roundcube.service"])
@staticmethod
def stop():
subprocess.run(
["systemctl", "stop", "phpfpm-roundcube.service"],
check=False,
)
@staticmethod
def start():
subprocess.run(
["systemctl", "start", "phpfpm-roundcube.service"],
check=False,
)
@staticmethod
def restart():
subprocess.run(
["systemctl", "restart", "phpfpm-roundcube.service"],
check=False,
)
@staticmethod
def get_configuration():
return {}
@staticmethod
def set_configuration(config_items):
return super().set_configuration(config_items)
@staticmethod
def get_logs():
return ""
@staticmethod
def get_folders() -> List[str]:
return []
def move_to_volume(self, volume: BlockDevice) -> Job:
raise NotImplementedError("roundcube service is not movable")

View file

@ -0,0 +1,7 @@
ROUNDCUBE_ICON = """
<svg fill="none" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(29.07 -.3244)">
<path d="m-17.02 2.705c-4.01 2e-7 -7.283 3.273-7.283 7.283 0 0.00524-1.1e-5 0.01038 0 0.01562l-1.85 1.068v5.613l9.105 5.26 9.104-5.26v-5.613l-1.797-1.037c1.008e-4 -0.01573 0.00195-0.03112 0.00195-0.04688-1e-7 -4.01-3.271-7.283-7.281-7.283zm0 2.012c2.923 1e-7 5.27 2.349 5.27 5.271 0 2.923-2.347 5.27-5.27 5.27-2.923-1e-6 -5.271-2.347-5.271-5.27 0-2.923 2.349-5.271 5.271-5.271z" fill="#000" fill-rule="evenodd" stroke-linejoin="bevel"/>
</g>
</svg>
"""

View file

@ -65,17 +65,17 @@ class Service(ABC):
""" """
pass pass
@staticmethod @classmethod
@abstractmethod @abstractmethod
def get_url() -> Optional[str]: def get_url(cls) -> Optional[str]:
""" """
The url of the service if it is accessible from the internet browser. The url of the service if it is accessible from the internet browser.
""" """
pass pass
@staticmethod @classmethod
@abstractmethod @abstractmethod
def get_subdomain() -> Optional[str]: def get_subdomain(cls) -> Optional[str]:
""" """
The assigned primary subdomain for this service. The assigned primary subdomain for this service.
""" """

View file

@ -57,14 +57,14 @@ class DummyService(Service):
# return "" # return ""
return base64.b64encode(BITWARDEN_ICON.encode("utf-8")).decode("utf-8") return base64.b64encode(BITWARDEN_ICON.encode("utf-8")).decode("utf-8")
@staticmethod @classmethod
def get_url() -> typing.Optional[str]: def get_url(cls) -> typing.Optional[str]:
"""Return service url.""" """Return service url."""
domain = "test.com" domain = "test.com"
return f"https://password.{domain}" return f"https://password.{domain}"
@staticmethod @classmethod
def get_subdomain() -> typing.Optional[str]: def get_subdomain(cls) -> typing.Optional[str]:
return "password" return "password"
@classmethod @classmethod

View file

@ -62,6 +62,9 @@
"simple-nixos-mailserver": { "simple-nixos-mailserver": {
"enable": true, "enable": true,
"location": "sdb" "location": "sdb"
},
"roundcube": {
"enable": true
} }
}, },
"volumes": [ "volumes": [