Dockerfile, nastrel verejne casti
This commit is contained in:
@@ -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
@@ -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
@@ -1,22 +1,46 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:15-alpine
|
image: mariadb:10.11
|
||||||
|
ports:
|
||||||
cron:
|
- 3306:3306
|
||||||
image:
|
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:
|
api:
|
||||||
context:
|
build:
|
||||||
build: ../api
|
context: ../api
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
volumes:
|
||||||
|
- ../api:/app
|
||||||
|
environment:
|
||||||
|
TZ: "Europe/Prague"
|
||||||
|
# command: ls -l /app
|
||||||
|
|
||||||
app:
|
app:
|
||||||
context:
|
build:
|
||||||
build: .
|
context: .
|
||||||
|
target: "develop-stage"
|
||||||
nginx:
|
ports:
|
||||||
image:
|
- "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
@@ -25,7 +25,7 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
// https://v2.quasar.dev/quasar-cli/prefetch-feature
|
// https://v2.quasar.dev/quasar-cli/prefetch-feature
|
||||||
// preFetch: true,
|
// preFetch: true,
|
||||||
|
|
||||||
// app boot file (/src/boot)
|
// app boot file (/poletani/boot)
|
||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli/boot-files
|
// https://v2.quasar.dev/quasar-cli/boot-files
|
||||||
boot: [
|
boot: [
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-avatar :size="`${size}px`">
|
<q-avatar :size="`${size}px`">
|
||||||
<div :style="`width: ${size}px; height: ${size}px; background-image: url('${src}'); background-size: cover`">
|
<span :style="`width: ${size}px; height: ${size}px; background-image: url('${src}'); background-size: cover`">
|
||||||
</div>
|
</span>
|
||||||
</q-avatar>
|
</q-avatar>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {FORMAT_DATE, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
import {computed} from "vue";
|
import {flightName} from "src/composables/objects/flight";
|
||||||
import {useQuasar} from "quasar";
|
|
||||||
import {useFlightsStore} from "stores/Flights";
|
|
||||||
import {flightName} from "../../composables/objects/flight";
|
|
||||||
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
|
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -37,26 +34,21 @@ const props = defineProps({
|
|||||||
<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>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<div class="text-h5">{{ flightName(flight) }}</div>
|
<div class="text-h5 q-mb-md">{{ flightName(flight) }}</div>
|
||||||
<div class="text-secondary q-mt-md">
|
|
||||||
<q-icon name="flight_takeoff"/>
|
|
||||||
{{ formatDatetime(flight.takeoffDatetime, FORMAT_DATE) }}
|
|
||||||
</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">
|
<div class="text-secondary" v-if="flight.copilot">
|
||||||
<q-icon name="person"/>
|
<q-icon name="person"/>
|
||||||
{{ flight.copilot.name }}
|
{{ flight.copilot.name }}
|
||||||
</div>
|
</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>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section side top v-if="flight.isPublic && displayVisibilityInfo">
|
<q-item-section side top v-if="flight.isPublic && displayVisibilityInfo">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import FormWrapper from "components/FormWrapper.vue";
|
|||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
import {reactive, watch} from "vue";
|
import {reactive, watch} from "vue";
|
||||||
import {setDefaultData} from "src/composables/form/formHandling";
|
import {setDefaultData} from "src/composables/form/formHandling";
|
||||||
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
user: {
|
user: {
|
||||||
@@ -27,9 +27,7 @@ watch(() => props.user, (newVal) => {
|
|||||||
<FormWrapper v-model="formData">
|
<FormWrapper v-model="formData">
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<q-avatar size="200px">
|
<AvatarFromPhoto :size="200" :src="user.avatarImageUrl" />
|
||||||
<img :src="user.avatarImageUrl" alt="Profilový obrázek"/>
|
|
||||||
</q-avatar>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-file
|
<q-file
|
||||||
|
|||||||
+15
-10
@@ -11,12 +11,11 @@ const uploadLink = createUploadLink({
|
|||||||
uri: 'http://localhost:8000/graphql',
|
uri: 'http://localhost:8000/graphql',
|
||||||
})
|
})
|
||||||
|
|
||||||
const errorLink = onError(
|
const handleRefreshToken = (forward, operation) => {
|
||||||
({graphQLErrors, networkError, operation, forward}) => {
|
|
||||||
if (networkError?.statusCode === 401) {
|
|
||||||
return fromPromise(
|
return fromPromise(
|
||||||
useUserStore().refreshAccessToken().catch((error) => {
|
useUserStore().refreshAccessToken()
|
||||||
useAppStore().appStatus = 401;
|
.catch((error) => {
|
||||||
|
window.location = "/login";
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.filter((value) => Boolean(value))
|
.filter((value) => Boolean(value))
|
||||||
@@ -34,20 +33,26 @@ const errorLink = onError(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errorLink = onError(
|
||||||
|
({graphQLErrors, networkError, operation, forward}) => {
|
||||||
|
if (networkError?.statusCode === 401) {
|
||||||
|
return handleRefreshToken(forward, operation);
|
||||||
|
}
|
||||||
|
|
||||||
if (graphQLErrors) {
|
if (graphQLErrors) {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
|
|
||||||
for (const error of graphQLErrors) {
|
for (const error of graphQLErrors) {
|
||||||
if (error.message === "Not found") {
|
if (error.message === "Not found") {
|
||||||
useAppStore().appStatus = 404;
|
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") {
|
// TODO: zaridit zobrazeni erroru z messages
|
||||||
window.location = "/login";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(graphQLErrors)
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const authLink = new ApolloLink((operation, forward) => {
|
const authLink = new ApolloLink((operation, forward) => {
|
||||||
|
|||||||
@@ -15,9 +15,23 @@
|
|||||||
{{ pageTitle || "Polétání.cz" }}
|
{{ pageTitle || "Polétání.cz" }}
|
||||||
</q-toolbar-title>
|
</q-toolbar-title>
|
||||||
|
|
||||||
<router-link :to="{name: 'profile'}">
|
<q-btn flat>
|
||||||
<AvatarFromPhoto :size="32" :src="loggedUser.avatarImageUrl"/>
|
<AvatarFromPhoto v-if="loggedUser.avatarImageUrl" :size="32" :src="loggedUser.avatarImageUrl"/>
|
||||||
</router-link>
|
<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-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
@@ -57,8 +71,10 @@ import {useUserStore} from "stores/User";
|
|||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import BreadcrumbsNavigation from "components/BreadcrumbsNavigation.vue";
|
import BreadcrumbsNavigation from "components/BreadcrumbsNavigation.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import {useMeta} from "quasar";
|
import {useMeta, useQuasar} from "quasar";
|
||||||
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
|
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
import {successObject} from "src/composables/dialog";
|
||||||
|
|
||||||
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
|
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
|
||||||
useMeta(() => ({
|
useMeta(() => ({
|
||||||
@@ -105,4 +121,14 @@ onMounted(() => {
|
|||||||
useUserStore().fetchLoggedUserDetail();
|
useUserStore().fetchLoggedUserDetail();
|
||||||
})
|
})
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
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>
|
</script>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<router-link :to="{name: 'login'}">
|
<router-link :to="{name: 'login'}">
|
||||||
<q-avatar icon="person">
|
<q-avatar icon="person" text-color="white">
|
||||||
<q-tooltip>
|
<q-tooltip>
|
||||||
Login
|
Login
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
about
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const flights = storeToRefs(useFlightsStore()).items;
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FlightsListSkeleton v-if="loading || !pilot"/>
|
<FlightsListSkeleton v-if="loading || !pilot"/>
|
||||||
|
|
||||||
<div class="row" v-else>
|
<div class="row" v-else>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-banner class="bg-primary text-white" v-if="flights.length === 0">
|
<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}}"
|
:route-detail="{name: 'publicFlightDetail', params: {username: username}}"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 sm-hide">
|
<div class="col-4 sm-hide">
|
||||||
<div class="text-center">
|
<q-card class="q-mr-md" flat>
|
||||||
<AvatarFromPhoto :size="150" :src="pilot.avatarImageUrl"/>
|
<q-img :src="pilot.titleImageUrl" v-if="pilot.titleImageUrl"/>
|
||||||
</div>
|
<q-card-section>
|
||||||
|
<div class="text-center" :style="`margin-top: ${pilot.titleImageUrl ? '-100' : '0'}px`">
|
||||||
<h3>{{ pilot.name }}</h3>
|
<AvatarFromPhoto :size="150" :src="pilot.avatarImageUrl" v-if="pilot.avatarImageUrl" class="q-mb-sm"/>
|
||||||
<div class="text-subtitle1">
|
<div class="text-h3">{{ pilot.name }}</div>
|
||||||
|
<div class="text-subtitle1 text-weight-thin">
|
||||||
Registrován: {{ formatDatetime(pilot.createdAt, FORMAT_DATE) }}
|
Registrován: {{ formatDatetime(pilot.createdAt, FORMAT_DATE) }}
|
||||||
</div>
|
</div>
|
||||||
|
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-6
@@ -47,9 +47,11 @@ export const useUserStore = defineStore('LoggedUser', {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
logout() {
|
logout() {
|
||||||
|
return api.post("/logout").then(response => {
|
||||||
Object.assign(this.user, {});
|
Object.assign(this.user, {});
|
||||||
localStorage.removeItem("accessToken");
|
localStorage.removeItem("accessToken");
|
||||||
localStorage.removeItem("accessTokenValidity");
|
localStorage.removeItem("accessTokenValidity");
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setTokens({access_token, access_token_validity}) {
|
setTokens({access_token, access_token_validity}) {
|
||||||
@@ -71,17 +73,11 @@ export const useUserStore = defineStore('LoggedUser', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async refreshAccessToken() {
|
async refreshAccessToken() {
|
||||||
if (this.isRefreshing) return;
|
|
||||||
|
|
||||||
this.isRefreshing = true;
|
|
||||||
return api.post("/refresh")
|
return api.post("/refresh")
|
||||||
.then(({data}) => {
|
.then(({data}) => {
|
||||||
this.setTokens(data);
|
this.setTokens(data);
|
||||||
return data.access_token;
|
return data.access_token;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
|
||||||
this.isRefreshing = false;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user