Add oidc

This commit is contained in:
Andreas Dinauer 2026-03-15 09:03:40 +01:00
parent f4b04691f5
commit 424b9ed713
23 changed files with 256 additions and 204 deletions

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM node:22-alpine
WORKDIR /usr/app
COPY . .
ARG VERSION
RUN npm install
RUN npx nuxi cleanup
RUN npx nuxi build
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

18
Jenkinsfile vendored
View File

@ -2,26 +2,10 @@ pipeline {
agent any
stages {
stage('Tag git commit') {
steps {
script {
sshagent(['jenkins']) {
sh """
git config user.email "jenkins"
git config user.name "jenkins@dinauer-paf.de"
git tag -a build-${env.BUILD_NUMBER} -m "Jenkins Build ${env.BUILD_NUMBER}"
git push origin build-${env.BUILD_NUMBER}
"""
}
}
}
}
stage('Set Image Name') {
steps {
script {
env.IMAGE = "harbor.dinauer.dev/kubooboo/frontend:${env.BUILD_NUMBER}";
env.IMAGE = "harbor.dinauer.dev/registry/frontend:${env.BUILD_NUMBER}";
}
}
}

View File

@ -1,6 +1,15 @@
<template>
<div class="app">
<NuxtPage></NuxtPage>
<div class="page">
<div class="sidebar">
<div>
<NuxtLink class="link left-center" :class="{ active: useRoute().fullPath === '/' }" to="/"><UiIcon>home</UiIcon>Home</NuxtLink>
<NuxtLink class="link left-center" :class="{ active: useRoute().fullPath.startsWith('/artifacts') }" to="/artifacts"><UiIcon>box</UiIcon>Artifacts</NuxtLink>
</div>
<NuxtLink class="link left-center" :class="{ active: useRoute().fullPath.startsWith('/settings') }" to="/settings"><UiIcon>settings</UiIcon>Settings</NuxtLink>
</div>
<NuxtPage></NuxtPage>
</div>
<PopupTemplate v-if="popup" :heading="popup.config.heading" :size="popup.config.size">
<component :is="popup.component"></component>
</PopupTemplate>
@ -10,8 +19,13 @@
<script setup lang="ts">
import {usePopup} from "~/components/ui/popup/Popup";
import PopupTemplate from "~/components/ui/popup/PopupTemplate.vue";
import {useAccountStore} from "~/auth/useAccountStore";
const popup = computed(() => usePopup().get());
onMounted(() => {
useAccountStore().init();
})
</script>
<style>
@ -19,4 +33,27 @@ const popup = computed(() => usePopup().get());
height: 100vh;
width: 100vw;
}
.page {
display: grid;
grid-template-columns: auto 1fr;
width: 100%;
min-height: 100%;
}
.page > * {
padding: 1rem;
}
.sidebar {
background-color: var(--tile-color);
border-right: 1px solid #cddaff;
display: grid;
grid-template-rows: 1fr auto;
}
.link {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
.link.active, .link.active * {
background-color: #3c74ff;
color: white;
}
</style>

View File

@ -1,8 +1,10 @@
import axios from "axios";
import {AuthApi} from "~/utils/AuthApi";
import {Api} from "~/utils/Api";
export class Account
{
static baseURL: "http://localhost:8089/api/iam-backend/realms/key/accounts";
firstname?: string;
lastname?: string;
username?: string;
@ -11,24 +13,18 @@ export class Account
roles?: string[];
initial?: boolean;
static get(id: string, token: string, onSuccess: (account: Account) => void)
static get(onSuccess: (account: Account) => void)
{
AuthApi.get(StringUtils.format('/accounts/%s', id), {
headers: {
Authorization: StringUtils.format("Bearer %s", token)
}
})
.then((response) => {
onSuccess(response.data);
});
}
static create(user: AccountCreation, onSuccess: () => void)
{
AuthApi.post('/accounts', user)
.then(() => {
onSuccess();
});
AuthApi.create().post('/accounts', user)
.then(() =>
{
onSuccess();
});
}
}

View File

@ -1,32 +1,39 @@
import {type AxiosResponse} from "axios";
import {AuthApi} from "~/utils/AuthApi";
import {Api} from "~/utils/Api";
import type {Account} from "~/auth/Account";
export class Session
{
static baseURL = "http://localhost:8089";
static COOKIE = "session";
constructor (
constructor(
public user: Account,
public token: string
) {}
)
{
}
static create(sessionCreation: SessionCreation, onSuccess: (token: string) => void, onError: () => void)
{
AuthApi.post<string>("/sessions", sessionCreation)
.then((response: AxiosResponse) => {
onSuccess(response.data);
})
.catch(() => {
onError();
});
AuthApi.create().post<string>("/sessions/" + useRuntimeConfig().public.clientId, sessionCreation)
.then((response: AxiosResponse) =>
{
onSuccess(response.data);
})
.catch(() =>
{
onError();
});
}
}
export class SessionCreation
{
constructor (
constructor(
public email?: string,
public password?: string
) {}
)
{
}
}

View File

@ -1,17 +1,20 @@
import {MavenApi} from "~/utils/MavenApi";
export class Token
{
static baseURL: "http://localhost:8080";
constructor(
public name: string,
public createdAt: Date,
public expiresAt: Date
) {}
)
{
}
static get(onSuccess: (tokens: Token[]) => void)
{
MavenApi.get<Token[]>("/tokens")
.then((response) => {
MavenApi.create().get<Token[]>("/tokens")
.then((response) =>
{
onSuccess(response.data);
});
}
@ -22,12 +25,15 @@ export class TokenCreation
constructor(
public name: string,
public expiresAt: Date
) {}
)
{
}
static create(tokenCreation: TokenCreation, onSuccess: (token: TokenSecret) => void)
{
MavenApi.post<TokenSecret>("/tokens", tokenCreation)
.then((response) => {
MavenApi.create().post<TokenSecret>("/tokens", tokenCreation)
.then((response) =>
{
onSuccess(response.data);
});
}
@ -40,5 +46,7 @@ export class TokenSecret
public expiresAt: Date,
public createdAt: Date,
public token: string
) {}
)
{
}
}

View File

@ -0,0 +1,25 @@
import {Account} from "~/auth/Account";
export const useAccountStore = defineStore('account', {
state: () => ({
account: undefined as Account | undefined
}),
getters: {
require: (state) =>
{
return (): Account | undefined =>
{
return state.account;
}
}
},
actions: {
init()
{
Account.get((account: Account) =>
{
this.account = account;
})
}
}
})

View File

@ -1,29 +1,34 @@
import axios from "axios";
import {Api, MavenApi} from "~/utils/Api";
export class Artifact
{
public baseURL = "http://localhost:8080";
constructor (
constructor(
public id: string,
public groupId: string,
public artifactId: string,
public updatedAt: string,
public versions: Version[],
public totalPullCount: number
) {}
)
{
}
static get(onSuccess: (artifacts: Artifact[]) => void)
{
axios.get<Artifact[]>("http://localhost:8080/artifacts")
.then((response) => {
MavenApi.create().get<Artifact[]>("/artifacts")
.then((response) =>
{
onSuccess(response.data)
});
}
static getById(id: string, onSuccess: (artifact: Artifact) => void)
{
axios.get<Artifact>("http://localhost:8080/artifacts/" + id)
.then((response) => {
MavenApi.create().get<Artifact>("/artifacts/" + id)
.then((response) =>
{
onSuccess(response.data)
});
}
@ -31,28 +36,34 @@ export class Artifact
export class Version
{
constructor (
constructor(
public id: string,
public groupId: string,
public artifactId: string,
public version: string,
public jars?: Jar[],
public pom?: Pom
) {}
)
{
}
}
export class Jar
{
constructor (
constructor(
public filename: string,
public url: string
) {}
)
{
}
}
export class Pom
{
constructor (
constructor(
public filename: string,
public url: string
) {}
)
{
}
}

View File

@ -2,7 +2,7 @@
<ContentRow class="artifact pointer" @click="() => useRouter().push('artifacts/' + artifact.id)">
<ContentCell>{{ artifact.groupId }}</ContentCell>
<ContentCell>{{ artifact.artifactId }}</ContentCell>
<ContentCell v-for="latest in [artifact.versions.at(0)]"><span v-if="latest">{{ latest.version }}</span></ContentCell>
<ContentCell><div v-if="artifact.versions" v-for="latest in [artifact.versions.at(0)]"><span v-if="latest">{{ latest.version }}</span></div></ContentCell>
<ContentCell>{{ artifact.totalPullCount }}</ContentCell>
<ContentCell>{{ dayjs(artifact.updatedAt).format("DD.MM.YYYY HH:mm") + " Uhr" }}</ContentCell>
</ContentRow>

View File

@ -1,19 +1,23 @@
import axios from "axios";
import type {Account} from "~/auth/Account";
import {Session} from "~/auth/Session";
export class Event
{
constructor (
constructor(
public type: EventType,
public timestamp: Date,
public resource: Resource,
public account: Account
) {}
)
{
}
static get(onSuccess: (events: Event[]) => void)
{
axios.get<Event[]>("http://localhost:8080/events")
.then((response) => {
axios.get<Event[]>("http://localhost:8080/events", {withCredentials: true})
.then((response) =>
{
onSuccess(response.data)
});
}
@ -21,11 +25,13 @@ export class Event
export class Resource
{
constructor (
constructor(
public groupId: string,
public artifactId: string,
public version: string
) {}
)
{
}
}
export enum EventType

View File

@ -1,14 +0,0 @@
import {Session} from "~/auth/Session";
export default defineNuxtRouteMiddleware((to) => {
const authenticated = useCookie<Session>(Session.COOKIE).value != null;
const path = to.path;
if (StringUtils.startsWith(path, '/app') && !authenticated)
{
return navigateTo('/')
}
if (StringUtils.equals(path, '/') && authenticated)
{
return navigateTo('/app')
}
})

View File

@ -1,42 +0,0 @@
<template>
<div class="page">
<div class="sidebar">
<div>
<NuxtLink class="link left-center" :class="{ active: useRoute().fullPath === '/app' }" to="/app"><UiIcon>home</UiIcon>Home</NuxtLink>
<NuxtLink class="link left-center" :class="{ active: useRoute().fullPath.startsWith('/app/artifacts') }" to="/app/artifacts"><UiIcon>box</UiIcon>Artifacts</NuxtLink>
</div>
<NuxtLink class="link left-center" :class="{ active: useRoute().fullPath.startsWith('/app/settings') }" to="/app/settings"><UiIcon>settings</UiIcon>Settings</NuxtLink>
</div>
<NuxtPage></NuxtPage>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
.page {
display: grid;
grid-template-columns: auto 1fr;
width: 100%;
min-height: 100%;
}
.page > * {
padding: 1rem;
}
.sidebar {
background-color: var(--tile-color);
border-right: 1px solid #cddaff;
display: grid;
grid-template-rows: 1fr auto;
}
.link {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
.link.active, .link.active * {
background-color: #3c74ff;
color: white;
}
</style>

View File

@ -1,15 +0,0 @@
<template>
<div class="content-l">
<h1>Home</h1>
<h2>Recent Events</h2>
<EventList></EventList>
</div>
</template>
<script setup lang="ts">
import EventList from "~/components/events/EventList.vue";
</script>
<style scoped>
</style>

View File

@ -14,7 +14,7 @@
<div class="content-m">
<h2>Files</h2>
<p class="tile-m" v-if="selectedVersion.jars" v-for="jar in selectedVersion.jars" @click="Download.download(jar.url)">{{ jar.filename }}</p>
<a class="tile-m" v-if="selectedVersion.pom" :href="MavenApi.defaults.baseURL + '/maven2/' + selectedVersion.pom.url" target="_blank">{{ selectedVersion.pom.filename }}</a>
<a class="tile-m" v-if="selectedVersion.pom" :href="Api.defaults.baseURL + '/maven2/' + selectedVersion.pom.url" target="_blank">{{ selectedVersion.pom.filename }}</a>
</div>
</div>
<div class="content-m">
@ -29,7 +29,6 @@
import {Artifact, Version} from "~/components/artifact/Artifact";
import Codebox from "~/components/artifact/Codebox.vue";
import {Download} from "~/utils/Download";
import {MavenApi} from "~/utils/MavenApi";
const artifact: Ref<Artifact | undefined> = ref(undefined);
const selectedVersion: Ref<Version | undefined> = ref(undefined);

View File

@ -1,10 +1,13 @@
<template>
<LoginComponent></LoginComponent>
<div class="content-l">
<h1>Home</h1>
<h2>Recent Events</h2>
<EventList></EventList>
</div>
</template>
<script setup lang="ts">
import LoginComponent from "~/components/auth/LoginComponent.vue";
import EventList from "~/components/events/EventList.vue";
</script>
<style scoped>

View File

@ -4,7 +4,7 @@
<div class="col-3">
<div class="tile-m">
<h3>Username</h3>
<DisplayNameComponent :account="session.user"></DisplayNameComponent>
</div>
<div class="tile-m">
<h3>E-Mail</h3>

50
app/utils/Api.ts Normal file
View File

@ -0,0 +1,50 @@
import axios, {type AxiosInstance} from "axios";
export abstract class Api
{
public baseURL: string;
protected constructor(baseURL: string)
{
this.baseURL = baseURL;
}
build()
{
const instance: AxiosInstance = axios.create({
baseURL: this.baseURL
});
instance.interceptors.request.use((config) =>
{
config.withCredentials = true;
return config;
})
return instance;
}
}
export class AuthApi extends Api
{
constructor()
{
super("http://localhost:8089/api/iam-backend");
}
static create()
{
return new AuthApi().build();
}
}
export class MavenApi extends Api
{
constructor()
{
super("http://localhost:8080");
}
static create()
{
return new MavenApi().build();
}
}

View File

@ -1,19 +0,0 @@
import axios, {type AxiosInstance} from "axios";
import {Session} from "~/auth/Session";
export const AuthApi: AxiosInstance = function()
{
const instance: AxiosInstance = axios.create({
baseURL: 'http://localhost:8089/api/iam-backend'
});
instance.interceptors.request.use((config) =>
{
const session: Session = useCookie<Session>(Session.COOKIE).value
if (session)
{
config.headers["Authorization"] = StringUtils.format("Bearer %s", session.token)
}
return config;
})
return instance;
}();

View File

@ -1,19 +0,0 @@
import axios, {type AxiosInstance} from "axios";
import {Session} from "~/auth/Session";
export const MavenApi: AxiosInstance = function()
{
const instance: AxiosInstance = axios.create({
baseURL: 'http://localhost:8080'
});
instance.interceptors.request.use((config) =>
{
const session: Session = useCookie<Session>(Session.COOKIE).value
if (session)
{
config.headers["Authorization"] = StringUtils.format("Bearer %s", session.token)
}
return config;
})
return instance;
}();

View File

@ -1,20 +1,27 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
compatibilityDate: '2025-07-15',
devtools: {enabled: true},
css: [
'@/assets/style.css',
'@/assets/base-style.css'
],
app: {
head: {
link: [
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200' }
]
}
},
modules: ['@pinia/nuxt']
css: [
'@/assets/style.css',
'@/assets/base-style.css'
],
app: {
head: {
link: [
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200'
}
]
}
},
runtimeConfig: {
public: {
clientId: process.env.NUXT_PUBLIC_CLIENT_ID,
idpUrl: process.env.NUXT_PUBLIC_IDP_URL
}
},
modules: ['@pinia/nuxt']
})

11
server/middleware/auth.ts Normal file
View File

@ -0,0 +1,11 @@
import {Session} from "~/auth/Session";
import {useRuntimeConfig} from "#imports";
export default defineEventHandler(async (event) =>
{
const cookie = getCookie(event, Session.COOKIE);
if (cookie == null)
{
await sendRedirect(event, useRuntimeConfig().public.idpUrl, 302);
}
});