🎉 Init
This commit is contained in:
commit
c8cfb6eeab
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
39
Jenkinsfile
vendored
Normal file
39
Jenkinsfile
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
stages {
|
||||||
|
|
||||||
|
stage('Set Image Name') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
env.IMAGE = "harbor.dinauer.dev/kubooboo/frontend:${env.BUILD_NUMBER}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Build Image') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
sh "docker build --no-cache -t ${env.IMAGE} ."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Push Image to Docker Hub') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
withCredentials([usernamePassword(credentialsId: 'harbor', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
|
||||||
|
sh 'echo ${PASSWORD} | docker login harbor.dinauer.dev -u ${USERNAME} --password-stdin'
|
||||||
|
sh "docker push ${env.IMAGE}"
|
||||||
|
sh "docker logout harbor.dinauer.dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Remove image from host') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
sh "docker image rm --force ${env.IMAGE}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
README.md
Normal file
75
README.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Nuxt Minimal Starter
|
||||||
|
|
||||||
|
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on `http://localhost:3000`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm preview
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn preview
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||||
22
app/app.vue
Normal file
22
app/app.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app">
|
||||||
|
<NuxtPage></NuxtPage>
|
||||||
|
<PopupTemplate v-if="popup" :heading="popup.config.heading" :size="popup.config.size">
|
||||||
|
<component :is="popup.component"></component>
|
||||||
|
</PopupTemplate>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {usePopup} from "~/components/ui/popup/Popup";
|
||||||
|
import PopupTemplate from "~/components/ui/popup/PopupTemplate.vue";
|
||||||
|
|
||||||
|
const popup = computed(() => usePopup().get());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
256
app/assets/base-style.css
Normal file
256
app/assets/base-style.css
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stretch-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: stretch;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-bottom {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spaced-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spaced-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content, *[class^='content-'], *[class*=' content-'] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content, *[class^='content-'], *[class*=' content-'] > *:is(div, form, header, footer) {
|
||||||
|
width: 100%;
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
.content .full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.content-s {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.content-m {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.content-l {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.content-xl {
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-2xl {
|
||||||
|
gap: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-3xl {
|
||||||
|
gap: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-m {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
.padding-l {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.padding-xl {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow {
|
||||||
|
width: min(100%, 1340px);
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
.narrow.s {
|
||||||
|
width: min(100%, 320px);
|
||||||
|
}
|
||||||
|
.narrow.m {
|
||||||
|
width: min(100%, 540px);
|
||||||
|
}
|
||||||
|
.narrow-b {
|
||||||
|
width: min(100%, 740px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile, *[class^='tile-'], *[class*=' tile-'] {
|
||||||
|
background-color: var(--tile-color);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #cddaff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-s {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-m {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-l {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-xl {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-3 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-4 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-5 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grayed-out {
|
||||||
|
color: #777777;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row_auto-1fr {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.height_100 {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-0 {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-m {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-l {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.col-1-m {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.col-2-m {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.col-3-m {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.center-m {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.full_20rem {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 20rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-shape {
|
||||||
|
height: 2.25rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.width-6rem {
|
||||||
|
width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nowrap {
|
||||||
|
white-space: nowrap
|
||||||
|
}
|
||||||
BIN
app/assets/logo.png
Normal file
BIN
app/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
27
app/assets/style.css
Normal file
27
app/assets/style.css
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
|
||||||
|
|
||||||
|
* {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Outfit", sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #__nuxt {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html {
|
||||||
|
--tile-color: #eef6ff;
|
||||||
|
--primary-color: #3c74ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: 550;
|
||||||
|
}
|
||||||
43
app/auth/Account.ts
Normal file
43
app/auth/Account.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import {AuthApi} from "~/utils/AuthApi";
|
||||||
|
|
||||||
|
export class Account
|
||||||
|
{
|
||||||
|
firstname?: string;
|
||||||
|
lastname?: string;
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
password?: string;
|
||||||
|
roles?: string[];
|
||||||
|
initial?: boolean;
|
||||||
|
|
||||||
|
static get(id: string, token: string, 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AccountCreation
|
||||||
|
{
|
||||||
|
firstname?: string;
|
||||||
|
lastname?: string;
|
||||||
|
email?: string;
|
||||||
|
password?: string;
|
||||||
|
username?: string;
|
||||||
|
role: string = "USER";
|
||||||
|
}
|
||||||
32
app/auth/Session.ts
Normal file
32
app/auth/Session.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {type AxiosResponse} from "axios";
|
||||||
|
import {AuthApi} from "~/utils/AuthApi";
|
||||||
|
import type {Account} from "~/auth/Account";
|
||||||
|
|
||||||
|
export class Session
|
||||||
|
{
|
||||||
|
static COOKIE = "session";
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SessionCreation
|
||||||
|
{
|
||||||
|
constructor (
|
||||||
|
public email?: string,
|
||||||
|
public password?: string
|
||||||
|
) {}
|
||||||
|
}
|
||||||
44
app/auth/Token.ts
Normal file
44
app/auth/Token.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import {MavenApi} from "~/utils/MavenApi";
|
||||||
|
|
||||||
|
export class Token
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public createdAt: Date,
|
||||||
|
public expiresAt: Date
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static get(onSuccess: (tokens: Token[]) => void)
|
||||||
|
{
|
||||||
|
MavenApi.get<Token[]>("/tokens")
|
||||||
|
.then((response) => {
|
||||||
|
onSuccess(response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
onSuccess(response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TokenSecret
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public expiresAt: Date,
|
||||||
|
public createdAt: Date,
|
||||||
|
public token: string
|
||||||
|
) {}
|
||||||
|
}
|
||||||
58
app/components/artifact/Artifact.ts
Normal file
58
app/components/artifact/Artifact.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export class Artifact
|
||||||
|
{
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
onSuccess(response.data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static getById(id: string, onSuccess: (artifact: Artifact) => void)
|
||||||
|
{
|
||||||
|
axios.get<Artifact>("http://localhost:8080/artifacts/" + id)
|
||||||
|
.then((response) => {
|
||||||
|
onSuccess(response.data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Version
|
||||||
|
{
|
||||||
|
constructor (
|
||||||
|
public id: string,
|
||||||
|
public groupId: string,
|
||||||
|
public artifactId: string,
|
||||||
|
public version: string,
|
||||||
|
public jars?: Jar[],
|
||||||
|
public pom?: Pom
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Jar
|
||||||
|
{
|
||||||
|
constructor (
|
||||||
|
public filename: string,
|
||||||
|
public url: string
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Pom
|
||||||
|
{
|
||||||
|
constructor (
|
||||||
|
public filename: string,
|
||||||
|
public url: string
|
||||||
|
) {}
|
||||||
|
}
|
||||||
26
app/components/artifact/ArtifactComponent.vue
Normal file
26
app/components/artifact/ArtifactComponent.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<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>{{ artifact.totalPullCount }}</ContentCell>
|
||||||
|
<ContentCell>{{ dayjs(artifact.updatedAt).format("DD.MM.YYYY HH:mm") + " Uhr" }}</ContentCell>
|
||||||
|
</ContentRow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {Artifact} from "~/components/artifact/Artifact";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import ContentRow from "~/components/ui/table/ContentRow.vue";
|
||||||
|
import ContentCell from "~/components/ui/table/ContentCell.vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
artifact: Artifact
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.artifact {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
app/components/artifact/ArtifactList.vue
Normal file
32
app/components/artifact/ArtifactList.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<Table v-if="artifacts" columns="auto auto auto 1fr auto">
|
||||||
|
<HeaderRow>
|
||||||
|
<HeaderCell>Group ID</HeaderCell>
|
||||||
|
<HeaderCell>Artifact ID</HeaderCell>
|
||||||
|
<HeaderCell>Latest Version</HeaderCell>
|
||||||
|
<HeaderCell>Pull Count</HeaderCell>
|
||||||
|
<HeaderCell>Updated At</HeaderCell>
|
||||||
|
</HeaderRow>
|
||||||
|
<ArtifactComponent :artifact="artifact" v-for="artifact in artifacts"></ArtifactComponent>
|
||||||
|
</Table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {Artifact} from "~/components/artifact/Artifact";
|
||||||
|
import ArtifactComponent from "~/components/artifact/ArtifactComponent.vue";
|
||||||
|
import Table from "~/components/ui/table/Table.vue";
|
||||||
|
import HeaderCell from "~/components/ui/table/HeaderCell.vue";
|
||||||
|
import HeaderRow from "~/components/ui/table/HeaderRow.vue";
|
||||||
|
|
||||||
|
const artifacts: Ref<Artifact[] | undefined> = ref(undefined);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
Artifact.get((_artifacts: Artifact[]) => {
|
||||||
|
artifacts.value = _artifacts;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
20
app/components/artifact/Codebox.vue
Normal file
20
app/components/artifact/Codebox.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="codebox">
|
||||||
|
<p v-html="content"></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
content: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.codebox * {
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
white-space: preserve;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
18
app/components/auth/DisplayNameComponent.vue
Normal file
18
app/components/auth/DisplayNameComponent.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p v-if="account && account.firstname && account.firstname">{{ account.firstname }}, {{ account.lastname }}</p>
|
||||||
|
<p v-else>-</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {Account} from "~/auth/Account";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
account?: Account
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
43
app/components/auth/LoginComponent.vue
Normal file
43
app/components/auth/LoginComponent.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<h1>Login</h1>
|
||||||
|
<UiInput label="E-Mail">
|
||||||
|
<input type="text" v-model="sessionCreation.email">
|
||||||
|
</UiInput>
|
||||||
|
<UiInput label="Passwort">
|
||||||
|
<input type="password" v-model="sessionCreation.password">
|
||||||
|
</UiInput>
|
||||||
|
<UiButton @click="login">Login</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {Session, SessionCreation} from "~/auth/Session";
|
||||||
|
import {jwtDecode} from "jwt-decode";
|
||||||
|
import {Account} from "~/auth/Account";
|
||||||
|
|
||||||
|
const sessionCreation = ref(function() {
|
||||||
|
if (StringUtils.equals("development", process.env.NODE_ENV))
|
||||||
|
{
|
||||||
|
return new SessionCreation("andreas.j.dinauer@gmail.com", "pw")
|
||||||
|
}
|
||||||
|
return new SessionCreation();
|
||||||
|
}());
|
||||||
|
|
||||||
|
function login()
|
||||||
|
{
|
||||||
|
Session.create(sessionCreation.value, (token: string) => {
|
||||||
|
const decode = jwtDecode(token) as any;
|
||||||
|
Account.get(decode.upn, token, (user: Account) => {
|
||||||
|
useCookie<Session>(Session.COOKIE).value = new Session(user, token);
|
||||||
|
useRouter().push('/app/artifacts');
|
||||||
|
});
|
||||||
|
}, () => {})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-page {
|
||||||
|
max-width: 540px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
app/components/events/Event.ts
Normal file
35
app/components/events/Event.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import type {Account} from "~/auth/Account";
|
||||||
|
|
||||||
|
export class Event
|
||||||
|
{
|
||||||
|
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) => {
|
||||||
|
onSuccess(response.data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Resource
|
||||||
|
{
|
||||||
|
constructor (
|
||||||
|
public groupId: string,
|
||||||
|
public artifactId: string,
|
||||||
|
public version: string
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EventType
|
||||||
|
{
|
||||||
|
UPLOAD = "UPLOAD",
|
||||||
|
DELETE = "DELETE"
|
||||||
|
}
|
||||||
30
app/components/events/EventComponent.vue
Normal file
30
app/components/events/EventComponent.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<ContentRow>
|
||||||
|
<ContentCell>
|
||||||
|
<UiIcon v-if="event.type === EventType.UPLOAD">upload</UiIcon>
|
||||||
|
</ContentCell>
|
||||||
|
<ContentCell> {{ event.resource.groupId }}</ContentCell>
|
||||||
|
<ContentCell>{{ event.resource.artifactId }}</ContentCell>
|
||||||
|
<ContentCell>{{ event.resource.version }}</ContentCell>
|
||||||
|
<ContentCell>
|
||||||
|
<DisplayNameComponent :account="event.account"></DisplayNameComponent>
|
||||||
|
</ContentCell>
|
||||||
|
<ContentCell>{{ Age.calc(event.timestamp) }}</ContentCell>
|
||||||
|
</ContentRow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ContentRow from "~/components/ui/table/ContentRow.vue";
|
||||||
|
import ContentCell from "~/components/ui/table/ContentCell.vue";
|
||||||
|
import {Event, EventType} from "~/components/events/Event";
|
||||||
|
import {Age} from "~/utils/Age";
|
||||||
|
import DisplayNameComponent from "~/components/auth/DisplayNameComponent.vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
event: Event
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
33
app/components/events/EventList.vue
Normal file
33
app/components/events/EventList.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<Table columns="auto auto auto auto 1fr auto" v-if="events">
|
||||||
|
<HeaderRow>
|
||||||
|
<HeaderCell></HeaderCell>
|
||||||
|
<HeaderCell>Group ID</HeaderCell>
|
||||||
|
<HeaderCell>Artifact ID</HeaderCell>
|
||||||
|
<HeaderCell>Version</HeaderCell>
|
||||||
|
<HeaderCell>User</HeaderCell>
|
||||||
|
<HeaderCell></HeaderCell>
|
||||||
|
</HeaderRow>
|
||||||
|
<EventComponent :event="event" v-for="event in events"></EventComponent>
|
||||||
|
</Table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Table from "~/components/ui/table/Table.vue";
|
||||||
|
import HeaderRow from "~/components/ui/table/HeaderRow.vue";
|
||||||
|
import EventComponent from "~/components/events/EventComponent.vue";
|
||||||
|
import HeaderCell from "~/components/ui/table/HeaderCell.vue";
|
||||||
|
import {Event} from "~/components/events/Event";
|
||||||
|
|
||||||
|
const events: Ref<Event[] | undefined> = ref(undefined);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
Event.get((_events: Event[]) => {
|
||||||
|
events.value = _events;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
16
app/components/group/Group.ts
Normal file
16
app/components/group/Group.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export class Group
|
||||||
|
{
|
||||||
|
constructor (
|
||||||
|
public groupId: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static get(onSuccess: (groups: Group[]) => void)
|
||||||
|
{
|
||||||
|
axios.get<Group[]>("http://localhost:8080/groups")
|
||||||
|
.then((response) => {
|
||||||
|
onSuccess(response.data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/components/group/GroupComponent.vue
Normal file
11
app/components/group/GroupComponent.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
49
app/components/settings/token/TokenCreationComponent.vue
Normal file
49
app/components/settings/token/TokenCreationComponent.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="!token" class="content-l">
|
||||||
|
<div class="col-2">
|
||||||
|
<UiInput label="Name" required>
|
||||||
|
<input type="text" v-model="name">
|
||||||
|
</UiInput>
|
||||||
|
<UiInput label="Expires At">
|
||||||
|
<input type="date" v-model="expiresAt">
|
||||||
|
</UiInput>
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
<UiButton @click="create" :disabled="name.length === 0">Create</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="content-l">
|
||||||
|
<p class="tile-m">{{ token.token }}</p>
|
||||||
|
<div class="center">
|
||||||
|
<UiButton class="width-6rem">Copy</UiButton>
|
||||||
|
<UiButton class="width-6rem" @click="usePopup().close()">Close</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {TokenCreation, TokenSecret} from "~/auth/Token";
|
||||||
|
import {usePopup} from "~/components/ui/popup/Popup";
|
||||||
|
|
||||||
|
const name: Ref<string> = ref('');
|
||||||
|
const expiresAt = ref(new Date().toISOString().substring(0, 10));
|
||||||
|
|
||||||
|
const token: Ref<TokenSecret | undefined> = ref(undefined);
|
||||||
|
function create()
|
||||||
|
{
|
||||||
|
if (!name.value)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TokenCreation.create(new TokenCreation(name.value, new Date(expiresAt.value)), (_token: TokenSecret) => {
|
||||||
|
token.value = _token;
|
||||||
|
usePopup().get()!.config.callback?.(_token);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
61
app/components/ui/UiButton.vue
Normal file
61
app/components/ui/UiButton.vue
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<button @click="() => click()" class="base-shape button pointer center" :class="{ loading: loading, reverse: reverse, disabled: disabled }"><UiIcon v-if="icon && !loading">{{ icon }}</UiIcon><slot></slot></button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import UiIcon from '@/components/ui/UiIcon.vue';
|
||||||
|
|
||||||
|
function click() {
|
||||||
|
if(!props.disabled) {
|
||||||
|
if(props.onclick != null) {
|
||||||
|
props.onclick()
|
||||||
|
}
|
||||||
|
if(props.to != null) {
|
||||||
|
useRouter().push(props.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
icon?: string,
|
||||||
|
reverse?: boolean,
|
||||||
|
loading?: boolean | undefined,
|
||||||
|
disabled?: boolean,
|
||||||
|
onclick?: () => void
|
||||||
|
to?: string
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.button {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
outline: none;
|
||||||
|
gap: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.button * {
|
||||||
|
color: white;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
.button.reverse {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
.button.loading {
|
||||||
|
background-color: #5c5c5c;
|
||||||
|
border-color: #5c5c5c;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.button.disabled {
|
||||||
|
background-color: #7a7a7a;
|
||||||
|
border-color: #7a7a7a;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.reverse {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
app/components/ui/UiIcon.vue
Normal file
14
app/components/ui/UiIcon.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<span class="material-symbols-outlined"><slot></slot></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings:
|
||||||
|
'FILL' 0,
|
||||||
|
'wght' 400,
|
||||||
|
'GRAD' 0,
|
||||||
|
'opsz' 24;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
52
app/components/ui/UiInput.vue
Normal file
52
app/components/ui/UiInput.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div class="field">
|
||||||
|
<div class="left-center">
|
||||||
|
<label v-if="label">{{ label }} <span class="required" v-if="required">*</span></label>
|
||||||
|
<slot name="label"></slot>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
label?: string,
|
||||||
|
required?: boolean
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.field input, .field .input, .field textarea, .field select {
|
||||||
|
min-height: 2.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background-color: rgb(255, 255, 255);
|
||||||
|
width: 100%;
|
||||||
|
border: 2px solid #444444;
|
||||||
|
outline: none;
|
||||||
|
padding: 0.25rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.field textarea {
|
||||||
|
min-height: 5rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
.field input:focus, .field textarea:focus, .field select:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.required {
|
||||||
|
color: #e63515;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.field input:disabled, textarea:disabled, select:disabled {
|
||||||
|
border: 2px solid #7a7a7a;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
app/components/ui/popup/Popup.ts
Normal file
71
app/components/ui/popup/Popup.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
export class Popup
|
||||||
|
{
|
||||||
|
constructor (
|
||||||
|
public component: Component,
|
||||||
|
public config: PopupConfig,
|
||||||
|
public payload?: any
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PopupConfig
|
||||||
|
{
|
||||||
|
heading: string;
|
||||||
|
size: PopupSize;
|
||||||
|
callback?: (payload?: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PopupSize
|
||||||
|
{
|
||||||
|
SMALL = "small", MEDIUM = "medium", LARGE = "large", FULL = "full"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePopup = defineStore('popup', {
|
||||||
|
state: () => ({
|
||||||
|
popup: shallowRef<Popup | undefined>(undefined)
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
get: (state) => {
|
||||||
|
return () => {
|
||||||
|
return state.popup;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
require: (state) => {
|
||||||
|
return () => {
|
||||||
|
if (state.popup)
|
||||||
|
{
|
||||||
|
return state.popup;
|
||||||
|
}
|
||||||
|
throw new Error('Expected to be in open popup state.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
open(popup: Popup) {
|
||||||
|
this.popup = popup;
|
||||||
|
disableScrolling();
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.popup = undefined;
|
||||||
|
enableScrolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function disableScrolling()
|
||||||
|
{
|
||||||
|
const body = document.getElementsByTagName('body');
|
||||||
|
for(const element of body)
|
||||||
|
{
|
||||||
|
element.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableScrolling()
|
||||||
|
{
|
||||||
|
const body = document.getElementsByTagName('body');
|
||||||
|
for(const element of body)
|
||||||
|
{
|
||||||
|
element.style.overflow = "visible";
|
||||||
|
}
|
||||||
|
}
|
||||||
80
app/components/ui/popup/PopupTemplate.vue
Normal file
80
app/components/ui/popup/PopupTemplate.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<div class="overlay center" @click="usePopup().close()">
|
||||||
|
<div class="popup" :class="size" @click.stop>
|
||||||
|
<div class="popup__header">
|
||||||
|
<h2>{{ heading }}</h2>
|
||||||
|
<UiButton icon="close" @click="usePopup().close()" class="square"></UiButton>
|
||||||
|
</div>
|
||||||
|
<div class="popup__body">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {PopupSize, usePopup} from "~/components/ui/popup/Popup";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
heading: string,
|
||||||
|
size: PopupSize
|
||||||
|
}>()
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if(event.key === 'Escape')
|
||||||
|
{
|
||||||
|
usePopup().close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.overlay {
|
||||||
|
background-color: rgba(54, 54, 54, 0.514);
|
||||||
|
backdrop-filter: blur(0.1rem);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
align-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: rgb(255, 255, 255);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.popup__body {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.medium {
|
||||||
|
height: auto;
|
||||||
|
width: 740px;
|
||||||
|
}
|
||||||
|
.small {
|
||||||
|
height: auto;
|
||||||
|
width: 540px;
|
||||||
|
}
|
||||||
|
.large {
|
||||||
|
height: auto;
|
||||||
|
width: 740px;
|
||||||
|
}
|
||||||
|
.full {
|
||||||
|
height: auto;
|
||||||
|
width: 540px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
app/components/ui/table/ContentCell.vue
Normal file
15
app/components/ui/table/ContentCell.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cell left-center">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cell {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
app/components/ui/table/ContentRow.vue
Normal file
24
app/components/ui/table/ContentRow.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-row display-contents">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.display-contents {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.content-row > * {
|
||||||
|
border-bottom: 1px solid #c1c1c1;
|
||||||
|
}
|
||||||
|
.content-row:hover > * {
|
||||||
|
background-color: var(--tile-color);
|
||||||
|
}
|
||||||
|
.content-row:last-of-type * {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
app/components/ui/table/HeaderCell.vue
Normal file
20
app/components/ui/table/HeaderCell.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="header">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid #c1c1c1;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background-color: #efefef;
|
||||||
|
}
|
||||||
|
.header, .header * {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
app/components/ui/table/HeaderRow.vue
Normal file
15
app/components/ui/table/HeaderRow.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="display-contents">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.display-contents {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
app/components/ui/table/Table.vue
Normal file
30
app/components/ui/table/Table.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
columns: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: v-bind(columns);
|
||||||
|
border: 1px solid #c1c1c1;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header * {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.artifact-list .artifact:hover * {
|
||||||
|
background-color: var(--tile-color);
|
||||||
|
}
|
||||||
|
.artifact-list .artifact:last-of-type * {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
app/middleware/auth.global.ts
Normal file
14
app/middleware/auth.global.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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')
|
||||||
|
}
|
||||||
|
})
|
||||||
42
app/pages/app.vue
Normal file
42
app/pages/app.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<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>
|
||||||
75
app/pages/app/artifacts/[id]/index.vue
Normal file
75
app/pages/app/artifacts/[id]/index.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="artifact" class="content-l">
|
||||||
|
<h1>Artifact</h1>
|
||||||
|
<div class="col-2">
|
||||||
|
<p class="tile-m">{{ artifact.groupId }}</p>
|
||||||
|
<p class="tile-m">{{ artifact.artifactId }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="artifact-page">
|
||||||
|
<div class="content-m" v-if="selectedVersion">
|
||||||
|
<h2>Dependency</h2>
|
||||||
|
<div class="tile-m" v-if="dependency">
|
||||||
|
<Codebox :content="dependency"></Codebox>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content-m">
|
||||||
|
<h2>Versions</h2>
|
||||||
|
<p class="tile-m pointer" v-for="version in artifact.versions" :class="{ active: (selectedVersion != null && version.id === selectedVersion.id) }" @click="() => { selectedVersion = version }">{{ version.version }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
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);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const id = useRoute().params.id as string;
|
||||||
|
Artifact.getById(id, (_artifact: Artifact) => {
|
||||||
|
artifact.value = _artifact;
|
||||||
|
const _version = _artifact.versions.at(0);
|
||||||
|
if (_version != null)
|
||||||
|
{
|
||||||
|
selectedVersion.value = _version;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const dependency = computed(() => {
|
||||||
|
if (selectedVersion.value)
|
||||||
|
{
|
||||||
|
const template = [
|
||||||
|
"<dependency>",
|
||||||
|
"\t<groupId>" + selectedVersion.value.groupId + "</groupId>",
|
||||||
|
"\t<artifactId>" + selectedVersion.value.artifactId + "</artifactId>",
|
||||||
|
"\t<version>" + selectedVersion.value.version + "</version>",
|
||||||
|
"</dependency>"
|
||||||
|
]
|
||||||
|
return template.join("\n").replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.artifact-page {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 10rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
background-color: #3c74ff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
app/pages/app/artifacts/index.vue
Normal file
14
app/pages/app/artifacts/index.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-l">
|
||||||
|
<h1>Artifacts</h1>
|
||||||
|
<ArtifactList></ArtifactList>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ArtifactList from "~/components/artifact/ArtifactList.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
15
app/pages/app/index.vue
Normal file
15
app/pages/app/index.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<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>
|
||||||
66
app/pages/app/settings/index.vue
Normal file
66
app/pages/app/settings/index.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-l">
|
||||||
|
<h1>Account Settings</h1>
|
||||||
|
<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>
|
||||||
|
<p>andreas.j.dinauer@gmail.com</p>
|
||||||
|
</div>
|
||||||
|
<div class="tile-m">
|
||||||
|
<h3>Password</h3>
|
||||||
|
<p>************</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1>Access Tokens</h1>
|
||||||
|
<div v-if="tokens">
|
||||||
|
<p v-if="tokens.length === 0" class="tile-m">No tokens created yet.</p>
|
||||||
|
<UiTable v-else columns="auto 1fr auto auto">
|
||||||
|
<HeaderRow>
|
||||||
|
<HeaderCell>Name</HeaderCell>
|
||||||
|
<HeaderCell>Created At</HeaderCell>
|
||||||
|
<HeaderCell>Expires At</HeaderCell>
|
||||||
|
<HeaderCell></HeaderCell>
|
||||||
|
</HeaderRow>
|
||||||
|
<ContentRow v-for="token in tokens">
|
||||||
|
<ContentCell>{{ token.name }}</ContentCell>
|
||||||
|
<ContentCell>{{ dayjs(token.createdAt).format("DD.MM.YYYY") }}</ContentCell>
|
||||||
|
<ContentCell>{{ dayjs(token.expiresAt).format("DD.MM.YYYY") }}</ContentCell>
|
||||||
|
<ContentCell style="padding: 0.25rem"><UiButton icon="delete"></UiButton></ContentCell>
|
||||||
|
</ContentRow>
|
||||||
|
</UiTable>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UiButton @click="usePopup().open(new Popup(TokenCreationComponent, { size: PopupSize.MEDIUM, heading: 'Create Token', callback: (token: TokenSecret) => { tokens?.push(new Token(token.name, token.createdAt, token.expiresAt)) } }))">Create</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DisplayNameComponent from "~/components/auth/DisplayNameComponent.vue";
|
||||||
|
import {Session} from "~/auth/Session";
|
||||||
|
import TokenCreationComponent from "~/components/settings/token/TokenCreationComponent.vue";
|
||||||
|
import {Popup, PopupSize, usePopup} from "~/components/ui/popup/Popup";
|
||||||
|
import {Token, type TokenSecret} from "~/auth/Token";
|
||||||
|
import HeaderCell from "~/components/ui/table/HeaderCell.vue";
|
||||||
|
import HeaderRow from "~/components/ui/table/HeaderRow.vue";
|
||||||
|
import ContentRow from "~/components/ui/table/ContentRow.vue";
|
||||||
|
import ContentCell from "~/components/ui/table/ContentCell.vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const session = useCookie<Session>(Session.COOKIE);
|
||||||
|
|
||||||
|
const tokens: Ref<Token[] | undefined> = ref(undefined);
|
||||||
|
onMounted(() => {
|
||||||
|
Token.get((_tokens: Token[]) => {
|
||||||
|
tokens.value = _tokens;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
12
app/pages/index.vue
Normal file
12
app/pages/index.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<LoginComponent></LoginComponent>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import LoginComponent from "~/components/auth/LoginComponent.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
17
app/server/download.ts
Normal file
17
app/server/download.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { defineEventHandler } from 'h3'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
if (event.context.params)
|
||||||
|
{
|
||||||
|
const filename = event.context.params.path;
|
||||||
|
const url = `http://localhost:8080/${filename}`
|
||||||
|
|
||||||
|
const res = await fetch(url)
|
||||||
|
const data = await res.arrayBuffer()
|
||||||
|
|
||||||
|
event.res.setHeader('Content-Type', res.headers.get('content-type') || 'application/octet-stream')
|
||||||
|
event.res.setHeader('Content-Disposition', `attachment; filename="${filename}"`)
|
||||||
|
|
||||||
|
return Buffer.from(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
83
app/utils/Age.ts
Normal file
83
app/utils/Age.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
export class Age
|
||||||
|
{
|
||||||
|
static calc(date: Date)
|
||||||
|
{
|
||||||
|
const today = new Date().getTime() / 1000;
|
||||||
|
const createdAt = new Date(date).getTime() / 1000;
|
||||||
|
const dif = today - createdAt;
|
||||||
|
if (dif < 60)
|
||||||
|
{
|
||||||
|
return Age.format(dif, ChronoUnit.SECOND) + " ago";
|
||||||
|
}
|
||||||
|
if (dif < 60 * 60)
|
||||||
|
{
|
||||||
|
const minutes = Math.floor((dif / 60));
|
||||||
|
return Age.format(minutes, ChronoUnit.MINUTE) + " ago";
|
||||||
|
}
|
||||||
|
if (dif < 60 * 60 * 24)
|
||||||
|
{
|
||||||
|
const hours = Math.floor(dif / (60 * 60));
|
||||||
|
return Age.format(hours, ChronoUnit.HOUR) + " ago";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const days = Math.floor(dif / (60 * 60 * 24));
|
||||||
|
return Age.format(days, ChronoUnit.DAY) + " ago";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static format(count: number, unit: ChronoUnit): string
|
||||||
|
{
|
||||||
|
if (unit === ChronoUnit.SECOND)
|
||||||
|
{
|
||||||
|
if (count != 1)
|
||||||
|
{
|
||||||
|
return count.toFixed(0) + " seconds";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return count.toFixed(0) + " second";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unit === ChronoUnit.MINUTE)
|
||||||
|
{
|
||||||
|
if (count != 1)
|
||||||
|
{
|
||||||
|
return count.toFixed(0) + " minutes";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return count.toFixed(0) + " minute";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unit === ChronoUnit.HOUR)
|
||||||
|
{
|
||||||
|
if (count != 1)
|
||||||
|
{
|
||||||
|
return count.toFixed(0) + " hours";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return count.toFixed(0) + " hour";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unit === ChronoUnit.DAY)
|
||||||
|
{
|
||||||
|
if (count != 1)
|
||||||
|
{
|
||||||
|
return count.toFixed(0) + " days";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return count.toFixed(0) + " day";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum ChronoUnit
|
||||||
|
{
|
||||||
|
SECOND, MINUTE, HOUR, DAY
|
||||||
|
}
|
||||||
19
app/utils/AuthApi.ts
Normal file
19
app/utils/AuthApi.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
}();
|
||||||
12
app/utils/Download.ts
Normal file
12
app/utils/Download.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export class Download
|
||||||
|
{
|
||||||
|
static download(url: string)
|
||||||
|
{
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.style.display = "none";
|
||||||
|
a.href = "http://localhost:8080/maven2/" + url;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/utils/MavenApi.ts
Normal file
19
app/utils/MavenApi.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
}();
|
||||||
36
app/utils/StringUtils.ts
Normal file
36
app/utils/StringUtils.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export class StringUtils
|
||||||
|
{
|
||||||
|
static format(template: string, ...varContext: string[]): string
|
||||||
|
{
|
||||||
|
const context = Array.from(varContext);
|
||||||
|
let contextIndex = 0;
|
||||||
|
while (template.includes("%s"))
|
||||||
|
{
|
||||||
|
const currentContext = context.at(contextIndex);
|
||||||
|
if (currentContext != null)
|
||||||
|
{
|
||||||
|
template = template.replace("%s", currentContext);
|
||||||
|
contextIndex++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
static equals(first: string | null | undefined, second: string | null | undefined): boolean
|
||||||
|
{
|
||||||
|
return first === second;
|
||||||
|
}
|
||||||
|
|
||||||
|
static startsWith(input: string | null | undefined, start: string | null | undefined)
|
||||||
|
{
|
||||||
|
if (input != null && start != null)
|
||||||
|
{
|
||||||
|
return input.startsWith(start);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
nuxt.config.ts
Normal file
20
nuxt.config.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
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']
|
||||||
|
})
|
||||||
10162
package-lock.json
generated
Normal file
10162
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@pinia/nuxt": "^0.11.3",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"nuxt": "^4.3.0",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.27",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.server.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.shared.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user