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:
|
||||
|
||||
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
@@ -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: [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
about
|
||||
</template>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user