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 @@
+
+
+
+
+
+ Dragonroll {{ config.public.gitTag }}-{{ config.public.gitCommit }}@{{ config.public.gitBranch }}
+
{{ config.public.buildDate }}
+
+
+
+
+
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(){
-
+
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