Entwurf einer
dienstorientierten
Plug-in-Architektur für
Webanwendungen

am Beispiel von

Bachelorarbeit

Jonas Thelemann

20.01.2022

Gliederung

  1. Projekt
  2. Entwurf
  3. Implementierung
  4. Vertiefung

Code

Dritter

Design

+

Kontext

2014

2016

2018

2020

2022

Gründung NAHhaft

Idee                     

Antragsphase

Gründung, Beta

Logistik-Marktplatz

nearbuy

Dashboard

Betriebsprofil

Marktplatz

nearbuy

Frontend

React

Anforderungen

  1. 🔒 sichere Datenzugriffe
  2. 🕵 Nachvollziehbarkeit von Interaktionen
  3. ⛓ Sicherstellung der Kompatibilität
     
  4. 🎨 visuelle Konsistenz
     
  5. 🏪 Erstellung eines Plug-in-Markplatzes
  6. 🔂 Installation pro Konto oder Betrieb
  7. 🪝 mehrere Erweiterungspunkte

Entwurf

Entwurf

Entwurf

Entwurf

Netzwerkstruktur

Netzwerkstruktur

Flexible Verwendung konsistenten Designs

Webkomponenten

Flexible Verwendung konsistenten Designs

Webkomponenten

Stencil

Flexible Verwendung konsistenten Designs

Webkomponenten

Stencil

Bit

Flexible Verwendung konsistenten Designs

Webkomponenten

Stencil

Bit

Datenmodell eines Plug-ins

const nearbuyPluginMuesliIndex: NearbuyPluginOptions = {
  id: "muesli-index",
  name: {
  	de: "Muesli-Index", en: "Muesli Index"
  },
  description: {
    en: "Calculates the 'Muesli index', representing the availability of ingredients
    required for a yummy muesli. Requests for missing ingredients can be added
    with one click."
  },
  version: packageJson.version,
  versionNearbuyCompatibility: packageJson.peerDependencies.nearbuy,
  url: new URL("http://localhost:3001"),
  iconPngBase64: "iVBORw0KGgoAAAA...",
  entryPoints: {
    dashboard: [{
      dashlet: [{
      	contentUrl: "https://nearbuy_muesli-index.jonas-thelemann.de/",
      }],
    }],
    plugin: {
    	contentUrl: "http://localhost:3001/",
    },
  },
}

Implementierung

  1. 🔑 Autorisierung von API-Zugriffen durch Dritte
  2. 💻 Webkomponenten
  3. 📦 Plug-in-Installation

1/2 Konfiguration von Keycloak

2/2 Erweiterung der Anfragevalidierung im Backend

val userinfo = parseUserinfo(String(Base64.getDecoder().decode(xUserinfoEncoded)))
this.scopes.addAll(userinfo.scope.split(" "))
val annotationScope =
	"${annotation.clazz.name.lowercase()}-${annotation.action.name.lowercase()}"
    
if (!(requestContext.securityContext as NearbuySecurityContext).hasScope(annotationScope)) {
	abort(requestContext)
}
@Allowed(READ, COMPANY, "{companyId}")

Autorisierung von API-Zugriffen durch Dritte

<Button
    onClick={(): void => this.props.
    onClose()}
/>


<Button
    onClick={(): void => this.saveOffer()}
    color="primary"
    variant="contained"
    disabled={...}
/>
<NbButton
    onNbButtonEvent={(): void =>
        this.props.onClose()
    }
    secondary
/>
<NbButton
    onNbButtonEvent={(): void =>
        this.saveOffer()
    }
    disabled={...}
/>

React

Einbindung in Webanwendungen

Buttons

Webkomponenten

import { applyPolyfills, defineCustomElements }
	from "@dargmuesli/nearbuy-web-components/loader"

Vue.config.ignoredElements = [[/nb-\w+/]

applyPolyfills().then(() => {
    defineCustomElements()
})
{
    hydratePath: ’@dargmuesli/nearbuy-web-components/hydrate’,
    lib: ’@dargmuesli/nearbuy-web-components’,
    loaderPath: ’@dargmuesli/nearbuy-web-components/loader’,
    prefix: ’nb-’,
},

Vue

Nuxt

Einbindung in Webanwendungen

Webkomponenten

Webkomponenten

@Prop() secondary: boolean = false;
@Prop() disabled: boolean = false;
@Prop({ attribute: ’first-last’ }) name: Name = {
    first: ’Max’,
    last: ’Mustermann’
};
@Event() nbButtonEvent: EventEmitter<string>;



render() {
  return <button
        class={[
            "font-medium leading-7 px-4 py-1 rounded-full text-sm uppercase",
            ...(this.secondary ? ["nb-secondary"] : []),
            ...(this.disabled ? ["nb-disabled"] : [])
        ].join(’ ’)}
        onClick={() => this.nbButtonEvent.emit(this.getText())}
        part="nb-button"
        type="button"
    >
        <slot>{this.getText()}</slot>
    </button>;
}

Erzeugung mit Stencil

Plug-in-Marktplatz

Plug-in-Installation

Visualisierung im Frontend

Plug-in-Oberfläche

Plug-in-Installation

Visualisierung im Frontend

Vertiefungen

1/3 Globales Theming trotz Shadow DOM

button {
    background-color: var(--primary-color, #92BE9B);
    font-family: ’Quicksand’, sans-serif;
    @apply font-bold shadow-md text-white;
}
body.theme-muesli-index nb-button[secondary]::part(nb-button):hover {
    background-color: rgba(72 111 80 / 4%);
}

CSS Custom Property

CSS Shadow Parts

2/3 API-Versionierung

Semantic

2/3 API-Versionierung

Semantic

Periodic

2/3 API-Versionierung

Semantic

Periodic

Deprecation

https://api.nearbuy-food.de/v1/public/companies

3/3 Weiterentwicklung des Kubernetes-Clusters

Istio

3/3 Weiterentwicklung des Kubernetes-Clusters

Istio

Flagger

3/3 Weiterentwicklung des Kubernetes-Clusters

Istio

Flagger

OPA

Zusammenfassung, Fazit und Ausblick

  • Entwurf
    • Plug-in-System
    • Plug-in + Dienst
    • Plug-in-Marktplatz
       
  • Gegenvorschlag
    • Keycloak
    • Backend-Auth
  • Design über Webkomponenten
    • React, Vue, Nuxt
       
  • Empfehlungen für Kubernetes
     
  • Codeanalyse, -stil & CI

nearbuy

Quarkus

Frontend

React

Backend

nearbuy

Quarkus

Frontend

React

Backend

Kubernetes

Orchestrierung

Plug-in-Installation

Persistenz im Backend

CREATE TABLE plugin_installation (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    plugin_id TEXT NOT NULL,
    owner_person UUID REFERENCES person ON DELETE CASCADE,
    owner_company UUID REFERENCES company ON DELETE CASCADE,
    date_installed TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
    UNIQUE (plugin_id, owner_person),
    UNIQUE (plugin_id, owner_company),
    CONSTRAINT either_person_or_company CHECK (num_nonnulls(owner_person, owner_company) = 1)
)

PostgreSQL

Made with Slides.com