Fixed docker deploments, moved files to _folders in dispatch app

This commit is contained in:
PxlLoewe
2025-05-27 17:34:44 -07:00
parent 5d5b2dc91f
commit 571ddfba85
60 changed files with 251 additions and 406 deletions

View File

@@ -10,3 +10,4 @@ Dockerfile
.eslint.config.msj .eslint.config.msj
.README.md .README.md
.env.example .env.example
.env

View File

@@ -1,37 +1,73 @@
# Allgemein / NextAuth # ───────────────────────────────────────────────
NEXTAUTH_URL=http://localhost:3000 # 🔐 Authentifizierung & Cookies
NEXTAUTH_COOKIE_PREFIX=HUB # ───────────────────────────────────────────────
NEXTAUTH_SECRET=var AUTH_DISPATCH_SECRET=dispatch
NEXTAUTH_HUB_SECRET=var-hub-secret AUTH_HUB_SECRET=var
# Datenbank AUTH_DISPATCH_COOKIE_PREFIX=DISPATCH
AUTH_HUB_COOKIE_PREFIX=HUB
AUTH_DISPATCH_URL=http://localhost:3001
AUTH_HUB_URL=http://localhost:3000
NEXT_PUBLIC_DISPATCH_SERVICE_ID=1
# ───────────────────────────────────────────────
# 🌐 Öffentliche URLs
# ───────────────────────────────────────────────
NEXT_PUBLIC_HUB_URL=http://localhost:3000
NEXT_PUBLIC_HUB_SERVER_URL=http://localhost:3003
NEXT_PUBLIC_DISPATCH_URL=http://localhost:3001
NEXT_PUBLIC_DISPATCH_SERVER_URL=http://localhost:3002
# ───────────────────────────────────────────────
# 🗄️ Datenbank
# ───────────────────────────────────────────────
DATABASE_URL=postgresql://persistant-data:persistant-data-pw@postgres:5432/var DATABASE_URL=postgresql://persistant-data:persistant-data-pw@postgres:5432/var
# Discord # ───────────────────────────────────────────────
# 📡 LiveKit Konfiguration
# ───────────────────────────────────────────────
NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880
LIVEKIT_API_KEY=APIAnsGdtdYp2Ho
LIVEKIT_API_SECRET=tdPjVsYUx8ddC7K9NvdmVAeLRF9GeADD6Fedm1x63fWC
# ───────────────────────────────────────────────
# 🚦 Dispatch Server (Backend)
# ───────────────────────────────────────────────
DISPATCH_SERVER_PORT=3000
DISPATCH_APP_TOKEN=dispatch
REDIS_HOST=redis
REDIS_PORT=6379
# ───────────────────────────────────────────────
# 🧠 HUB Server (Backend)
# ───────────────────────────────────────────────
HUB_SERVER_PORT=3000
HUB_URL=
# ───────────────────────────────────────────────
# 📚 Moodle
# ───────────────────────────────────────────────
MOODLE_URL=http://localhost:8081
MOODLE_API_TOKEN=ac346f0324647b68488d13fd52a9bbe8
MOODLE_USER_PASSWORD=var-api-user-P1
NEXT_PUBLIC_MOODLE_URL=http://localhost:8081
# ───────────────────────────────────────────────
# 📧 E-Mail Einstellungen (nur HUB Server)
# ───────────────────────────────────────────────
MAIL_SERVER=asmtp.mail.hostpoint.ch
MAIL_PORT=465
MAIL_USER=noreply@virtualairrescue.com
MAIL_PASSWORD=b7316PB8aDPCC%-&
# ───────────────────────────────────────────────
# 🕹️ Discord OAuth (optional)
# ───────────────────────────────────────────────
DISCORD_OAUTH_CLIENT_ID= DISCORD_OAUTH_CLIENT_ID=
DISCORD_OAUTH_SECRET= DISCORD_OAUTH_SECRET=
DISCORD_BOT_TOKEN= DISCORD_BOT_TOKEN=
DISCORD_REDIRECT_URL=
NEXT_PUBLIC_DISCORD_URL= NEXT_PUBLIC_DISCORD_URL=
DISCORD_REDIRECT=
# Moodle
MOODLE_PW=var-api-user-P1
MOODLE_TOKEN=ac346f0324647b68488d13fd52a9bbe8
NEXT_PUBLIC_MOODLE_URL=http://localhost:8081
# Hub Server
NEXT_PUBLIC_HUB_SERVER_URL=http://localhost:3003
# Dispatch Server
NEXT_PUBLIC_DISPATCH_SERVER_URL=http://localhost:3001
# Livekit
NEXT_PUBLIC_LIVEKIT_URL=http://localhost:7880
LIVEKIT_API_KEY=
LIVEKIT_API_SECRET=
# HUB Server
HUB_API_PORT=3000
# Redis (Beispiel)
REDIS_URL=redis://localhost:6379

102
README.md
View File

@@ -1,103 +1,9 @@
# Turborepo starter # Turborepo Monorepo für LST V2
This is an official starter Turborepo. ## Docker Dev
## Using this example Um lokal Docker-Images zu bauen, gib die `.env`-Datei mit folgendem Befehl an `docker compose` weiter:
Run the following command:
```sh ```sh
npx create-turbo@latest docker compose --env-file .env.prod -f 'docker-compose.prod.yml' up -d
``` ```
## What's inside?
This Turborepo includes the following packages/apps:
### Apps and Packages
- `dispatch`: a dispatching platform for web based unit/mission dispatching
- `docs`: a [Next.js](https://nextjs.org/) app
- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications
- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
### Utilities
This Turborepo has some additional tools already setup for you:
- [TypeScript](https://www.typescriptlang.org/) for static type checking
- [ESLint](https://eslint.org/) for code linting
- [Prettier](https://prettier.io) for code formatting
### Build
To build all apps and packages, run the following command:
```
cd my-turborepo
pnpm build
```
### Develop
To develop all apps and packages, run the following command:
```
cd my-turborepo
pnpm dev
```
### Remote Caching
> [!TIP]
> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache).
Turborepo can use a technique known as [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands:
```
cd my-turborepo
npx turbo login
```
This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:
```
npx turbo link
```
## Useful Links
Learn more about the power of Turborepo:
- [Tasks](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks)
- [Caching](https://turbo.build/repo/docs/core-concepts/caching)
- [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching)
- [Filtering](https://turbo.build/repo/docs/core-concepts/monorepos/filtering)
- [Configuration Options](https://turbo.build/repo/docs/reference/configuration)
- [CLI Usage](https://turbo.build/repo/docs/reference/command-line-reference)
## Execution policy
MachinePolicy Undefined
UserPolicy Undefined
Process Undefined
CurrentUser RemoteSigned
LocalMachine RemoteSigned
## Moodle:
1. Im docker volume gehe in lib -> Classes -> OAuth2 -> Endpoint.php
2. überspringe die https enforcement rule am Ende der Datei (true in if abfrage)
3. Moodle Admin -> General -> HTTP Security -> Curl einschränkungen löschen
4. http://localhost:8081/admin/category.php?category=authsettings -> Guest login button -> Hide
5. http://localhost:8081/admin/settings.php?section=sitepolicies -> emailchangeconfirmation -> False
6. Beim anlegen des Auth-Services Require Email verification deaktivieren
7. Beim erstellen der Role für API user: permission moodle/site:viewuseridentity geben
8. API user zu moodle kursen hinzufügen, um andere Nutzer hinzuzufügen

View File

@@ -47,6 +47,6 @@ app.use(cookieParser());
app.use(authMiddleware as any); app.use(authMiddleware as any);
app.use(router); app.use(router);
server.listen(process.env.PORT, () => { server.listen(process.env.DISPATCH_SERVER_PORT, () => {
console.log(`Server running on port ${process.env.PORT}`); console.log(`Server running on port ${process.env.DISPATCH_SERVER_PORT}`);
}); });

View File

@@ -1,6 +1,8 @@
import { createClient, RedisClientType } from "redis"; import { createClient, RedisClientType } from "redis";
export const pubClient: RedisClientType = createClient(); export const pubClient: RedisClientType = createClient({
url: `redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`,
});
export const subClient: RedisClientType = pubClient.duplicate(); export const subClient: RedisClientType = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => { Promise.all([pubClient.connect(), subClient.connect()]).then(() => {

View File

@@ -5,7 +5,7 @@
}, },
"scripts": { "scripts": {
"dev": "nodemon --signal SIGINT", "dev": "nodemon --signal SIGINT",
"start": "node index.js", "start": "tsx index.ts --transpile-only",
"build": "tsc" "build": "tsc"
}, },
"devDependencies": { "devDependencies": {
@@ -34,6 +34,7 @@
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"react": "^19.0.0", "react": "^19.0.0",
"redis": "^4.7.0", "redis": "^4.7.0",
"socket.io": "^4.8.1" "socket.io": "^4.8.1",
"tsx": "^4.19.4"
} }
} }

View File

@@ -4,3 +4,4 @@ Dockerfile
.eslint.config.msj .eslint.config.msj
.README.md .README.md
.env.example .env.example
.env

View File

@@ -1,10 +1,10 @@
NEXTAUTH_SECRET=dispatch AUTH_DISPATCH_SECRET=dispatch
NEXTAUTH_COOKIE_PREFIX=DISPATCH AUTH_DISPATCH_COOKIE_PREFIX=DISPATCH
NEXT_PUBLIC_DISPATCH_SERVER_URL=http://localhost:3002 NEXT_PUBLIC_DISPATCH_SERVER_URL=http://localhost:3002
NEXTAUTH_URL=http://localhost:3001 NEXTAUTH_URL=http://localhost:3001
NEXT_PUBLIC_HUB_URL=http://localhost:3000 NEXT_PUBLIC_HUB_URL=http://localhost:3000
NEXT_PUBLIC_PUBLIC_URL=http://localhost:3001 NEXT_PUBLIC_DISPATCH_URL=http://localhost:3001
NEXT_PUBLIC_SERVICE_ID=1 NEXT_PUBLIC_DISPATCH_SERVICE_ID=1
DATABASE_URL=postgresql://persistant-data:persistant-data-pw@localhost:5432/var DATABASE_URL=postgresql://persistant-data:persistant-data-pw@localhost:5432/var
NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880 NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880
LIVEKIT_API_KEY=APIAnsGdtdYp2Ho LIVEKIT_API_KEY=APIAnsGdtdYp2Ho

View File

@@ -1,5 +1,17 @@
FROM node:22-alpine AS base FROM node:22-alpine AS base
ARG NEXT_PUBLIC_DISPATCH_URL
ARG NEXT_PUBLIC_DISPATCH_SERVER_URL
ARG NEXT_PUBLIC_HUB_URL
ARG NEXT_PUBLIC_DISPATCH_SERVICE_ID
ARG NEXT_PUBLIC_LIVEKIT_URL
ENV NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL
ENV NEXT_PUBLIC_DISPATCH_URL=$NEXT_PUBLIC_DISPATCH_URL
ENV NEXT_PUBLIC_HUB_URL=$NEXT_PUBLIC_HUB_URL
ENV NEXT_PUBLIC_DISPATCH_SERVICE_ID=$NEXT_PUBLIC_DISPATCH_SERVICE_ID
ENV NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL
ENV PNPM_HOME="/usr/local/pnpm" ENV PNPM_HOME="/usr/local/pnpm"
ENV PATH="${PNPM_HOME}:${PATH}" ENV PATH="${PNPM_HOME}:${PATH}"
RUN corepack enable && corepack prepare pnpm@latest --activate RUN corepack enable && corepack prepare pnpm@latest --activate
@@ -12,6 +24,10 @@ RUN apk add --no-cache libc6-compat
WORKDIR /usr/app WORKDIR /usr/app
RUN echo "NEXT_PUBLIC_HUB_URL is: $NEXT_PUBLIC_HUB_URL"
RUN echo "NEXT_PUBLIC_DISPATCH_SERVICE_ID is: $NEXT_PUBLIC_DISPATCH_SERVICE_ID"
RUN echo "NEXT_PUBLIC_DISPATCH_SERVER_URL is: $NEXT_PUBLIC_DISPATCH_SERVER_URL"
COPY . . COPY . .
RUN turbo prune dispatch --docker RUN turbo prune dispatch --docker

View File

@@ -49,16 +49,10 @@ export const Login = () => {
<div className="form-control mt-6"> <div className="form-control mt-6">
<a <a
href={`${process.env.NEXT_PUBLIC_HUB_URL}/oauth?service=${encodeURIComponent(process.env.NEXT_PUBLIC_SERVICE_ID || "")}&redirect_uri=${encodeURIComponent(`${process.env.NEXT_PUBLIC_PUBLIC_URL}/login`)}`} href={`${process.env.NEXT_PUBLIC_HUB_URL}/oauth?service=${encodeURIComponent(process.env.NEXT_PUBLIC_DISPATCH_SERVICE_ID || "")}&redirect_uri=${encodeURIComponent(`${process.env.NEXT_PUBLIC_DISPATCH_URL}/login`)}`}
> >
<button <button className="btn btn-primary" name="loginBtn" disabled={isLoading}>
className="btn btn-primary" {isLoading && <span className="loading loading-spinner loading-sm"></span>}
name="loginBtn"
disabled={isLoading}
>
{isLoading && (
<span className="loading loading-spinner loading-sm"></span>
)}
Login{isLoading && "..."} Login{isLoading && "..."}
</button> </button>
</a> </a>

View File

@@ -15,7 +15,7 @@ import {
ZapOff, ZapOff,
} from "lucide-react"; } from "lucide-react";
import { useAudioStore } from "_store/audioStore"; import { useAudioStore } from "_store/audioStore";
import { cn } from "helpers/cn"; import { cn } from "_helpers/cn";
import { ConnectionQuality } from "livekit-client"; import { ConnectionQuality } from "livekit-client";
import { ROOMS } from "_data/livekitRooms"; import { ROOMS } from "_data/livekitRooms";

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { cn } from "helpers/cn"; import { cn } from "_helpers/cn";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
type MicrophoneLevelProps = { type MicrophoneLevelProps = {

View File

@@ -1,20 +1,11 @@
"use client"; "use client";
import { import { FieldValues, Path, RegisterOptions, UseFormReturn } from "react-hook-form";
FieldValues, import SelectTemplate, { Props as SelectTemplateProps, StylesConfig } from "react-select";
Path, import { cn } from "_helpers/cn";
RegisterOptions,
UseFormReturn,
} from "react-hook-form";
import SelectTemplate, {
Props as SelectTemplateProps,
StylesConfig,
} from "react-select";
import { cn } from "helpers/cn";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { CSSProperties } from "react"; import { CSSProperties } from "react";
interface SelectProps<T extends FieldValues> interface SelectProps<T extends FieldValues> extends Omit<SelectTemplateProps, "form"> {
extends Omit<SelectTemplateProps, "form"> {
label?: any; label?: any;
name: Path<T>; name: Path<T>;
form: UseFormReturn<T> | any; form: UseFormReturn<T> | any;
@@ -69,9 +60,7 @@ const SelectCom = <T extends FieldValues>({
}: SelectProps<T>) => { }: SelectProps<T>) => {
return ( return (
<div> <div>
<span className="label-text text-lg flex items-center gap-2"> <span className="label-text text-lg flex items-center gap-2">{label}</span>
{label}
</span>
<SelectTemplate <SelectTemplate
onChange={(newValue: any) => { onChange={(newValue: any) => {
if (Array.isArray(newValue)) { if (Array.isArray(newValue)) {
@@ -88,12 +77,8 @@ const SelectCom = <T extends FieldValues>({
}} }}
value={ value={
(inputProps as any)?.isMulti (inputProps as any)?.isMulti
? (inputProps as any).options?.filter((o: any) => ? (inputProps as any).options?.filter((o: any) => form.watch(name)?.includes(o.value))
form.watch(name)?.includes(o.value), : (inputProps as any).options?.find((o: any) => o.value === form.watch(name))
)
: (inputProps as any).options?.find(
(o: any) => o.value === form.watch(name),
)
} }
styles={customStyles as any} styles={customStyles as any}
className={cn("w-full placeholder:text-neutral-600", className)} className={cn("w-full placeholder:text-neutral-600", className)}
@@ -101,17 +86,13 @@ const SelectCom = <T extends FieldValues>({
{...inputProps} {...inputProps}
/> />
{form.formState.errors[name]?.message && ( {form.formState.errors[name]?.message && (
<p className="text-error"> <p className="text-error">{form.formState.errors[name].message as string}</p>
{form.formState.errors[name].message as string}
</p>
)} )}
</div> </div>
); );
}; };
const SelectWrapper = <T extends FieldValues>(props: SelectProps<T>) => ( const SelectWrapper = <T extends FieldValues>(props: SelectProps<T>) => <SelectCom {...props} />;
<SelectCom {...props} />
);
export const Select = dynamic(() => Promise.resolve(SelectWrapper), { export const Select = dynamic(() => Promise.resolve(SelectWrapper), {
ssr: false, ssr: false,

View File

@@ -4,7 +4,7 @@ import { GearIcon } from "@radix-ui/react-icons";
import { SettingsIcon, Volume2 } from "lucide-react"; import { SettingsIcon, Volume2 } from "lucide-react";
import MicVolumeBar from "_components/MicVolumeIndication"; import MicVolumeBar from "_components/MicVolumeIndication";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { editUserAPI, getUserAPI } from "querys/user"; import { editUserAPI, getUserAPI } from "_querys/user";
import { Prisma } from "@repo/db"; import { Prisma } from "@repo/db";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useAudioStore } from "_store/audioStore"; import { useAudioStore } from "_store/audioStore";

View File

@@ -1,10 +1,5 @@
import { cn } from "helpers/cn"; import { cn } from "_helpers/cn";
import { import { RefAttributes, useCallback, useEffect, useImperativeHandle } from "react";
RefAttributes,
useCallback,
useEffect,
useImperativeHandle,
} from "react";
import { createContext, Ref, useContext, useState } from "react"; import { createContext, Ref, useContext, useState } from "react";
import { Popup, PopupProps, useMap } from "react-leaflet"; import { Popup, PopupProps, useMap } from "react-leaflet";
import { Popup as LPopup } from "leaflet"; import { Popup as LPopup } from "leaflet";
@@ -113,9 +108,9 @@ export const SmartPopup = (
const [showContent, setShowContent] = useState(false); const [showContent, setShowContent] = useState(false);
const { smartPopupRef, id, className, wrapperClassName, options } = props; const { smartPopupRef, id, className, wrapperClassName, options } = props;
const [anchor, setAnchor] = useState< const [anchor, setAnchor] = useState<"topleft" | "topright" | "bottomleft" | "bottomright">(
"topleft" | "topright" | "bottomleft" | "bottomright" "topleft",
>("topleft"); );
const handleConflict = useCallback(() => { const handleConflict = useCallback(() => {
const newAnchor = calculateAnchor(id, "popup", options); const newAnchor = calculateAnchor(id, "popup", options);
@@ -160,9 +155,7 @@ export const SmartPopup = (
anchor.includes("top") && "-translate-y-1/2", anchor.includes("top") && "-translate-y-1/2",
)} )}
/> />
<PopupContext.Provider value={{ anchor: anchor }}> <PopupContext.Provider value={{ anchor: anchor }}>{props.children}</PopupContext.Provider>
{props.children}
</PopupContext.Provider>
</div> </div>
</Popup> </Popup>
); );

View File

@@ -1,4 +1,4 @@
import { cn } from "helpers/cn"; import { cn } from "_helpers/cn";
export const BaseNotification = ({ export const BaseNotification = ({
children, children,

View File

@@ -3,10 +3,10 @@ import { ChatBubbleIcon, PaperPlaneIcon } from "@radix-ui/react-icons";
import { useLeftMenuStore } from "_store/leftMenuStore"; import { useLeftMenuStore } from "_store/leftMenuStore";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { Fragment, useEffect, useState } from "react"; import { Fragment, useEffect, useState } from "react";
import { cn } from "helpers/cn"; import { cn } from "_helpers/cn";
import { asPublicUser } from "@repo/db"; import { asPublicUser } from "@repo/db";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getConnectedUserAPI } from "querys/connected-user"; import { getConnectedUserAPI } from "_querys/connected-user";
export const Chat = () => { export const Chat = () => {
const { const {
@@ -88,8 +88,7 @@ export const Chat = () => {
{[ {[
...(connectedUser?.filter( ...(connectedUser?.filter(
(user, idx, arr) => (user, idx, arr) => arr.findIndex((u) => u.userId === user.userId) === idx,
arr.findIndex((u) => u.userId === user.userId) === idx,
) || []), ) || []),
].map((user) => ( ].map((user) => (
<option key={user.userId} value={user.userId}> <option key={user.userId} value={user.userId}>
@@ -100,9 +99,7 @@ export const Chat = () => {
<button <button
className="btn btn-sm btn-soft btn-primary join-item" className="btn btn-sm btn-soft btn-primary join-item"
onClick={() => { onClick={() => {
const user = connectedUser?.find( const user = connectedUser?.find((user) => user.userId === addTabValue);
(user) => user.userId === addTabValue,
);
if (!user) return; if (!user) return;
addChat(addTabValue, asPublicUser(user.publicUser).fullName); addChat(addTabValue, asPublicUser(user.publicUser).fullName);
setSelectedChat(addTabValue); setSelectedChat(addTabValue);
@@ -135,28 +132,21 @@ export const Chat = () => {
/> />
<div className="tab-content bg-base-100 border-base-300 p-6"> <div className="tab-content bg-base-100 border-base-300 p-6">
{chat.messages.map((chatMessage) => { {chat.messages.map((chatMessage) => {
const isSender = const isSender = chatMessage.senderId === session.data?.user.id;
chatMessage.senderId === session.data?.user.id;
return ( return (
<div <div
key={chatMessage.id} key={chatMessage.id}
className={`chat ${isSender ? "chat-end" : "chat-start"}`} className={`chat ${isSender ? "chat-end" : "chat-start"}`}
> >
<p className="chat-footer opacity-50"> <p className="chat-footer opacity-50">
{new Date( {new Date(chatMessage.timestamp).toLocaleTimeString()}
chatMessage.timestamp,
).toLocaleTimeString()}
</p> </p>
<div className="chat-bubble"> <div className="chat-bubble">{chatMessage.text}</div>
{chatMessage.text}
</div>
</div> </div>
); );
})} })}
{!chat.messages.length && ( {!chat.messages.length && (
<p className="text-xs opacity-50"> <p className="text-xs opacity-50">Noch keine Nachrichten</p>
Noch keine Nachrichten
</p>
)} )}
</div> </div>
</Fragment> </Fragment>

View File

@@ -2,17 +2,16 @@
import { ExclamationTriangleIcon, PaperPlaneIcon } from "@radix-ui/react-icons"; import { ExclamationTriangleIcon, PaperPlaneIcon } from "@radix-ui/react-icons";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { cn } from "helpers/cn"; import { cn } from "_helpers/cn";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useLeftMenuStore } from "_store/leftMenuStore"; import { useLeftMenuStore } from "_store/leftMenuStore";
import { asPublicUser } from "@repo/db"; import { asPublicUser } from "@repo/db";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getConnectedUserAPI } from "querys/connected-user"; import { getConnectedUserAPI } from "_querys/connected-user";
import { sendReportAPI } from "querys/report"; import { sendReportAPI } from "_querys/report";
export const Report = () => { export const Report = () => {
const { setChatOpen, setReportTabOpen, reportTabOpen, setOwnId } = const { setChatOpen, setReportTabOpen, reportTabOpen, setOwnId } = useLeftMenuStore();
useLeftMenuStore();
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const session = useSession(); const session = useSession();
const [selectedPlayer, setSelectedPlayer] = useState<string>("default"); const [selectedPlayer, setSelectedPlayer] = useState<string>("default");
@@ -34,12 +33,7 @@ export const Report = () => {
}); });
return ( return (
<div <div className={cn("dropdown dropdown-right", reportTabOpen && "dropdown-open")}>
className={cn(
"dropdown dropdown-right",
reportTabOpen && "dropdown-open",
)}
>
<div className="indicator"> <div className="indicator">
<button <button
className="btn btn-soft btn-sm btn-error" className="btn btn-soft btn-sm btn-error"
@@ -78,8 +72,7 @@ export const Report = () => {
)} )}
{[ {[
...(connectedUser?.filter( ...(connectedUser?.filter(
(user, idx, arr) => (user, idx, arr) => arr.findIndex((u) => u.userId === user.userId) === idx,
arr.findIndex((u) => u.userId === user.userId) === idx,
) || []), ) || []),
].map((user) => ( ].map((user) => (
<option key={user.userId} value={user.userId}> <option key={user.userId} value={user.userId}>

View File

@@ -1,26 +1,10 @@
import { Marker, useMap } from "react-leaflet"; import { Marker, useMap } from "react-leaflet";
import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet"; import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet";
import { useMapStore } from "_store/mapStore"; import { useMapStore } from "_store/mapStore";
import { import { Fragment, useCallback, useEffect, useRef, useState, useMemo } from "react";
Fragment, import { cn } from "_helpers/cn";
useCallback, import { ChevronsRightLeft, House, MessageSquareText, Minimize2 } from "lucide-react";
useEffect, import { SmartPopup, calculateAnchor, useSmartPopup } from "_components/SmartPopup";
useRef,
useState,
useMemo,
} from "react";
import { cn } from "helpers/cn";
import {
ChevronsRightLeft,
House,
MessageSquareText,
Minimize2,
} from "lucide-react";
import {
SmartPopup,
calculateAnchor,
useSmartPopup,
} from "_components/SmartPopup";
import FMSStatusHistory, { import FMSStatusHistory, {
FMSStatusSelector, FMSStatusSelector,
MissionTab, MissionTab,
@@ -29,9 +13,9 @@ import FMSStatusHistory, {
} from "./_components/AircraftMarkerTabs"; } from "./_components/AircraftMarkerTabs";
import { ConnectedAircraft, Station } from "@repo/db"; import { ConnectedAircraft, Station } from "@repo/db";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getConnectedAircraftsAPI } from "querys/aircrafts"; import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { getMissionsAPI } from "querys/missions"; import { getMissionsAPI } from "_querys/missions";
import { checkSimulatorConnected } from "helpers/simulatorConnected"; import { checkSimulatorConnected } from "_helpers/simulatorConnected";
export const FMS_STATUS_COLORS: { [key: string]: string } = { export const FMS_STATUS_COLORS: { [key: string]: string } = {
"0": "rgb(140,10,10)", "0": "rgb(140,10,10)",
@@ -86,9 +70,7 @@ const AircraftPopupContent = ({
aircraft: ConnectedAircraft & { Station: Station }; aircraft: ConnectedAircraft & { Station: Station };
}) => { }) => {
const setAircraftTab = useMapStore((state) => state.setAircraftTab); const setAircraftTab = useMapStore((state) => state.setAircraftTab);
const currentTab = useMapStore( const currentTab = useMapStore((state) => state.aircraftTabs[aircraft.id] || "home");
(state) => state.aircraftTabs[aircraft.id] || "home",
);
// Memoize the tab change handler to avoid unnecessary re-renders // Memoize the tab change handler to avoid unnecessary re-renders
const handleTabChange = useCallback( const handleTabChange = useCallback(
@@ -126,9 +108,7 @@ const AircraftPopupContent = ({
<MissionTab mission={mission} /> <MissionTab mission={mission} />
) : ( ) : (
<div className="flex flex-col items-center justify-center min-h-full"> <div className="flex flex-col items-center justify-center min-h-full">
<span className="text-gray-500 my-10 font-semibold"> <span className="text-gray-500 my-10 font-semibold">Kein aktiver Einsatz</span>
Kein aktiver Einsatz
</span>
</div> </div>
); );
case "chat": case "chat":
@@ -138,9 +118,7 @@ const AircraftPopupContent = ({
} }
}, [currentTab, aircraft, mission]); }, [currentTab, aircraft, mission]);
const setOpenAircraftMarker = useMapStore( const setOpenAircraftMarker = useMapStore((state) => state.setOpenAircraftMarker);
(state) => state.setOpenAircraftMarker,
);
const { anchor } = useSmartPopup(); const { anchor } = useSmartPopup();
return ( return (
<> <>
@@ -278,19 +256,13 @@ const AircraftPopupContent = ({
); );
}; };
const AircraftMarker = ({ const AircraftMarker = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Station } }) => {
aircraft,
}: {
aircraft: ConnectedAircraft & { Station: Station };
}) => {
const [hideMarker, setHideMarker] = useState(false); const [hideMarker, setHideMarker] = useState(false);
const map = useMap(); const map = useMap();
const markerRef = useRef<LMarker>(null); const markerRef = useRef<LMarker>(null);
const popupRef = useRef<LPopup>(null); const popupRef = useRef<LPopup>(null);
const { openAircraftMarker, setOpenAircraftMarker } = useMapStore( const { openAircraftMarker, setOpenAircraftMarker } = useMapStore((store) => store);
(store) => store,
);
useEffect(() => { useEffect(() => {
const handleClick = () => { const handleClick = () => {
@@ -319,9 +291,9 @@ const AircraftMarker = ({
}; };
}, [aircraft.id, openAircraftMarker, setOpenAircraftMarker]); }, [aircraft.id, openAircraftMarker, setOpenAircraftMarker]);
const [anchor, setAnchor] = useState< const [anchor, setAnchor] = useState<"topleft" | "topright" | "bottomleft" | "bottomright">(
"topleft" | "topright" | "bottomleft" | "bottomright" "topleft",
>("topleft"); );
const handleConflict = useCallback(() => { const handleConflict = useCallback(() => {
const newAnchor = calculateAnchor(`aircraft-${aircraft.id}`, "marker"); const newAnchor = calculateAnchor(`aircraft-${aircraft.id}`, "marker");

View File

@@ -4,7 +4,7 @@ import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { useMapStore } from "_store/mapStore"; import { useMapStore } from "_store/mapStore";
import { usePannelStore } from "_store/pannelStore"; import { usePannelStore } from "_store/pannelStore";
import { MapPin, MapPinned, Radius, Ruler, Search, RulerDimensionLine, Scan } from "lucide-react"; import { MapPin, MapPinned, Radius, Ruler, Search, RulerDimensionLine, Scan } from "lucide-react";
import { getOsmAddress } from "querys/osm"; import { getOsmAddress } from "_querys/osm";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Popup, useMap } from "react-leaflet"; import { Popup, useMap } from "react-leaflet";

View File

@@ -3,7 +3,7 @@ import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet";
import { useMapStore } from "_store/mapStore"; import { useMapStore } from "_store/mapStore";
import { usePannelStore } from "_store/pannelStore"; import { usePannelStore } from "_store/pannelStore";
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { cn } from "helpers/cn"; import { cn } from "_helpers/cn";
import { ClipboardList, Cross, House, Minimize2, SmartphoneNfc, PencilLine } from "lucide-react"; import { ClipboardList, Cross, House, Minimize2, SmartphoneNfc, PencilLine } from "lucide-react";
import { calculateAnchor, SmartPopup, useSmartPopup } from "_components/SmartPopup"; import { calculateAnchor, SmartPopup, useSmartPopup } from "_components/SmartPopup";
import { Mission, MissionState } from "@repo/db"; import { Mission, MissionState } from "@repo/db";
@@ -13,10 +13,10 @@ import Einsatzdetails, {
Rettungsmittel, Rettungsmittel,
} from "./_components/MissionMarkerTabs"; } from "./_components/MissionMarkerTabs";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getMissionsAPI } from "querys/missions"; import { getMissionsAPI } from "_querys/missions";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { HPGValidationRequired } from "helpers/hpgValidationRequired"; import { HPGValidationRequired } from "_helpers/hpgValidationRequired";
import { getConnectedAircraftsAPI } from "querys/aircrafts"; import { getConnectedAircraftsAPI } from "_querys/aircrafts";
export const MISSION_STATUS_COLORS: Record<MissionState | "attention", string> = { export const MISSION_STATUS_COLORS: Record<MissionState | "attention", string> = {
draft: "#0092b8", draft: "#0092b8",

View File

@@ -4,7 +4,7 @@ import { Fragment, useEffect, useRef } from "react";
import { Marker, Polygon, Popup } from "react-leaflet"; import { Marker, Polygon, Popup } from "react-leaflet";
import L from "leaflet"; import L from "leaflet";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getMissionsAPI } from "querys/missions"; import { getMissionsAPI } from "_querys/missions";
import { OSMWay } from "@repo/db"; import { OSMWay } from "@repo/db";
import { usePannelStore } from "_store/pannelStore"; import { usePannelStore } from "_store/pannelStore";

View File

@@ -13,9 +13,9 @@ import {
} from "@repo/db"; } from "@repo/db";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { editConnectedAircraftAPI } from "querys/aircrafts"; import { editConnectedAircraftAPI } from "_querys/aircrafts";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { cn } from "helpers/cn"; import { cn } from "_helpers/cn";
import { PersonIcon } from "@radix-ui/react-icons"; import { PersonIcon } from "@radix-ui/react-icons";
import { import {
Ban, Ban,
@@ -37,7 +37,7 @@ import {
TextSearch, TextSearch,
} from "lucide-react"; } from "lucide-react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { editMissionAPI, sendSdsMessageAPI } from "querys/missions"; import { editMissionAPI, sendSdsMessageAPI } from "_querys/missions";
const FMSStatusHistory = ({ const FMSStatusHistory = ({
aircraft, aircraft,
@@ -48,25 +48,18 @@ const FMSStatusHistory = ({
}) => { }) => {
console.log("FMSStatusHistory", mission?.missionLog); console.log("FMSStatusHistory", mission?.missionLog);
const log = ((mission?.missionLog as unknown as MissionLog[]) || []) const log = ((mission?.missionLog as unknown as MissionLog[]) || [])
.filter( .filter((entry) => entry.type === "station-log" && entry.data.stationId === aircraft.Station.id)
(entry) =>
entry.type === "station-log" &&
entry.data.stationId === aircraft.Station.id,
)
.reverse() .reverse()
.splice(0, 6) as MissionStationLog[]; .splice(0, 6) as MissionStationLog[];
const aircraftUser = const aircraftUser =
typeof aircraft.publicUser === "string" typeof aircraft.publicUser === "string" ? JSON.parse(aircraft.publicUser) : aircraft.publicUser;
? JSON.parse(aircraft.publicUser)
: aircraft.publicUser;
return ( return (
<div className="p-4"> <div className="p-4">
<ul className="text-base-content font-semibold"> <ul className="text-base-content font-semibold">
<li className="flex items-center gap-2 mb-1"> <li className="flex items-center gap-2 mb-1">
<PersonIcon className="w-5 h-5" /> {aircraftUser.fullName} ( <PersonIcon className="w-5 h-5" /> {aircraftUser.fullName} ({aircraftUser.publicId})
{aircraftUser.publicId})
</li> </li>
</ul> </ul>
<div className="divider mt-0 mb-0" /> <div className="divider mt-0 mb-0" />
@@ -99,8 +92,7 @@ const FMSStatusSelector = ({
}: { }: {
aircraft: ConnectedAircraft & { Station: Station }; aircraft: ConnectedAircraft & { Station: Station };
}) => { }) => {
const dispatcherConnected = const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
useDispatchConnectionStore((s) => s.status) === "connected";
const [hoveredStatus, setHoveredStatus] = useState<string | null>(null); const [hoveredStatus, setHoveredStatus] = useState<string | null>(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const changeAircraftMutation = useMutation({ const changeAircraftMutation = useMutation({
@@ -299,13 +291,7 @@ const SDSTab = ({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const sendSdsMutation = useMutation({ const sendSdsMutation = useMutation({
mutationFn: async ({ mutationFn: async ({ id, message }: { id: number; message: MissionSdsLog }) => {
id,
message,
}: {
id: number;
message: MissionSdsLog;
}) => {
await sendSdsMessageAPI(id, message); await sendSdsMessageAPI(id, message);
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["missions"], queryKey: ["missions"],
@@ -318,9 +304,7 @@ const SDSTab = ({
?.slice() ?.slice()
.reverse() .reverse()
.filter( .filter(
(entry) => (entry) => entry.type === "sds-log" && entry.data.stationId === aircraft.Station.id,
entry.type === "sds-log" &&
entry.data.stationId === aircraft.Station.id,
) || []; ) || [];
return ( return (

View File

@@ -5,13 +5,13 @@ import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { useMapStore } from "_store/mapStore"; import { useMapStore } from "_store/mapStore";
import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_components/map/AircraftMarker"; import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_components/map/AircraftMarker";
import { MISSION_STATUS_COLORS, MISSION_STATUS_TEXT_COLORS } from "_components/map/MissionMarkers"; import { MISSION_STATUS_COLORS, MISSION_STATUS_TEXT_COLORS } from "_components/map/MissionMarkers";
import { cn } from "helpers/cn"; import { cn } from "_helpers/cn";
import { checkSimulatorConnected } from "helpers/simulatorConnected"; import { checkSimulatorConnected } from "_helpers/simulatorConnected";
import { getConnectedAircraftsAPI } from "querys/aircrafts"; import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { getMissionsAPI } from "querys/missions"; import { getMissionsAPI } from "_querys/missions";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useMap } from "react-leaflet"; import { useMap } from "react-leaflet";
import { HPGValidationRequired } from "helpers/hpgValidationRequired"; import { HPGValidationRequired } from "_helpers/hpgValidationRequired";
const PopupContent = ({ const PopupContent = ({
aircrafts, aircrafts,

View File

@@ -32,13 +32,13 @@ import {
import { usePannelStore } from "_store/pannelStore"; import { usePannelStore } from "_store/pannelStore";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { deleteMissionAPI, editMissionAPI, sendMissionAPI } from "querys/missions"; import { deleteMissionAPI, editMissionAPI, sendMissionAPI } from "_querys/missions";
import { getConnectedAircraftsAPI } from "querys/aircrafts"; import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { getStationsAPI } from "querys/stations"; import { getStationsAPI } from "_querys/stations";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { HPGValidationRequired } from "helpers/hpgValidationRequired"; import { HPGValidationRequired } from "_helpers/hpgValidationRequired";
import { getOsmAddress } from "querys/osm"; import { getOsmAddress } from "_querys/osm";
import { hpgStateToFMSStatus } from "helpers/hpgStateToFmsStatus"; import { hpgStateToFMSStatus } from "_helpers/hpgStateToFmsStatus";
const Einsatzdetails = ({ const Einsatzdetails = ({
mission, mission,

View File

@@ -1,12 +1,9 @@
import { ConnectedAircraft, Prisma, Station } from "@repo/db"; import { ConnectedAircraft, Prisma, Station } from "@repo/db";
import axios from "axios"; import axios from "axios";
import { serverApi } from "helpers/axios"; import { serverApi } from "_helpers/axios";
export const getConnectedAircraftsAPI = async () => { export const getConnectedAircraftsAPI = async () => {
const res = const res = await axios.get<(ConnectedAircraft & { Station: Station })[]>("/api/aircrafts"); // return only connected aircrafts
await axios.get<(ConnectedAircraft & { Station: Station })[]>(
"/api/aircrafts",
); // return only connected aircrafts
if (res.status !== 200) { if (res.status !== 200) {
throw new Error("Failed to fetch stations"); throw new Error("Failed to fetch stations");
} }
@@ -17,9 +14,6 @@ export const editConnectedAircraftAPI = async (
id: number, id: number,
mission: Prisma.ConnectedAircraftUpdateInput, mission: Prisma.ConnectedAircraftUpdateInput,
) => { ) => {
const respone = await serverApi.patch<ConnectedAircraft>( const respone = await serverApi.patch<ConnectedAircraft>(`/aircrafts/${id}`, mission);
`/aircrafts/${id}`,
mission,
);
return respone.data; return respone.data;
}; };

View File

@@ -1,6 +1,6 @@
import { Mission, MissionSdsLog, Prisma } from "@repo/db"; import { Mission, MissionSdsLog, Prisma } from "@repo/db";
import axios from "axios"; import axios from "axios";
import { serverApi } from "helpers/axios"; import { serverApi } from "_helpers/axios";
export const getMissionsAPI = async (filter?: Prisma.MissionWhereInput) => { export const getMissionsAPI = async (filter?: Prisma.MissionWhereInput) => {
const res = await axios.get<Mission[]>("/api/missions", { const res = await axios.get<Mission[]>("/api/missions", {

View File

@@ -1,17 +1,11 @@
import { Prisma, Report } from "@repo/db"; import { Prisma, Report } from "@repo/db";
import { serverApi } from "helpers/axios"; import { serverApi } from "_helpers/axios";
export const sendReportAPI = async ( export const sendReportAPI = async (
report: report:
| (Prisma.Without< | (Prisma.Without<Prisma.ReportCreateInput, Prisma.ReportUncheckedCreateInput> &
Prisma.ReportCreateInput,
Prisma.ReportUncheckedCreateInput
> &
Prisma.ReportUncheckedCreateInput) Prisma.ReportUncheckedCreateInput)
| (Prisma.Without< | (Prisma.Without<Prisma.ReportUncheckedCreateInput, Prisma.ReportCreateInput> &
Prisma.ReportUncheckedCreateInput,
Prisma.ReportCreateInput
> &
Prisma.ReportCreateInput), Prisma.ReportCreateInput),
) => { ) => {
const repsonse = await serverApi.put("/report", report); const repsonse = await serverApi.put("/report", report);

View File

@@ -1,13 +1,13 @@
import { PublicUser } from "@repo/db"; import { PublicUser } from "@repo/db";
import { dispatchSocket } from "dispatch/socket"; import { dispatchSocket } from "dispatch/socket";
import { serverApi } from "helpers/axios"; import { serverApi } from "_helpers/axios";
import { import {
handleActiveSpeakerChange, handleActiveSpeakerChange,
handleDisconnect, handleDisconnect,
handleLocalTrackUnpublished, handleLocalTrackUnpublished,
handleTrackSubscribed, handleTrackSubscribed,
handleTrackUnsubscribed, handleTrackUnsubscribed,
} from "helpers/liveKitEventHandler"; } from "_helpers/liveKitEventHandler";
import { ConnectionQuality, Room, RoomEvent } from "livekit-client"; import { ConnectionQuality, Room, RoomEvent } from "livekit-client";
import { pilotSocket } from "pilot/socket"; import { pilotSocket } from "pilot/socket";
import { create } from "zustand"; import { create } from "zustand";

View File

@@ -1,7 +1,4 @@
import { import { AuthOptions, getServerSession as getNextAuthServerSession } from "next-auth";
AuthOptions,
getServerSession as getNextAuthServerSession,
} from "next-auth";
import { PrismaAdapter } from "@next-auth/prisma-adapter"; import { PrismaAdapter } from "@next-auth/prisma-adapter";
import Credentials from "next-auth/providers/credentials"; import Credentials from "next-auth/providers/credentials";
import { prisma, PrismaClient } from "@repo/db"; import { prisma, PrismaClient } from "@repo/db";
@@ -36,10 +33,10 @@ export const options: AuthOptions = {
}, },
}), }),
], ],
secret: process.env.NEXTAUTH_SECRET, secret: process.env.AUTH_DISPATCH_SECRET,
cookies: { cookies: {
sessionToken: { sessionToken: {
name: `${process.env.NEXTAUTH_COOKIE_PREFIX}-next-auth.session-token`, // Ändere den Namen für App 1 name: `${process.env.AUTH_DISPATCH_COOKIE_PREFIX}-next-auth.session-token`, // Ändere den Namen für App 1
options: { options: {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
@@ -47,7 +44,7 @@ export const options: AuthOptions = {
}, },
}, },
csrfToken: { csrfToken: {
name: `${process.env.NEXTAUTH_COOKIE_PREFIX}-next-auth.csrf-token`, name: `${process.env.AUTH_DISPATCH_COOKIE_PREFIX}-next-auth.csrf-token`,
options: { options: {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",

View File

@@ -15,13 +15,13 @@ import {
editMissionAPI, editMissionAPI,
sendMissionAPI, sendMissionAPI,
startHpgValidation, startHpgValidation,
} from "querys/missions"; } from "_querys/missions";
import { getKeywordsAPI } from "querys/keywords"; import { getKeywordsAPI } from "_querys/keywords";
import { getStationsAPI } from "querys/stations"; import { getStationsAPI } from "_querys/stations";
import { useMapStore } from "_store/mapStore"; import { useMapStore } from "_store/mapStore";
import { getConnectedAircraftsAPI } from "querys/aircrafts"; import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { HPGValidationRequired } from "helpers/hpgValidationRequired"; import { HPGValidationRequired } from "_helpers/hpgValidationRequired";
import { selectRandomHPGMissionSzenery } from "helpers/selectRandomHPGMission"; import { selectRandomHPGMissionSzenery } from "_helpers/selectRandomHPGMission";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
export const MissionForm = () => { export const MissionForm = () => {

View File

@@ -1,15 +1,14 @@
import { usePannelStore } from "_store/pannelStore"; import { usePannelStore } from "_store/pannelStore";
import { cn } from "helpers/cn"; import { cn } from "_helpers/cn";
import { MissionForm } from "./MissionForm"; import { MissionForm } from "./MissionForm";
import { Rss, Trash2Icon } from "lucide-react"; import { Rss, Trash2Icon } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getMissionsAPI } from "querys/missions"; import { getMissionsAPI } from "_querys/missions";
export const Pannel = () => { export const Pannel = () => {
const { setOpen, setMissionFormValues } = usePannelStore(); const { setOpen, setMissionFormValues } = usePannelStore();
const { isEditingMission, setEditingMission, missionFormValues } = const { isEditingMission, setEditingMission, missionFormValues } = usePannelStore();
usePannelStore();
const missions = useQuery({ const missions = useQuery({
queryKey: ["missions"], queryKey: ["missions"],
queryFn: () => queryFn: () =>
@@ -20,9 +19,7 @@ export const Pannel = () => {
useEffect(() => { useEffect(() => {
if (isEditingMission && missionFormValues) { if (isEditingMission && missionFormValues) {
const mission = missions.data?.find( const mission = missions.data?.find((mission) => mission.id === missionFormValues.id);
(mission) => mission.id === missionFormValues.id,
);
if (!mission) { if (!mission) {
setEditingMission(false, null); setEditingMission(false, null);
setMissionFormValues({}); setMissionFormValues({});

View File

@@ -2,7 +2,7 @@
import { Pannel } from "dispatch/_components/pannel/Pannel"; import { Pannel } from "dispatch/_components/pannel/Pannel";
import { usePannelStore } from "_store/pannelStore"; import { usePannelStore } from "_store/pannelStore";
import { cn } from "helpers/cn"; import { cn } from "_helpers/cn";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Chat } from "../_components/left/Chat"; import { Chat } from "../_components/left/Chat";
import { Report } from "../_components/left/Report"; import { Report } from "../_components/left/Report";

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { io } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import type { Socket } from "socket.io-client"; console.log("ENV:", process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL);
export const dispatchSocket: Socket = io(process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL, { export const dispatchSocket: Socket = io(process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL, {
autoConnect: false, autoConnect: false,

View File

@@ -2,21 +2,18 @@ import { ConnectedAircraft } from "@repo/db";
import { usePilotConnectionStore } from "_store/pilot/connectionStore"; import { usePilotConnectionStore } from "_store/pilot/connectionStore";
import { useMrtStore } from "_store/pilot/MrtStore"; import { useMrtStore } from "_store/pilot/MrtStore";
import { pilotSocket } from "pilot/socket"; import { pilotSocket } from "pilot/socket";
import { editConnectedAircraftAPI } from "querys/aircrafts"; import { editConnectedAircraftAPI } from "_querys/aircrafts";
import { useEffect } from "react"; import { useEffect } from "react";
export const useButtons = () => { export const useButtons = () => {
const station = usePilotConnectionStore((state) => state.selectedStation); const station = usePilotConnectionStore((state) => state.selectedStation);
const connectedAircraft = usePilotConnectionStore( const connectedAircraft = usePilotConnectionStore((state) => state.connectedAircraft);
(state) => state.connectedAircraft,
);
const connectionStatus = usePilotConnectionStore((state) => state.status); const connectionStatus = usePilotConnectionStore((state) => state.status);
const { page, setPage } = useMrtStore((state) => state); const { page, setPage } = useMrtStore((state) => state);
const handleButton = const handleButton =
(button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0") => (button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0") => () => {
() => {
if (connectionStatus !== "connected") return; if (connectionStatus !== "connected") return;
if (!station) return; if (!station) return;
if (!connectedAircraft?.id) return; if (!connectedAircraft?.id) return;

View File

@@ -3,7 +3,7 @@ import { useSession } from "next-auth/react";
import { usePilotConnectionStore } from "_store/pilot/connectionStore"; import { usePilotConnectionStore } from "_store/pilot/connectionStore";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getStationsAPI } from "querys/stations"; import { getStationsAPI } from "_querys/stations";
export const ConnectionBtn = () => { export const ConnectionBtn = () => {
const modalRef = useRef<HTMLDialogElement>(null); const modalRef = useRef<HTMLDialogElement>(null);

View File

@@ -1,5 +1,4 @@
"use client"; "use client";
import { io, Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
export const pilotSocket: Socket = io(process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL, { export const pilotSocket: Socket = io(process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL, {

View File

@@ -1,6 +1,6 @@
NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_COOKIE_PREFIX=HUB AUTH_HUB_COOKIE_PREFIX=HUB
NEXTAUTH_SECRET=var AUTH_HUB_SECRET=var
DATABASE_URL=postgresql://persistant-data:persistant-data-pw@localhost:5432/var DATABASE_URL=postgresql://persistant-data:persistant-data-pw@localhost:5432/var
DISCORD_OAUTH_CLIENT_ID= DISCORD_OAUTH_CLIENT_ID=
DISCORD_OAUTH_SECRET= DISCORD_OAUTH_SECRET=

View File

@@ -1,12 +1,12 @@
FROM node:22-alpine AS base FROM node:22-alpine AS base
ARG DATABASE_URL
ENV DATABASE_URL=${DATABASE_URL}
ENV PNPM_HOME="/usr/local/pnpm" ENV PNPM_HOME="/usr/local/pnpm"
ENV PATH="${PNPM_HOME}:${PATH}" ENV PATH="${PNPM_HOME}:${PATH}"
RUN corepack enable && corepack prepare pnpm@latest --activate RUN corepack enable && corepack prepare pnpm@latest --activate
RUN echo "NEXT_PUBLIC_DISPATCH_SERVER_URL=${NEXT_PUBLIC_DISPATCH_SERVER_URL}"
RUN pnpm add -g turbo@^2.5 RUN pnpm add -g turbo@^2.5
FROM base AS builder FROM base AS builder

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { signIn, useSession } from "next-auth/react"; import { signIn } from "next-auth/react";
import Link from "next/link"; import Link from "next/link";
import { redirect, useSearchParams } from "next/navigation"; import { redirect, useSearchParams } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@@ -72,12 +72,7 @@ export const Login = () => {
<path d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" /> <path d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" />
<path d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" /> <path d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
</svg> </svg>
<input <input type="text" className="grow" {...form.register("email")} placeholder="Email" />
type="text"
className="grow"
{...form.register("email")}
placeholder="Email"
/>
</label> </label>
<p className="text-error"> <p className="text-error">
{typeof form.formState.errors.email?.message === "string" {typeof form.formState.errors.email?.message === "string"
@@ -111,11 +106,7 @@ export const Login = () => {
</Link> </Link>
</span> </span>
<div className="card-actions mt-6"> <div className="card-actions mt-6">
<Button <Button disabled={isLoading} isLoading={isLoading} className="btn btn-primary btn-block">
disabled={isLoading}
isLoading={isLoading}
className="btn btn-primary btn-block"
>
Login Login
</Button> </Button>
</div> </div>

View File

@@ -70,21 +70,14 @@ export const VerticalNav = () => {
export const HorizontalNav = () => ( export const HorizontalNav = () => (
<div className="navbar bg-base-200 shadow-md rounded-lg mb-4"> <div className="navbar bg-base-200 shadow-md rounded-lg mb-4">
<div className="flex items-center"> <div className="flex items-center">
<a className="btn btn-ghost normal-case text-xl"> <a className="btn btn-ghost normal-case text-xl">Virtual Air Rescue - HUB</a>
Virtual Air Rescue - HUB
</a>
<WarningAlert /> <WarningAlert />
</div> </div>
<div className="flex items-center ml-auto"> <div className="flex items-center ml-auto">
<ul className="flex space-x-2 px-1"> <ul className="flex space-x-2 px-1">
<li> <li>
<Link <Link href={process.env.NEXT_PUBLIC_DISPATCH_URL || "#!"} rel="noopener noreferrer">
href={process.env.NEXT_PUBLIC_DISPATCH_URL || "#!"} <button className="btn btn-sm btn-outline btn-primary">Zur Leitstelle</button>
rel="noopener noreferrer"
>
<button className="btn btn-sm btn-outline btn-primary">
Zur Leitstelle
</button>
</Link> </Link>
</li> </li>
<li> <li>

View File

@@ -27,14 +27,14 @@ export const options: AuthOptions = {
}, },
}), }),
], ],
secret: process.env.NEXTAUTH_SECRET, secret: process.env.AUTH_HUB_SECRET,
session: { session: {
strategy: "jwt", strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, maxAge: 30 * 24 * 60 * 60,
}, },
cookies: { cookies: {
sessionToken: { sessionToken: {
name: `${process.env.NEXTAUTH_COOKIE_PREFIX}-next-auth.session-token`, // Ändere den Namen für App 1 name: `${process.env.AUTH_HUB_COOKIE_PREFIX}-next-auth.session-token`, // Ändere den Namen für App 1
options: { options: {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
@@ -42,7 +42,7 @@ export const options: AuthOptions = {
}, },
}, },
csrfToken: { csrfToken: {
name: `${process.env.NEXTAUTH_COOKIE_PREFIX}-next-auth.csrf-token`, name: `${process.env.AUTH_HUB_COOKIE_PREFIX}-next-auth.csrf-token`,
options: { options: {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",

View File

@@ -60,7 +60,7 @@ export const POST = async (req: NextRequest) => {
{ {
...accessRequest.user, ...accessRequest.user,
}, },
process.env.NEXTAUTH_SECRET as string, process.env.AUTH_HUB_SECRET as string,
{ {
expiresIn: "30d", expiresIn: "30d",
}, },

View File

@@ -11,7 +11,7 @@ export const GET = async (req: NextRequest) => {
if (!authHeader || !token) { if (!authHeader || !token) {
return NextResponse.json({ error: "Not logged in" }, { status: 401 }); return NextResponse.json({ error: "Not logged in" }, { status: 401 });
} }
const decoded = await verify(token, process.env.NEXTAUTH_SECRET as string); const decoded = await verify(token, process.env.AUTH_HUB_SECRET as string);
if (typeof decoded === "string") if (typeof decoded === "string")
return NextResponse.json({ error: "Invalid token" }, { status: 401 }); return NextResponse.json({ error: "Invalid token" }, { status: 401 });
@@ -22,8 +22,7 @@ export const GET = async (req: NextRequest) => {
}, },
}); });
if (!user) if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 });
return NextResponse.json({ error: "User not found" }, { status: 404 });
setTimeout(async () => { setTimeout(async () => {
const moodleUser = await getMoodleUserById(user.id); const moodleUser = await getMoodleUserById(user.id);
await prisma.user.update({ await prisma.user.update({
@@ -49,10 +48,7 @@ export const GET = async (req: NextRequest) => {
}, },
}); });
participatingEvents.forEach(async (p) => { participatingEvents.forEach(async (p) => {
await inscribeToMoodleCourse( await inscribeToMoodleCourse(p.Event.finisherMoodleCourseId!, moodleUser?.id);
p.Event.finisherMoodleCourseId!,
moodleUser?.id,
);
}); });
}, 1000); }, 1000);

View File

@@ -13,7 +13,7 @@ const geistMono = Geist_Mono({
subsets: ["latin"], subsets: ["latin"],
}); });
export default async ({ const RootLayout = async ({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
@@ -23,12 +23,14 @@ export default async ({
return ( return (
<html lang="en"> <html lang="en">
<NextAuthSessionProvider session={session}> <NextAuthSessionProvider session={session}>
<body <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children} {children}
</body> </body>
</NextAuthSessionProvider> </NextAuthSessionProvider>
</html> </html>
); );
}; };
RootLayout.displayName = "RootLayout";
export default RootLayout;

View File

@@ -3,6 +3,12 @@ services:
build: build:
context: . context: .
dockerfile: ./apps/dispatch/Dockerfile dockerfile: ./apps/dispatch/Dockerfile
args:
- NEXT_PUBLIC_DISPATCH_URL=$NEXT_PUBLIC_DISPATCH_URL
- NEXT_PUBLIC_HUB_URL=$NEXT_PUBLIC_HUB_URL
- NEXT_PUBLIC_DISPATCH_SERVICE_ID=1
- NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL
- NEXT_PUBLIC_DISPATCH_SERVER_URL=$NEXT_PUBLIC_DISPATCH_SERVER_URL
container_name: dispatch container_name: dispatch
ports: ports:
- "3001:3000" - "3001:3000"
@@ -41,14 +47,17 @@ services:
dockerfile: ./apps/dispatch-server/Dockerfile dockerfile: ./apps/dispatch-server/Dockerfile
container_name: dispatch-server container_name: dispatch-server
ports: ports:
- "3003:3000" - "3002:3000"
env_file: env_file:
- .env.prod - .env.prod
networks: networks:
- postgres_network - postgres_network
- redis_network
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
redis:
condition: service_healthy
postgres: postgres:
image: postgres:13 image: postgres:13
container_name: postgres container_name: postgres
@@ -84,6 +93,10 @@ services:
- "6379:6379" - "6379:6379"
volumes: volumes:
- "redis_data:/data" - "redis_data:/data"
networks:
- redis_network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
moodle_database: moodle_database:
container_name: moodle_database container_name: moodle_database
@@ -141,6 +154,8 @@ networks:
postgres_network: postgres_network:
driver: bridge driver: bridge
redis_network:
driver: bridge
volumes: volumes:
postgres-data: postgres-data:

View File

@@ -6,11 +6,11 @@
"EMAIL_FROM", "EMAIL_FROM",
"SECRET", "SECRET",
"DATABASE_URL", "DATABASE_URL",
"NEXTAUTH_SECRET", "AUTH_DISPATCH_SECRET",
"LIVEKIT_API_KEY", "LIVEKIT_API_KEY",
"LIVEKIT_API_SECRET", "LIVEKIT_API_SECRET",
"NEXTAUTH_HUB_SECRET", "NEXTAUTH_HUB_SECRET",
"NEXTAUTH_COOKIE_PREFIX" "AUTH_DISPATCH_COOKIE_PREFIX"
], ],
"ui": "tui", "ui": "tui",
"tasks": { "tasks": {