diff --git a/Caddyfile b/Caddyfile index b2fc8c5b..95d620d1 100644 --- a/Caddyfile +++ b/Caddyfile @@ -5,33 +5,35 @@ (production) { root * app/dist # will be used as the start directory for the ditrectives below try_files {path} /index.html # catch all (non existent files) redirect for SPA frontend - file_server # serve the files! + file_server # serve the app files! } -(production_tls) { - # log -} +# (online_tls) { +# # log +# } -(development_tls) { - tls ./certs/localhost+2.pem ./certs/localhost+2-key.pem -} +# (local_tls) { +# tls ./certs/localhost+2.pem ./certs/localhost+2-key.pem +# } # @isDev expression `{$ENVIRONMENT} == development` {$EXPOSED_SERVER_URL} { - import `{$ENVIRONMENT:production}_tls` + # This does not work currently because the variable is a true or false rather than an actual value + # import `{$EXPOSED_LOCAL:online}_tls` + # tls ./certs/localhost+2.pem ./certs/localhost+2-key.pem # Important to use the route directive so that the reverse_proxy is matched first # (default is try_files before reverse_proxy) route { handle_path /auth* { - reverse_proxy localhost:{$AUTH_PORT} + reverse_proxy localhost:{$EXPOSED_AUTH_PORT} } handle_path /socket* { - reverse_proxy localhost:{$MEDIASOUP_PORT} + reverse_proxy localhost:{$EXPOSED_MEDIASOUP_PORT} } handle_path /files* { - reverse_proxy localhost:{$FILESERVER_PORT} + reverse_proxy localhost:{$EXPOSED_FILESERVER_PORT} } import {$ENVIRONMENT:production} diff --git a/app/src/modules/authClient.ts b/app/src/modules/authClient.ts index ee1090d8..7f748a48 100644 --- a/app/src/modules/authClient.ts +++ b/app/src/modules/authClient.ts @@ -4,8 +4,18 @@ import type { JwtPayload, JwtUserData, UserRole } from 'schemas'; import type { User } from 'database/schema'; import decodeJwt from 'jwt-decode'; -const completeAuthUrl = `https://${import.meta.env.EXPOSED_SERVER_URL}${import.meta.env.EXPOSED_AUTH_PATH}`; -// console.log('authUrl: ', completeAuthUrl); +const devMode = import.meta.env.DEV; +const localMode = import.meta.env.EXPOSED_LOCAL === 'true'; +console.log('localMode:', localMode); + +let completeAuthUrl: string; +if (localMode) { + completeAuthUrl = `http://${import.meta.env.EXPOSED_SERVER_URL}:${import.meta.env.EXPOSED_AUTH_PORT}`; +} else { + completeAuthUrl = `https://${import.meta.env.EXPOSED_SERVER_URL}${import.meta.env.EXPOSED_AUTH_PATH}`; +} + +console.log('authUrl: ', completeAuthUrl); const authEndpoint = axios.create({ baseURL: completeAuthUrl, withCredentials: true }); export function createUser(username: string, password: string, role: UserRole) { @@ -37,7 +47,7 @@ export function updateUser(userData: {userId: string, username?: string, passwor } export function deleteUser(userId: string) { - return handleResponse(() => authEndpoint.post('/user/delete', {userId})); + return handleResponse(() => authEndpoint.post('/user/delete', { userId })); } type FetchedUsers = Omit[] @@ -186,15 +196,15 @@ export const loginWithAutoToken = async (username: string, password: string) => export const guestJwt = (params?: {requestedUsername?: string, previousToken?: string}) => { if(params?.previousToken){ - return handleResponse(() =>authEndpoint.get(`/guest-jwt?prevToken=${params.previousToken}`)); + return handleResponse(() => authEndpoint.get(`/guest-jwt?prevToken=${params.previousToken}`)); } if(params?.requestedUsername){ - return handleResponse(() =>authEndpoint.get(`/guest-jwt?username=${params.requestedUsername}`)); + return handleResponse(() => authEndpoint.get(`/guest-jwt?username=${params.requestedUsername}`)); } - return handleResponse(() =>authEndpoint.get('/guest-jwt')); + return handleResponse(() => authEndpoint.get('/guest-jwt')); }; export const getJwt = () => handleResponse(() => authEndpoint.get('user/jwt')); -export const getMe = () => handleResponse(() => authEndpoint.get('/user/me')); +export const getMe = () => handleResponse(() => authEndpoint.get('user/me')); export const logout = () => { clearTimeout(activeTimeout); return handleResponse(() => authEndpoint.get('user/logout')); diff --git a/app/src/modules/trpcClient.ts b/app/src/modules/trpcClient.ts index bf373f2e..bd69bae1 100644 --- a/app/src/modules/trpcClient.ts +++ b/app/src/modules/trpcClient.ts @@ -13,8 +13,12 @@ import { createReceiver } from 'ts-event-bridge/receiver' import { shallowRef, type ShallowRef, computed, type ComputedRef } from 'vue'; import type { Payload } from 'ts-event-bridge/sender'; -const wsBaseURL = `wss://${import.meta.env.EXPOSED_SERVER_URL}${import.meta.env.EXPOSED_MEDIASOUP_PATH}`; +const localMode = import.meta.env.EXPOSED_LOCAL === 'true'; +let wsBaseURL = `wss://${import.meta.env.EXPOSED_SERVER_URL}${import.meta.env.EXPOSED_MEDIASOUP_PATH}`; +if (localMode) { + wsBaseURL = `ws://${import.meta.env.EXPOSED_SERVER_URL}:${import.meta.env.EXPOSED_MEDIASOUP_PORT}`; +} const { receiver, onMessageReceived } = createReceiver({ onUnhandledEvent(evt, msg) { console.warn('unhandled event: ', evt, msg); diff --git a/app/src/modules/utils.ts b/app/src/modules/utils.ts index 8564561f..3d6dd623 100644 --- a/app/src/modules/utils.ts +++ b/app/src/modules/utils.ts @@ -2,17 +2,28 @@ import axios, { CanceledError, type AxiosProgressEvent } from "axios"; import type { UploadResponse } from "fileserver"; import type { AssetId } from "schemas"; +const devMode = import.meta.env.DEV; +const localMode = import.meta.env.EXPOSED_LOCAL === 'true'; + export function getAssetUrl(generatedName: T) { // console.log('getAssetUrl called', generatedName); // if (generatedName === undefined) { // return generatedName // } - return `https://${import.meta.env.EXPOSED_SERVER_URL}${import.meta.env.EXPOSED_FILESERVER_PATH}/file/${generatedName}`; - // return `https://${process.env.EXPOSED_SERVER_URL}${process.env.EXPOSED_FILESERVER_PATH}/files/${generatedName}`; + if (localMode) { + return `http://${import.meta.env.EXPOSED_SERVER_URL}:${import.meta.env.EXPOSED_FILESERVER_PORT}/file/${generatedName}`; + } else { + return `https://${import.meta.env.EXPOSED_SERVER_URL}${import.meta.env.EXPOSED_FILESERVER_PATH}/file/${generatedName}`; + } } -const fileserverUrl = `https://${import.meta.env.EXPOSED_SERVER_URL}${import.meta.env.EXPOSED_FILESERVER_PATH}` as const +let fileserverUrl: string; +if (localMode) { + fileserverUrl = `http://${import.meta.env.EXPOSED_SERVER_URL}:${import.meta.env.EXPOSED_FILESERVER_PORT}`; +} else { + fileserverUrl = `https://${import.meta.env.EXPOSED_SERVER_URL}${import.meta.env.EXPOSED_FILESERVER_PATH}`; +} export const assetsUrl = `${fileserverUrl}/file/` as const; export async function uploadFileData({ data, authToken, onProgress, abortController }: { data: FormData, authToken: string, onProgress?: (progressEvent: AxiosProgressEvent) => void, abortController?: AbortController }) { // We can apparently receive upload progress after the upload is actually finished. diff --git a/app/src/views/admin/AdminUserManagerView.vue b/app/src/views/admin/AdminUserManagerView.vue index 10fd4dbc..8cdd6e20 100644 --- a/app/src/views/admin/AdminUserManagerView.vue +++ b/app/src/views/admin/AdminUserManagerView.vue @@ -39,6 +39,9 @@ +
+ {{ responseMessage }} +

Befintliga användare

@@ -92,7 +95,7 @@ class="btn"> edit - @@ -143,6 +146,10 @@ function getClassForRole(role: UserRole) { } } +const responseMessage = ref(); +const responseType = ref<'alert-success' | 'alert-error'>('alert-success'); +const messageTimeoutId = ref(null); + const createdUsername = ref(''); const createdPassword = ref(''); const createdRole = ref('guest'); @@ -209,12 +216,41 @@ onBeforeMount(async () => { console.log(fetchedUsers.value); }); -async function makeCallThenResetList(fetchReq: (...p: any) => Promise) { - await fetchReq(); - editedUserId.value = undefined; - createdUsername.value = ''; - createdPassword.value = ''; - fetchedUsers.value = await getUsers(); +async function makeCallThenResetList( + fetchReq: (...p: any) => Promise, + showMessageBool: boolean = true) { + try { + await fetchReq(); + + if (showMessageBool) { + showMessage(`Användare "${createdUsername.value}" skapad!`, 'alert-success'); + } + createdUsername.value = ''; + createdPassword.value = ''; + createdRole.value = 'guest'; + } catch (e: any) { + if (e.message === 'username already exists') { + showMessage('Användarnamnet finns redan.', 'alert-error'); + } else { + showMessage(`Fel: ${e.message || 'Okänt fel vid skapande av användare'}`, 'alert-error'); + } + } finally { + fetchedUsers.value = await getUsers(); + editedUserId.value = undefined; + } +} + +function showMessage(msg: string, type: 'alert-success' | 'alert-error') { + responseMessage.value = msg; + responseType.value = type; + + if (messageTimeoutId.value) { + clearTimeout(messageTimeoutId.value); + } + + messageTimeoutId.value = setTimeout(() => { + responseMessage.value = ''; + }, 5000); } diff --git a/backend/auth/src/index.ts b/backend/auth/src/index.ts index fcc84896..e1bee183 100644 --- a/backend/auth/src/index.ts +++ b/backend/auth/src/index.ts @@ -21,6 +21,7 @@ const haikunator = new Haikunator({ // console.log('environment: ', process.env); const devMode = process.env.DEVELOPMENT; +const localMode = process.env.EXPOSED_LOCAL === 'true'; const app = express(); @@ -32,11 +33,11 @@ app.set('trust proxy', 1); // Find a way to make our auth flow work without this set to false let cookieHttpOnly = false; let cookieSecure = true; -if (devMode) { +let originUrls: string[] = []; +if (localMode) { // NOTE: I couldnt come up with a way to allow all origins so we have hardcoded the devservers url here - const devServerUrl = 'http://localhost:5173'; - console.log('allowing cors for development: ', devServerUrl); - app.use(cors({ credentials: true, origin: [devServerUrl] })); + originUrls = ['http://localhost:5173', 'http://127.0.0.1:5173']; + console.log('allowing cors for local development: ', originUrls); console.log('allowing cookie despite not https'); cookieHttpOnly = false; @@ -46,13 +47,12 @@ if (devMode) { console.error('no EXPOSED_SERVER_URL provided from env'); process.exit(1); } - console.log('restricting CORS for production'); - app.use(cors({ - origin: [process.env.EXPOSED_SERVER_URL], - credentials: true - })); + originUrls = [`https://${process.env.EXPOSED_SERVER_URL}`]; + console.log('restricting CORS for https:', originUrls); } +app.use(cors({ credentials: true, origin: originUrls })); + app.use((req, res, next) => { return parseJsonBody()(req, res, (err) => { if (err) { @@ -167,7 +167,7 @@ app.get('/guest-jwt', (req, res) => { }); -const port = Number.parseInt(process.env.AUTH_PORT || '3333'); +const port = Number.parseInt(process.env.EXPOSED_AUTH_PORT || '3333'); app.listen(port, () => { console.log(`listening on ${port}`); if (process.env.DEVELOPMENT) diff --git a/backend/auth/src/userRoutes.ts b/backend/auth/src/userRoutes.ts index 2e8453f7..801f3b21 100644 --- a/backend/auth/src/userRoutes.ts +++ b/backend/auth/src/userRoutes.ts @@ -90,6 +90,12 @@ const createUser: RequestHandler = async (req: CreateUserRequest, res) => { res.status(201).send(dbResponse); return; } catch (e) { + //Checking if error is from unique constraint violation on username + if (typeof e === 'object' && e !== null && 'code' in e && (e as { code?: string }).code === '23505') { + res.status(409).send('username already exists'); + return; + } + console.error('database error when creating user'); console.error(e); res.status(501).send(e); @@ -105,6 +111,7 @@ interface CreateSenderRequest extends ExpressReq { streamId?: StreamId, } } + const createSenderForVenue: RequestHandler = async (req : CreateSenderRequest, res) => { const userData = req.session.user // console.log(userData); @@ -147,6 +154,7 @@ interface UpdateUserRequest extends ExpressReq { password?: string, } } + const updateUser: RequestHandler = async (req: UpdateUserRequest, res) => { const userData = req.session.user; const payload = req.body; @@ -206,6 +214,7 @@ interface DeleteUserRequest extends ExpressReq { userId: UserId } } + const deleteUser: RequestHandler = async (req: DeleteUserRequest, res) => { try { const userData = req.session.user; @@ -397,7 +406,7 @@ const loginUser: RequestHandler = async (req, res) => { where: eq(schema.users.username, username), }) if (!foundUser) { - throw new Error('no user with that username found!'); + throw new Error('no user with that username and password found!'); } const correct = await bcrypt.compare(password, foundUser.password); if (correct) { @@ -406,14 +415,18 @@ const loginUser: RequestHandler = async (req, res) => { req.session.user = pickedUserData; res.status(200).send(); return; + } else { + throw new Error('no user with that username and password found!'); } } catch (e) { console.error(e); - res.status(501).send('failed when trying to login'); + if (e instanceof Error && e.message === 'no user with that username and password found!') { + res.status(401).send('User with that username or password not found'); + } else { + res.status(501).send('failed when trying to login'); + } return; } - res.status(403).send('You shall not pass!'); - return; }; diff --git a/backend/fileserver/src/index.ts b/backend/fileserver/src/index.ts index a4511688..9dc36817 100644 --- a/backend/fileserver/src/index.ts +++ b/backend/fileserver/src/index.ts @@ -3,6 +3,7 @@ import { HttpStatus } from 'http-status-ts'; import { serveStatic } from '@hono/node-server/serve-static' import { zValidator } from '@hono/zod-validator'; import { Hono } from 'hono' +import { cors } from 'hono/cors' // import { logger } from 'hono/logger'; // import { createMiddleware } from 'hono/factory' import { HTTPException } from 'hono/http-exception' @@ -13,6 +14,7 @@ import { randomUUID } from 'crypto' import { Stream, pipeline } from 'node:stream'; import type { ReadableStream } from 'node:stream/web'; import fs from 'fs' +import promise from 'fs/promises'; import { hc, InferRequestType, InferResponseType } from 'hono/client' @@ -29,6 +31,10 @@ import sharp from 'sharp'; const savePathAbsolute = path.resolve('.', 'uploads') const savePathRelative = './uploads/' +const devMode = process.env.DEVELOPMENT === 'true'; +const localMode = process.env.EXPOSED_LOCAL === 'true'; + +await promise.mkdir(savePathAbsolute, { recursive: true }); // const authHandler = basicAuth({ username: 'gunnar', password: 'hemligt' }) // const jwtHandler = jwt({ secret: 'secret' }); @@ -83,6 +89,9 @@ publicRoutes.get('/file/:filename', root: savePathRelative, }), ) + .get('/test', (c) => { + return c.text('test', HttpStatus.OK); + }); const privateRoutes = new Hono<{ Variables: { jwtPayload: JwtPayload } }>() .use((c, next) => { @@ -202,11 +211,30 @@ const privateRoutes = new Hono<{ Variables: { jwtPayload: JwtPayload } }>() return c.json(dbResponse, HttpStatus.OK); }); -const app = new Hono<{ Variables: { jwtPayload: JwtPayload } }>() - .route('/', publicRoutes) - .route('/', privateRoutes); +const app = new Hono<{ Variables: { jwtPayload: JwtPayload } }>(); + +if (localMode) { + app.use( + '*', + cors({ + origin: ['http://localhost:5173', 'http://127.0.0.1:5173'], + credentials: true, + }) + ); +} else { + app.use( + '*', + cors({ + origin: [`https://${process.env.EXPOSED_SERVER_URL}`], + credentials: true + }) + ) +} + +app.route('/', publicRoutes) +app.route('/', privateRoutes); -const port = Number.parseInt(process.env.FILESERVER_PORT ?? '3000'); +const port = Number.parseInt(process.env.EXPOSED_FILESERVER_PORT ?? '3000'); console.log(`Server is running on port ${port}`) serve({ diff --git a/backend/fileserver/src/index_old.ts b/backend/fileserver/src/index_old.ts index 3a7f93c1..fcbfac20 100644 --- a/backend/fileserver/src/index_old.ts +++ b/backend/fileserver/src/index_old.ts @@ -182,5 +182,5 @@ app.post('/remove', (req,res) => { }) // Run Express app -const port = Number.parseInt(process.env.FILESERVER_PORT || '9002') +const port = Number.parseInt(process.env.EXPOSED_FILESERVER_PORT || '9002') app.listen(port, () => console.log('Application listening on port ' + port)) diff --git a/backend/mediaserver/src/index.ts b/backend/mediaserver/src/index.ts index d68502c2..472902fc 100644 --- a/backend/mediaserver/src/index.ts +++ b/backend/mediaserver/src/index.ts @@ -247,7 +247,7 @@ app.ws('/*', { onSocketClose(ws, msgString); } -}).listen(Number.parseInt(process.env.MEDIASOUP_PORT || '9001'), (listenSocket) => { +}).listen(Number.parseInt(process.env.EXPOSED_MEDIASOUP_PORT || '9001'), (listenSocket) => { if (listenSocket) { console.log('listenSocket:' ,listenSocket); console.log('Listening to port 9001'); diff --git a/example.env b/example.env index b45c1d8b..3820ea6e 100644 --- a/example.env +++ b/example.env @@ -9,7 +9,16 @@ # ########################### -EXPOSED_PROJECT_NAME="samvr" +# possible levels from less to more logging: TRACE, DEBUG, INFO, WARN, ERROR, FATAL and OFF +# DEBUG_LEVEL="DEBUG" + +# If doing local development (localhost), set this to true. +# Local dev uses localhost and ports instead of paths to construct the urls. +# set to false in server environments with reverse proxy and https +EXPOSED_LOCAL="false" + + +EXPOSED_PROJECT_NAME="SamVR" EXPOSED_SERVER_URL="example.com" @@ -20,13 +29,13 @@ LISTEN_IP="123.123.123.123" # If this isnt provided the server will try to get it's own local IP using the node module called "ip" INTERNAL_IP="456.456.456.456" -AUTH_PORT="3333" +EXPOSED_AUTH_PORT="3333" EXPOSED_AUTH_PATH="/auth" -MEDIASOUP_PORT="9001" +EXPOSED_MEDIASOUP_PORT="9001" EXPOSED_MEDIASOUP_PATH="/socket" -FILESERVER_PORT="9002" +EXPOSED_FILESERVER_PORT="9002" EXPOSED_FILESERVER_PATH='/files' # Be aware! The database variables are used on INITIAL STARTUP of the database container only. So change these to appropriate values BEFORE creating the database container.