diff --git a/backend/.gitignore b/backend/.gitignore index a2e08b2..ed22581 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -14,3 +14,5 @@ logs .env .env.* !.env.production + +uploads/ \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js index 99a0467..b9965de 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -2,6 +2,7 @@ const express = require("express"); const cors = require('cors'); const cookieParser = require('cookie-parser'); const passport = require('passport'); +const path = require('path'); const dotenv = require('dotenv'); @@ -16,6 +17,11 @@ if(process.env.NODE_ENV) { const app = express(); const connectDB = require("./db"); + +// PUBLIC +const uploadDir = path.join(__dirname, 'uploads'); +app.use('/public', express.static(uploadDir)); + // JSON LIMIT EXPRESS app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ @@ -26,6 +32,7 @@ app.use(express.urlencoded({ // connect database connectDB(); +// MIDDLEWARE app.use(passport.initialize()); require('./services/passport')(passport); diff --git a/backend/src/routes/user.js b/backend/src/routes/user.js index 3c1a548..5b04b5d 100644 --- a/backend/src/routes/user.js +++ b/backend/src/routes/user.js @@ -158,7 +158,7 @@ router.post("/upload-avatar", upload.single("image"), passport.authenticate('jwt router.get("/retrieve-avatar", async (req, res) => { try { const data = await User.findOne({ username: req.query.username }); - res.json({ status: "ok", image: data.image }); + res.json({ status: "ok", image: `${data.image}` }); } catch (err) { res.json({ status: "error" }); } diff --git a/backend/src/services/storage.js b/backend/src/services/storage.js index 1a2a074..79a4a8e 100644 --- a/backend/src/services/storage.js +++ b/backend/src/services/storage.js @@ -1,14 +1,22 @@ const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); + +const uploadDir = path.join(__dirname, '..', 'uploads'); // adjust if needed + +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); +} var storage = multer.diskStorage({ destination: function (req, file, cb) { - cb(null, 'uploads') + cb(null, uploadDir); }, filename: function (req, file, cb) { - cb(null, file.fieldname + '-' + Date.now()) + const ext = path.extname(file.originalname); + cb(null, file.fieldname + '-' + Date.now() + ext); } }); var upload = multer({storage: storage}); -module.exports = upload; - \ No newline at end of file +module.exports = upload; \ No newline at end of file diff --git a/frontend/app/app.vue b/frontend/app/app.vue index 3d15c55..665ffbe 100644 --- a/frontend/app/app.vue +++ b/frontend/app/app.vue @@ -4,13 +4,31 @@ import ToastManager from './components/managers/ToastManager.vue'; import WindowManager from './components/managers/WindowManager.vue'; import { CreateWindow } from '@/services/Windows' +import { GetUser, HasAdmin } from './services/User'; async function start(){ - CreateWindow('login'); + if(GetUser()){ + CreateWindow('main_menu'); + return; + } + if(await HasAdmin()){ + CreateWindow('login'); + } else { + CreateWindow('register', {firstTime: true}); + } // DisplayToast('aqua', 'All plugins loaded successfully'); } +useHead({ + title: 'Dragonroll', + meta: [ + { name: 'description', content: 'Dragonroll is a free and open-source tabletop RPG virtual tabletop. It allows you to play your favorite pen-and-paper RPGs online with your friends, with features like character sheets, dice rolling, maps, tokens, and more.' }, + { name: 'keywords', content: 'virtual tabletop, vtt, online rpg, pen-and-paper rpg, dungeons and dragons, pathfinder, roll20 alternative' }, + { name: 'author', content: 'Aran Roig' }, + ], +}) + onMounted(() => { setupTheme(); setTheme('dark'); diff --git a/frontend/app/assets/css/colors.scss b/frontend/app/assets/css/colors.scss index ed979d4..a645109 100644 --- a/frontend/app/assets/css/colors.scss +++ b/frontend/app/assets/css/colors.scss @@ -16,6 +16,8 @@ $themes: ( button-hover: #202020aa, button-active: #202020cc, + toast-background: #202020, + hover: #21262d, selected: #4a4a4b, border-color: #819796, @@ -24,6 +26,9 @@ $themes: ( container-shadow: #151d28, sticky-header-bg: #20202077, + red: #e06c75, + green: #98c379, + icon-invert: 100% ), light: ( @@ -41,6 +46,8 @@ $themes: ( button-hover: #e9e9e9, button-active: #d4d4d4, + toast-background: #f0f0f0, + border-color: #e0e0e0, border: #f0f0f0, hover: #e9e9e9, @@ -49,6 +56,9 @@ $themes: ( container-shadow: #5f6774, sticky-header-bg: #fff, + red: #e06c75, + green: #98c379, + icon-invert: 0% ) ); diff --git a/frontend/app/assets/css/main.scss b/frontend/app/assets/css/main.scss index 1340187..bb4a1fa 100644 --- a/frontend/app/assets/css/main.scss +++ b/frontend/app/assets/css/main.scss @@ -355,3 +355,11 @@ span.artifact { display: flex; flex-direction: column; } + +.red { + color: var(--color-red); +} + +.green { + color: var(--color-green); +} \ No newline at end of file diff --git a/frontend/app/components/managers/ToastManager.vue b/frontend/app/components/managers/ToastManager.vue index ebf40ac..879e7c4 100644 --- a/frontend/app/components/managers/ToastManager.vue +++ b/frontend/app/components/managers/ToastManager.vue @@ -51,12 +51,12 @@ emitter.on('toast', data => { diff --git a/frontend/app/components/partials/Spinner.vue b/frontend/app/components/partials/Spinner.vue new file mode 100644 index 0000000..b97bed7 --- /dev/null +++ b/frontend/app/components/partials/Spinner.vue @@ -0,0 +1,33 @@ + + + + + \ No newline at end of file diff --git a/frontend/app/components/partials/VersionRender.vue b/frontend/app/components/partials/VersionRender.vue new file mode 100644 index 0000000..76cb274 --- /dev/null +++ b/frontend/app/components/partials/VersionRender.vue @@ -0,0 +1,24 @@ + + + + + + + diff --git a/frontend/app/components/windows/LoginWindow.vue b/frontend/app/components/windows/LoginWindow.vue index 89b8c72..9b5fe32 100644 --- a/frontend/app/components/windows/LoginWindow.vue +++ b/frontend/app/components/windows/LoginWindow.vue @@ -14,6 +14,7 @@ import WindowHandle from './partials/WindowHandle.vue'; import { DisplayToast } from '~/services/Toaster'; import Server from '~/services/Server'; import { SetUser } from '~/services/User'; +import Spinner from '../partials/Spinner.vue'; const handle = ref(null); @@ -25,6 +26,8 @@ let id = data.type; const username = ref(""); const password = ref(""); +const loading = ref(false); + onMounted(() => { SetupHandle(id, handle); SetSize(id, {width: 450, height: 480}); @@ -32,27 +35,28 @@ onMounted(() => { ResetPosition(id, "center"); }); +function ShowMainMenu(){ + CreateWindow('main_menu'); + ClearWindow('login'); +} + function login() { - Server().post('/user/login', { username: username.value, password: password.value }).then((response) => { - const data = response.data; - console.log(data); + loading.value = true; + Server().post('/user/login', { usermail: username.value, password: password.value }).then((response) => { + loading.value = false; + const data = response.data; - if(data.status == "error"){ - DisplayToast('red', "Wrong username or password", 3000) - } else { - SetUser(data.token); - - ShowMainMenu(); - } - }).catch((error) => { - console.log(error); - if(error.response.status == 429){ - // errorMessage.value = error.response.data; - } else { - // errorMessage.value = "Hi ha hagut un error intern, torna'ho a provar més tard"; - console.log(error); - } - }); + if(data.status == "error"){ + DisplayToast('red', $t(data.msg), 3000) + } else { + SetUser(data.token); + DisplayToast('green', $t('login.success'), 3000); + ShowMainMenu(); + } + }).catch((error) => { + loading.value = false; + DisplayToast('red', $t("errors.internal"), 3000); + }); } function toRegister(){ @@ -84,7 +88,14 @@ function toRegister(){
- +

{{$t('login.no-account')}} {{$t('login.register')}}

diff --git a/frontend/app/components/windows/MainMenuWindow.vue b/frontend/app/components/windows/MainMenuWindow.vue new file mode 100644 index 0000000..03f16c8 --- /dev/null +++ b/frontend/app/components/windows/MainMenuWindow.vue @@ -0,0 +1,78 @@ + + + + + + + + + diff --git a/frontend/app/components/windows/RegisterWindow.vue b/frontend/app/components/windows/RegisterWindow.vue index 7857ad2..1415bb5 100644 --- a/frontend/app/components/windows/RegisterWindow.vue +++ b/frontend/app/components/windows/RegisterWindow.vue @@ -1,8 +1,12 @@ @@ -26,17 +98,134 @@ onMounted(() => { -
+
+
+
+ +
+ + + + Dragonroll logo + +
+
+

{{ $t('register.first-register-message') }}

+

{{ $t('register.welcome') }}

+

{{ $t('register.message') }}

+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ +
+
+

{{$t('register.have-account')}} {{$t('register.login')}}

+
+
+ +
diff --git a/frontend/app/services/WindowDefinitions.js b/frontend/app/services/WindowDefinitions.js index 482fbc1..4a5379c 100644 --- a/frontend/app/services/WindowDefinitions.js +++ b/frontend/app/services/WindowDefinitions.js @@ -4,17 +4,21 @@ Put here all dragonroll windows const defWindows = { login: { - title: 'Login', + title: 'windows.login', movable: false, component: () => import('~/components/windows/LoginWindow.vue'), }, register: { - title: 'Register', + title: 'windows.register', movable: false, component: () => import('~/components/windows/RegisterWindow.vue'), }, + main_menu: { + title: 'windows.main-menu', + component: () => import('~/components/windows/MainMenuWindow.vue'), + }, example: { - title: 'Example', + title: 'windows.example', component: () => import('~/components/windows/ExampleWindow.vue'), } } diff --git a/frontend/i18n/locales/en.json b/frontend/i18n/locales/en.json index 74649a2..c461ff2 100644 --- a/frontend/i18n/locales/en.json +++ b/frontend/i18n/locales/en.json @@ -2,15 +2,58 @@ "windows": { "login": "Login", "register": "Register", + "main-menu": "Dragonroll", "example": "Example Window" }, "login": { - "username": "Username", - "username-placeholder": "Enter your username here...", + "username": "Username or email", + "username-placeholder": "Enter your username or email here...", "password": "Password", "password-placeholder": "Enter your password...", "log-in": "Log in", "no-account": "You don't have an account?", - "register": "Register" + "register": "Register", + "errors": { + "invalid-credentials": "Invalid username/email or password.", + "params": "Please enter both username/email and password." + }, + "success": "Login successful!" + }, + "register": { + "name": "Name", + "name-placeholder": "Enter your name here...", + "email": "Email", + "email-placeholder": "Enter your email here...", + "username": "Username", + "username-placeholder": "Enter your username here...", + "password": "Password", + "password-placeholder": "Enter your password...", + "confirm-password": "Confirm Password", + "confirm-password-placeholder": "Re-enter your password...", + "register": "Register", + "have-account": "Already have an account?", + "login": "Login", + "password-confirm-placeholder": "Confirm your password...", + "welcome": "Welcome to DragonRoll!", + "message": "Please enter your desired username and password to create an account.", + "first-register-message": "You are about to create the first account on this DragonRoll instance. This account will be granted administrator privileges.", + "errors": { + "name-empty": "Please enter your name.", + "email-empty": "Please enter a valid email address.", + "username-empty": "Please enter a username.", + "passwords-no-match": "The passwords you entered do not match.", + "email-username-exists": "An account with this email or username already exists." + }, + "success": "Registration successful! You can now log in." + }, + "errors": { + "internal": "An internal error occurred." + }, + "main-menu": { + "main-menu": "Main menu", + "edit-profile": "Edit profile", + "campaigns": "Campaigns", + "log-out": "Log out", + "settings": "Settings" } } \ No newline at end of file diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 7852106..f1a25d3 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -1,4 +1,24 @@ -// https://nuxt.com/docs/api/configuration/nuxt-config +import { execSync } from 'node:child_process' + +function getGitInfo() { + try { + const commit = execSync('git rev-parse --short HEAD').toString().trim() + const tag = execSync('git describe --tags --abbrev=0').toString().trim() + const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim() + + return { commit, tag, branch } + } catch { + // fallback (production / no .git) + return { + commit: process.env.NUXT_PUBLIC_GIT_COMMIT || 'unknown', + tag: process.env.NUXT_PUBLIC_GIT_TAG || 'no-tag', + branch: process.env.NUXT_PUBLIC_GIT_BRANCH || 'unknown' + } + } +} + +const git = getGitInfo(); + export default defineNuxtConfig({ vite: { optimizeDeps: { @@ -22,7 +42,11 @@ export default defineNuxtConfig({ runtimeConfig: { public: { - apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:5000/api' + apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:5000/api', + gitCommit: git.commit, + gitTag: git.tag, + gitBranch: git.branch, + buildDate: new Date().toISOString(), } }, diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index 18993ad..edf28de 100644 Binary files a/frontend/public/favicon.ico and b/frontend/public/favicon.ico differ diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png new file mode 100644 index 0000000..5a2e59f Binary files /dev/null and b/frontend/public/favicon.png differ diff --git a/frontend/public/img/def-avatar.jpg b/frontend/public/img/def-avatar.jpg new file mode 100644 index 0000000..3c5b56e Binary files /dev/null and b/frontend/public/img/def-avatar.jpg differ