Dockerfile, nastrel verejne casti

This commit is contained in:
Michal Kváček
2023-08-14 15:01:38 +02:00
parent 85bec30ca9
commit b6ae794489
13 changed files with 170 additions and 71 deletions
+29
View File
@@ -0,0 +1,29 @@
yarn-debug.log*
yarn-error.log*
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
+18
View File
@@ -0,0 +1,18 @@
# develop stage
FROM node:20-alpine as develop-stage
WORKDIR /app
COPY package*.json ./
RUN yarn global add @quasar/cli && yarn --production=false
COPY . .
# build stage
FROM develop-stage as build-stage
RUN yarn --production=false
RUN quasar build
# production stage
FROM nginx:1.17.5-alpine as production-stage
COPY --from=build-stage /app/dist/spa /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+36 -12
View File
@@ -1,22 +1,46 @@
services:
db:
image: postgres:15-alpine
cron:
image:
image: mariadb:10.11
ports:
- 3306:3306
environment:
- MYSQL_USER=root
- MYSQL_DATABASE=ull_tracker
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true
volumes:
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
- db-data:/var/lib/mysql:rw
api:
context:
build: ../api
build:
context: ../api
ports:
- 8000:8000
volumes:
- ../api:/app
environment:
TZ: "Europe/Prague"
# command: ls -l /app
app:
context:
build: .
nginx:
image:
build:
context: .
target: "develop-stage"
ports:
- "9001:9000"
volumes:
- ./src:/app/src
- ./public:/app/public
- ./node_modules:/app/node_modules:delegated
command: quasar dev
# nginx:
# image:
#
#
volumes:
db-data: { }
+1 -1
View File
@@ -25,7 +25,7 @@ module.exports = configure(function (/* ctx */) {
// https://v2.quasar.dev/quasar-cli/prefetch-feature
// preFetch: true,
// app boot file (/src/boot)
// app boot file (/poletani/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files
boot: [
+2 -2
View File
@@ -14,7 +14,7 @@ const props = defineProps({
<template>
<q-avatar :size="`${size}px`">
<div :style="`width: ${size}px; height: ${size}px; background-image: url('${src}'); background-size: cover`">
</div>
<span :style="`width: ${size}px; height: ${size}px; background-image: url('${src}'); background-size: cover`">
</span>
</q-avatar>
</template>
+9 -17
View File
@@ -1,9 +1,6 @@
<script setup>
import {FORMAT_DATE, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
import {computed} from "vue";
import {useQuasar} from "quasar";
import {useFlightsStore} from "stores/Flights";
import {flightName} from "../../composables/objects/flight";
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
import {flightName} from "src/composables/objects/flight";
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
const props = defineProps({
@@ -34,29 +31,24 @@ const props = defineProps({
:to="{name: routeDetail.name, params:{...routeDetail.params, id: flight.id}}"
>
<q-item-section avatar>
<AvatarFromPhoto :size="120" v-if="flight.coverPhoto" :src="flight.coverPhoto.thumbnailUrl" />
<AvatarFromPhoto :size="120" v-if="flight.coverPhoto" :src="flight.coverPhoto.thumbnailUrl"/>
</q-item-section>
<q-item-section>
<div class="text-h5">{{ flightName(flight) }}</div>
<div class="text-secondary q-mt-md">
<q-icon name="flight_takeoff"/>
{{ formatDatetime(flight.takeoffDatetime, FORMAT_DATE) }}
<div class="text-h5 q-mb-md">{{ flightName(flight) }}</div>
<div class="text-secondary" v-if="flight.copilot">
<q-icon name="person"/>
{{ flight.copilot.name }}
</div>
<div class="text-secondary">
<q-icon name="schedule"/>
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
</div>
<div class="text-secondary">
<q-icon name="flight"/>
{{ flight.aircraft.callSign }}
</div>
<div class="text-secondary" v-if="flight.copilot">
<q-icon name="person" />
{{ flight.copilot.name }}
</div>
</q-item-section>
<q-item-section side top v-if="flight.isPublic && displayVisibilityInfo">
+2 -4
View File
@@ -3,7 +3,7 @@ import FormWrapper from "components/FormWrapper.vue";
import {commonProps} from "src/composables/inputs";
import {reactive, watch} from "vue";
import {setDefaultData} from "src/composables/form/formHandling";
import RichTextEditor from "components/inputs/RichTextEditor.vue";
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
const props = defineProps({
user: {
@@ -27,9 +27,7 @@ watch(() => props.user, (newVal) => {
<FormWrapper v-model="formData">
<div class="text-center">
<q-avatar size="200px">
<img :src="user.avatarImageUrl" alt="Profilový obrázek"/>
</q-avatar>
<AvatarFromPhoto :size="200" :src="user.avatarImageUrl" />
</div>
<q-file
+17 -12
View File
@@ -11,13 +11,12 @@ const uploadLink = createUploadLink({
uri: 'http://localhost:8000/graphql',
})
const errorLink = onError(
({graphQLErrors, networkError, operation, forward}) => {
if (networkError?.statusCode === 401) {
return fromPromise(
useUserStore().refreshAccessToken().catch((error) => {
useAppStore().appStatus = 401;
})
const handleRefreshToken = (forward, operation) => {
return fromPromise(
useUserStore().refreshAccessToken()
.catch((error) => {
window.location = "/login";
})
)
.filter((value) => Boolean(value))
.flatMap((accessToken) => {
@@ -32,21 +31,27 @@ const errorLink = onError(
// retry the request, returning the new observable
return forward(operation);
});
}
const errorLink = onError(
({graphQLErrors, networkError, operation, forward}) => {
if (networkError?.statusCode === 401) {
return handleRefreshToken(forward, operation);
}
if (graphQLErrors) {
const messages = [];
for (const error of graphQLErrors) {
if (error.message === "Not found") {
useAppStore().appStatus = 404;
} else if (error.message.includes("401") || error.message.includes("Not authorized")) {
return handleRefreshToken(forward, operation);
} else {
messages.push(error.message);
}
if (error.message === "Not authenticated") {
window.location = "/login";
}
// TODO: zaridit zobrazeni erroru z messages
}
console.log(graphQLErrors)
}
});
+30 -4
View File
@@ -15,9 +15,23 @@
{{ pageTitle || "Polétání.cz" }}
</q-toolbar-title>
<router-link :to="{name: 'profile'}">
<AvatarFromPhoto :size="32" :src="loggedUser.avatarImageUrl"/>
</router-link>
<q-btn flat>
<AvatarFromPhoto v-if="loggedUser.avatarImageUrl" :size="32" :src="loggedUser.avatarImageUrl"/>
<q-icon name="person" v-else/>
<q-menu auto-close>
<q-list style="min-width: 100px">
<q-item clickable :to="{name: 'profile'}">
<q-item-section>Osobní nastavení</q-item-section>
</q-item>
<q-item clickable @click="logout">
<q-item-section>Odhlásit</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</q-toolbar>
</q-header>
@@ -57,8 +71,10 @@ import {useUserStore} from "stores/User";
import {storeToRefs} from "pinia";
import BreadcrumbsNavigation from "components/BreadcrumbsNavigation.vue";
import {useAppStore} from "stores/App";
import {useMeta} from "quasar";
import {useMeta, useQuasar} from "quasar";
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
import {useRouter} from "vue-router";
import {successObject} from "src/composables/dialog";
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
useMeta(() => ({
@@ -105,4 +121,14 @@ onMounted(() => {
useUserStore().fetchLoggedUserDetail();
})
const loggedUser = storeToRefs(useUserStore()).user;
const $q = useQuasar();
const $router = useRouter();
const logout = () => {
useUserStore().logout().then(() => {
$q.notify(successObject("Odhlášení bylo úspěšné"));
$router.push({name: "homepage"});
})
}
</script>
+1 -1
View File
@@ -13,7 +13,7 @@
</template>
<template v-else>
<router-link :to="{name: 'login'}">
<q-avatar icon="person">
<q-avatar icon="person" text-color="white">
<q-tooltip>
Login
</q-tooltip>
+5
View File
@@ -0,0 +1,5 @@
<script setup></script>
<template>
about
</template>
+15 -9
View File
@@ -39,6 +39,7 @@ const flights = storeToRefs(useFlightsStore()).items;
<template>
<FlightsListSkeleton v-if="loading || !pilot"/>
<div class="row" v-else>
<div class="col">
<q-banner class="bg-primary text-white" v-if="flights.length === 0">
@@ -52,19 +53,24 @@ const flights = storeToRefs(useFlightsStore()).items;
:route-detail="{name: 'publicFlightDetail', params: {username: username}}"
/>
</template>
</div>
<div class="col-4 sm-hide">
<div class="text-center">
<AvatarFromPhoto :size="150" :src="pilot.avatarImageUrl"/>
</div>
<q-card class="q-mr-md" flat>
<q-img :src="pilot.titleImageUrl" v-if="pilot.titleImageUrl"/>
<q-card-section>
<div class="text-center" :style="`margin-top: ${pilot.titleImageUrl ? '-100' : '0'}px`">
<AvatarFromPhoto :size="150" :src="pilot.avatarImageUrl" v-if="pilot.avatarImageUrl" class="q-mb-sm"/>
<div class="text-h3">{{ pilot.name }}</div>
<div class="text-subtitle1 text-weight-thin">
Registrován: {{ formatDatetime(pilot.createdAt, FORMAT_DATE) }}
</div>
</div>
<h3>{{ pilot.name }}</h3>
<div class="text-subtitle1">
Registrován: {{ formatDatetime(pilot.createdAt, FORMAT_DATE) }}
</div>
<div class="q-my-lg" v-html="pilot.description"/>
<div class="q-my-lg" v-html="pilot.description"/>
</q-card-section>
</q-card>
</div>
</div>
+5 -9
View File
@@ -47,9 +47,11 @@ export const useUserStore = defineStore('LoggedUser', {
})
},
logout() {
Object.assign(this.user, {});
localStorage.removeItem("accessToken");
localStorage.removeItem("accessTokenValidity");
return api.post("/logout").then(response => {
Object.assign(this.user, {});
localStorage.removeItem("accessToken");
localStorage.removeItem("accessTokenValidity");
});
},
setTokens({access_token, access_token_validity}) {
@@ -71,17 +73,11 @@ export const useUserStore = defineStore('LoggedUser', {
},
async refreshAccessToken() {
if (this.isRefreshing) return;
this.isRefreshing = true;
return api.post("/refresh")
.then(({data}) => {
this.setTokens(data);
return data.access_token;
})
.finally(() => {
this.isRefreshing = false;
});
},
}
})