mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
WIP: frontend module federation setup.
Initial work to accept future user related apps.
This commit is contained in:
@@ -23,6 +23,7 @@ node_modules
|
||||
/static/dist
|
||||
/frontend/webpack-stats.json
|
||||
/frontend/src/translations.json
|
||||
/frontend/.__mf__temp/
|
||||
|
||||
# TOX stuff
|
||||
test_project/modoboa.db
|
||||
|
||||
+6
-3
@@ -9,14 +9,15 @@ services:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.dev
|
||||
environment:
|
||||
PYTHONPATH: ".."
|
||||
DB: "SQLITE"
|
||||
DEBUG: "True"
|
||||
REDIS_HOST: redis
|
||||
PYTHONPATH: "..:/modoboa_user"
|
||||
command: >
|
||||
sh -c "cd test_project; python3 manage.py rqworker dkim modoboa"
|
||||
volumes:
|
||||
- .:/code
|
||||
- ../modoboa-user:/modoboa_user
|
||||
|
||||
api:
|
||||
container_name: modoboa_api
|
||||
@@ -29,10 +30,10 @@ services:
|
||||
env_file:
|
||||
- test_project/oidc_key
|
||||
environment:
|
||||
PYTHONPATH: ".."
|
||||
DB: "SQLITE"
|
||||
DEBUG: "True"
|
||||
REDIS_HOST: redis
|
||||
PYTHONPATH: "..:/modoboa_user"
|
||||
command: >
|
||||
sh -c "cd test_project
|
||||
python3 manage.py migrate
|
||||
@@ -41,6 +42,7 @@ services:
|
||||
python3 manage.py runserver_plus 0.0.0.0:8000 --cert-file cert.crt"
|
||||
volumes:
|
||||
- .:/code
|
||||
- ../modoboa-user:/modoboa_user
|
||||
|
||||
api-unsecured:
|
||||
container_name: modoboa_api_unsecured
|
||||
@@ -53,15 +55,16 @@ services:
|
||||
env_file:
|
||||
- test_project/oidc_key
|
||||
environment:
|
||||
PYTHONPATH: ".."
|
||||
DB: "SQLITE"
|
||||
DEBUG: "True"
|
||||
REDIS_HOST: redis
|
||||
PYTHONPATH: "..:/modoboa_user"
|
||||
command: >
|
||||
sh -c "cd test_project
|
||||
python3 manage.py runserver_plus 0.0.0.0:8001"
|
||||
volumes:
|
||||
- .:/code
|
||||
- ../modoboa-user:/modoboa_user
|
||||
|
||||
front:
|
||||
container_name: modoboa_front
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
FROM python:3.12-alpine3.20 AS base
|
||||
|
||||
LABEL org.opencontainers.image.authors="Antoine Nguyen <tonio@ngyn.org>"
|
||||
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
@@ -12,11 +13,11 @@ RUN apk add --update openssl librrd python3-dev libffi-dev gcc g++ musl-dev libx
|
||||
RUN python3 -m venv $VIRTUAL_ENV
|
||||
WORKDIR /tmp
|
||||
COPY requirements.txt /tmp
|
||||
COPY user-requirements.txt /tmp
|
||||
COPY test-requirements.txt /tmp
|
||||
COPY dev-requirements.txt /tmp
|
||||
RUN python -m pip install -U pip
|
||||
RUN pip install -r requirements.txt -r test-requirements.txt -r dev-requirements.txt
|
||||
|
||||
RUN pip install -r requirements.txt -r user-requirements.txt -r test-requirements.txt -r dev-requirements.txt
|
||||
FROM base AS run
|
||||
COPY docker/doveadm /usr/local/bin
|
||||
RUN chmod +x /usr/local/bin/doveadm
|
||||
|
||||
@@ -114,4 +114,7 @@ export default {
|
||||
`${resource}/filtersets/${filterSetName}/filters/${filter}/`
|
||||
)
|
||||
},
|
||||
getAvailableApplications() {
|
||||
return repository.get(`${resource}/available_applications/`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<v-btn icon="mdi-apps" flat> </v-btn>
|
||||
</template>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="top-menu">
|
||||
<v-menu activator="parent" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn icon="mdi-apps" flat class="mr-4" v-bind="props"></v-btn>
|
||||
</template>
|
||||
<v-card min-width="350" max-width="400" class="pa-4">
|
||||
<v-row class="justify-center">
|
||||
<v-col
|
||||
v-for="application in applications"
|
||||
:key="application.name"
|
||||
cols="6"
|
||||
>
|
||||
<v-sheet
|
||||
rounded
|
||||
class="application text-center pa-4 text-body-2"
|
||||
@click="$router.push(application.url)"
|
||||
>
|
||||
<v-icon color="primary" :icon="application.icon" size="x-large" />
|
||||
<div class="mt-2">{{ application.label }}</div>
|
||||
</v-sheet>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<v-btn icon flat color="primary">
|
||||
{{ userInitials }}
|
||||
<v-menu activator="parent" location="bottom">
|
||||
<v-card min-width="300" max-width="350">
|
||||
<v-list>
|
||||
<div class="text-center">
|
||||
<v-avatar color="primary">
|
||||
<span class="text-h5">{{ userInitials }}</span>
|
||||
</v-avatar>
|
||||
<v-list-item :title="user.username" />
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
<template v-if="remote">
|
||||
<v-list-item href="https://localhost:3000/account">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-account-circle-outline"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Account</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="logout">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-logout"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Logout</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { getActivePinia } from 'pinia'
|
||||
import accountApi from '@/api/account'
|
||||
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
remote: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const applications = ref([])
|
||||
|
||||
const userInitials = computed(() => {
|
||||
return props.user.first_name && props.user.last_name
|
||||
? `${props.user.first_name[0].toUpperCase()}${props.user.last_name[0].toUpperCase()}`
|
||||
: props.user.username.slice(0, 2).toUpperCase()
|
||||
})
|
||||
|
||||
async function logout() {
|
||||
getActivePinia()._s.forEach(async (store) => await store.$reset())
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
accountApi.getAvailableApplications().then((resp) => {
|
||||
applications.value = resp.data
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.top-menu {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.application {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.application:hover {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-fab app icon flat location="top right" color="primary">
|
||||
<v-btn icon flat color="primary">
|
||||
{{ userInitials }}
|
||||
<v-menu activator="parent" location="bottom">
|
||||
<v-card min-width="300" max-width="350">
|
||||
@@ -28,7 +28,7 @@
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-fab>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
src="@/assets/Modoboa_RVB-BLANC-SANS.png"
|
||||
max-width="190"
|
||||
class="logo"
|
||||
@click="router.push({ name: 'Dashboard' })"
|
||||
@click="router.push(props.logoRoute)"
|
||||
/>
|
||||
<v-btn
|
||||
:icon="rail ? 'mdi-chevron-right' : 'mdi-chevron-left'"
|
||||
@@ -24,8 +24,20 @@
|
||||
<v-list nav>
|
||||
<template v-for="item in menuItems" :key="item.text">
|
||||
<template v-if="displayMenuItem(item)">
|
||||
<v-list-subheader v-if="item.subheader">
|
||||
{{ item.text.toUpperCase() }}
|
||||
</v-list-subheader>
|
||||
<v-list-item
|
||||
v-if="!item.children"
|
||||
v-else-if="item.action"
|
||||
:value="item"
|
||||
:exact="item.exact"
|
||||
:title="item.text"
|
||||
:prepend-icon="item.icon"
|
||||
@click="item.action"
|
||||
>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-else-if="!item.children"
|
||||
:value="item"
|
||||
:to="item.to"
|
||||
link
|
||||
@@ -124,6 +136,10 @@ const props = defineProps({
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
logoRoute: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
@@ -358,6 +374,12 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-subheader {
|
||||
&__text {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -37,10 +37,13 @@ export default defineConfig({
|
||||
name: 'coretools',
|
||||
filename: 'remoteEntry.js',
|
||||
exposes: {
|
||||
'./api-repository': './src/api/repository.js',
|
||||
'./auth-store': './src/stores/auth.store.js',
|
||||
'./auth-callback': './src/views/login/LoginCallbackView.vue',
|
||||
'./navbar': './src/layouts/dashboard/NavBar.vue',
|
||||
'./user-menu': './src/components/shared/UserMenu.vue',
|
||||
'./top-menu': './src/components/shared/TopMenu.vue',
|
||||
'./menu-items': './src/components/tools/MenuItems.vue',
|
||||
'./form-rules': './src/plugins/rules.js',
|
||||
},
|
||||
shared: ['vue', 'pinia', 'vue-router', 'vue3-gettext'],
|
||||
}),
|
||||
|
||||
@@ -531,3 +531,12 @@ class NotificationSerializer(serializers.Serializer):
|
||||
url = serializers.CharField(required=False)
|
||||
text = serializers.CharField()
|
||||
level = serializers.CharField()
|
||||
|
||||
|
||||
class ModoboaApplicationSerializer(serializers.Serializer):
|
||||
|
||||
label = serializers.CharField()
|
||||
name = serializers.CharField()
|
||||
icon = serializers.CharField()
|
||||
url = serializers.CharField()
|
||||
description = serializers.CharField(required=False)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Core API v2 viewsets."""
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import filters, permissions, response, viewsets, mixins
|
||||
@@ -10,6 +12,7 @@ from rest_framework.exceptions import PermissionDenied
|
||||
from modoboa.admin.api.v1 import serializers as admin_v1_serializers
|
||||
from modoboa.core.api.v1 import serializers as core_v1_serializers
|
||||
from modoboa.core.api.v1 import viewsets as core_v1_viewsets
|
||||
from modoboa.core.extensions import exts_pool
|
||||
from modoboa.lib import pagination
|
||||
from modoboa.lib.permissions import IsSuperUser
|
||||
from modoboa.lib.throttle import GetThrottleViewsetMixin
|
||||
@@ -130,6 +133,27 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet):
|
||||
return response.Response(status=204)
|
||||
return response.Response({"tokens": tokens})
|
||||
|
||||
@extend_schema(
|
||||
description="Get the list of available Modoboa applications for current user",
|
||||
responses={200: serializers.ModoboaApplicationSerializer},
|
||||
)
|
||||
@action(methods=["get"], detail=False)
|
||||
def available_applications(self, request):
|
||||
apps = []
|
||||
if request.user.role != "SimpleUsers":
|
||||
apps.append(
|
||||
{
|
||||
"name": "admin",
|
||||
"label": _("Administration"),
|
||||
"icon": "mdi-toolbox",
|
||||
"description": _("Administration console"),
|
||||
"url": "/admin",
|
||||
}
|
||||
)
|
||||
apps += exts_pool.get_available_apps()
|
||||
serializer = serializers.ModoboaApplicationSerializer(apps, many=True)
|
||||
return response.Response(serializer.data)
|
||||
|
||||
|
||||
class LogViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet):
|
||||
"""Log viewset."""
|
||||
|
||||
+27
-14
@@ -1,12 +1,14 @@
|
||||
"""Extension management."""
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import include
|
||||
from django.urls import re_path
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
|
||||
class ModoExtension(object):
|
||||
class ModoExtension:
|
||||
"""
|
||||
Base extension class.
|
||||
|
||||
@@ -14,14 +16,17 @@ class ModoExtension(object):
|
||||
considered as valid.
|
||||
"""
|
||||
|
||||
name = None
|
||||
label = None
|
||||
version = "NA"
|
||||
description = ""
|
||||
needs_media = False
|
||||
always_active = False
|
||||
url = None
|
||||
topredirection_url = None
|
||||
name: str
|
||||
label: str
|
||||
version: str = "NA"
|
||||
description: str = ""
|
||||
needs_media: bool = False
|
||||
always_active: bool = False
|
||||
url: Optional[str] = None
|
||||
topredirection_url: Optional[str] = None
|
||||
|
||||
def get_available_apps(self) -> list:
|
||||
return []
|
||||
|
||||
def get_url(self):
|
||||
"""Return extension base url."""
|
||||
@@ -50,13 +55,13 @@ class ModoExtension(object):
|
||||
pass
|
||||
|
||||
|
||||
class ExtensionsPool(object):
|
||||
class ExtensionsPool:
|
||||
"""The extensions manager"""
|
||||
|
||||
def __init__(self):
|
||||
self.extensions = {}
|
||||
def __init__(self) -> None:
|
||||
self.extensions: Dict[str, Dict] = {}
|
||||
|
||||
def register_extension(self, ext, show=True):
|
||||
def register_extension(self, ext: ModoExtension, show: bool = True) -> None:
|
||||
"""Register an extension.
|
||||
|
||||
:param ext: a class inheriting from ``Extension``
|
||||
@@ -64,7 +69,7 @@ class ExtensionsPool(object):
|
||||
"""
|
||||
self.extensions[ext.name] = {"cls": ext, "show": show}
|
||||
|
||||
def get_extension(self, name):
|
||||
def get_extension(self, name: str) -> Optional[ModoExtension]:
|
||||
"""Retrieve the current instance of an extension."""
|
||||
if name not in self.extensions:
|
||||
return None
|
||||
@@ -102,6 +107,14 @@ class ExtensionsPool(object):
|
||||
for ext in settings.MODOBOA_APPS:
|
||||
self.load_extension(ext)
|
||||
|
||||
def get_available_apps(self) -> list:
|
||||
result: list = []
|
||||
for ext_name in list(self.extensions.keys()):
|
||||
ext = self.get_extension(ext_name)
|
||||
if ext:
|
||||
result += ext.get_available_apps()
|
||||
return result
|
||||
|
||||
def get_urls(self, category="app"):
|
||||
"""Get all urls defined by extensions."""
|
||||
result = []
|
||||
|
||||
@@ -102,6 +102,8 @@ MODOBOA_APPS = (
|
||||
"modoboa.postfix_autoreply",
|
||||
"modoboa.sievefilters",
|
||||
# Modoboa extensions here.
|
||||
"modoboa_user",
|
||||
"modoboa_user.contacts",
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user