Compare commits

..

10 Commits

Author SHA1 Message Date
Kalista Payne d466e9bc5e 5.48.3 2026-06-09 12:18:52 -05:00
Weblate eb2660f606 Merge branch 'origin/develop' into Weblate. 2026-06-09 18:57:58 +02:00
Kalista Payne ab38ed4d2a Update README.md 2026-06-09 11:56:50 -05:00
Weblate d8c1bfc80a Translated using Weblate (Japanese)
Currently translated at 96.8% (3495 of 3607 strings)

Translated using Weblate (Japanese)

Currently translated at 99.3% (154 of 155 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3607 of 3607 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (131 of 131 strings)

Co-authored-by: Nisa Hadisti <nisahadisti18@gmail.com>
Co-authored-by: Serhii <serzh.photograf@gmail.com>
Co-authored-by: Stelio Passaris <habitica@stelio.net>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/id/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ja/
Translation: Habitica/Gear
Translation: Habitica/Npc
Translation: Habitica/Settings
Translation: Habitica/Tasks
2026-06-09 17:29:06 +02:00
Kalista Payne 106ea68e31 fix(test): add expected CSP header 2026-06-04 11:25:40 -05:00
Fiz e6eda1bdaa fix(tasks): correct move route for challenge task reordering (#15660)
route challenge task moves to the user endpoint, not group-tasks
2026-06-04 11:20:36 -05:00
Kalista Payne bca081d7be fix(csp): do it ourself 2026-06-04 11:17:37 -05:00
Kalista Payne dcbb491bd2 5.48.2 2026-06-04 10:48:00 -05:00
Phillip Thelen 9bc9ce917f Correctly initialize rate limiter 2026-06-04 10:47:57 -05:00
Kalista Payne 7b7dc255df Merge commit from fork
Co-authored-by: YoyoChaud <yoan-degans-1337-yoyo@proton.me>
2026-06-04 10:46:31 -05:00
14 changed files with 114 additions and 21 deletions
+3
View File
@@ -1,3 +1,6 @@
## ⚠️ **No AI-Generated Code Policy**
This repository prohibits the submission of code generated by large language models (LLMs), AI coding assistants, or automated generation tools. All code must be authored entirely by humans. AI-assisted commits will be rejected during pull request reviews.
Habitica ![Build Status](https://github.com/HabitRPG/habitica/workflows/Test/badge.svg)
===============
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "habitica",
"version": "5.48.1",
"version": "5.48.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "habitica",
"version": "5.48.1",
"version": "5.48.3",
"hasInstallScript": true,
"dependencies": {
"@babel/core": "^7.22.10",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.48.1",
"version": "5.48.3",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
+2
View File
@@ -23,6 +23,7 @@ describe('cors middleware', () => {
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
'Access-Control-Allow-Headers': 'Authorization,Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
'Access-Control-Expose-Headers': 'X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,Retry-After',
'Content-Security-Policy': "default-src 'self' habitica.com *.habitica.com *.amazon.com *.amazonaws.com *.amplitude.com *.loggly.com *.payments-amazon.com *.stripe.com *.stripe.network; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src * data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'",
});
expect(res.sendStatus).to.not.have.been.called;
expect(next).to.have.been.calledOnce;
@@ -36,6 +37,7 @@ describe('cors middleware', () => {
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
'Access-Control-Allow-Headers': 'Authorization,Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
'Access-Control-Expose-Headers': 'X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,Retry-After',
'Content-Security-Policy': "default-src 'self' habitica.com *.habitica.com *.amazon.com *.amazonaws.com *.amplitude.com *.loggly.com *.payments-amazon.com *.stripe.com *.stripe.network; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src * data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'",
});
expect(res.sendStatus).to.have.been.calledWith(200);
expect(next).to.not.have.been.called;
@@ -582,7 +582,7 @@ export default {
const newPosition = where === 'top' ? 0 : list.length;
list.splice(newPosition, 0, moved[0]);
if (!this.isUser) {
if (task.group.id && !this.isUser) {
await this.$store.dispatch('tasks:moveGroupTask', {
taskId: taskIdToMove,
position: newPosition,
@@ -592,7 +592,7 @@ export default {
taskId: taskIdToMove,
position: newPosition,
});
this.user.tasksOrder[`${this.type}s`] = newOrder;
if (!this.taskListOverride) this.user.tasksOrder[`${this.type}s`] = newOrder;
}
},
async rewardSorted (data) {
@@ -237,5 +237,84 @@ describe('Task Column', () => {
});
});
});
// each board type should hit the right move route
describe('moveTo (task ordering route)', () => {
function makeStore (userData = {}, extraGetters = {}) {
return new Store({
getters: {
'tasks:getFilteredTaskList': () => () => [],
'tasks:getUnfilteredTaskList': () => () => [],
...extraGetters,
},
state: {
user: {
data: {
preferences: { tasks: { activeFilter: {} } },
tasksOrder: { habits: [] },
...userData,
},
},
},
});
}
function stubDispatch (vm) {
const calls = [];
vm.$store.dispatch = (action, payload) => {
calls.push({ action, payload });
return Promise.resolve(['b', 'a']);
};
return calls;
}
test('challenge tasks (no group.id) use tasks:move and keep the user order untouched', async () => {
wrapper = makeWrapper({ store: makeStore() });
wrapper.setProps({
isUser: false,
challenge: { _id: 'c1' },
taskListOverride: [{ _id: 'a', group: {} }, { _id: 'b', group: {} }],
});
const calls = stubDispatch(wrapper.vm);
await wrapper.vm.moveTo({ _id: 'a', group: {} }, 'bottom');
expect(calls).to.have.lengthOf(1);
expect(calls[0].action).to.eq('tasks:move');
// an overridden list must never overwrite the user's personal order
expect(wrapper.vm.user.tasksOrder.habits.join(',')).to.eq('');
});
test('group-plan tasks (with group.id) use tasks:moveGroupTask', async () => {
wrapper = makeWrapper({ store: makeStore() });
wrapper.setProps({
isUser: false,
group: { _id: 'g1' },
taskListOverride: [{ _id: 'a', group: { id: 'g1' } }, { _id: 'b', group: { id: 'g1' } }],
});
const calls = stubDispatch(wrapper.vm);
await wrapper.vm.moveTo({ _id: 'a', group: { id: 'g1' } }, 'bottom');
expect(calls).to.have.lengthOf(1);
expect(calls[0].action).to.eq('tasks:moveGroupTask');
});
test('user tasks use tasks:move and update the user order', async () => {
const store = makeStore(
{ tasksOrder: { habits: ['a', 'b'] } },
{ 'tasks:getUnfilteredTaskList': () => () => [{ _id: 'a', group: {} }, { _id: 'b', group: {} }] },
);
wrapper = makeWrapper({ store });
wrapper.setProps({ isUser: true });
const calls = stubDispatch(wrapper.vm);
await wrapper.vm.moveTo({ _id: 'a', group: {} }, 'bottom');
expect(calls).to.have.lengthOf(1);
expect(calls[0].action).to.eq('tasks:move');
expect(wrapper.vm.user.tasksOrder.habits.join(',')).to.eq('b,a');
});
});
});
});
+1 -1
View File
@@ -1217,7 +1217,7 @@
"headArmoireGraduateCapNotes": "Congratulations! Your deep thoughts have earned you this thinking cap. Increases Intelligence by <%= int %>. Enchanted Armoire: Graduate Set (Item 3 of 3).",
"headArmoireGreenFloppyHatText": "Green Floppy Hat",
"headArmoireGreenFloppyHatNotes": "Many spells have been sewn into this simple hat, giving it a gorgeous green colour. Increases Constitution, Intelligence, and Perception by <%= attrs %> each. Enchanted Armoire: Green Loungewear Set (Item 1 of 3).",
"headArmoireCannoneerBandannaText": "Cannoneer Bandanna",
"headArmoireCannoneerBandannaText": "Cannoneer Bandana",
"headArmoireCannoneerBandannaNotes": "'Tis a cannoneer's life for me! Increases Intelligence and Perception by <%= attrs %> each. Enchanted Armoire: Cannoneer Set (Item 3 of 3).",
"headArmoireFalconerCapText": "Falconer Cap",
"headArmoireFalconerCapNotes": "This jaunty cap helps you better understand birds of prey. Increases Intelligence by <%= int %>. Enchanted Armoire: Falconer Set (Item 2 of 3).",
+2 -2
View File
@@ -5,14 +5,14 @@
"welcomeTo": "Selamat datang di",
"welcomeBack": "Selamat datang kembali!",
"justin": "Justin",
"justinIntroMessage1": "Halo! Kamu pasti baru di sini. Nama saya <strong>Justin</strong>, dan aku akan menjadi pemandumu di Habitica. Jadi, kamu ingin terlihat seperti apa sekarang? Jangan khawatir, kamu dapat mengganti penampilanmu nanti.",
"justinIntroMessage1": "Halo! Kamu pasti baru di sini. Nama saya <strong>Justin</strong>, dan aku akan menjadi pemandumu di Habitica. Jadi, kamu ingin terlihat seperti apa sekarang? Jangan khawatir, kamu dapat mengganti penampilanmu nanti.",
"justinIntroMessage3": "Bagus! Sekarang, apa yang kamu mau perbaiki selama perjalanan ini?",
"introTour": "Ini dia! Aku telah mengisi beberapa Tugas untukmu berdasarkan minatmu, jadi kamu dapat memulai langsung. Klik sebuah Tugas untuk mengedit atau tambahkan Tugas baru untuk menyesuaikan jadwalmu!",
"prev": "Sebelum",
"next": "Setelah",
"randomize": "Acak-acak",
"mattBoch": "Matt Boch",
"mattBochText1": "Selamat datang di kandang! Aku Matt, sang Penakluk Hewan. Setiap kali kamu menyelesaikan sebuah tugas, kamu akan mendapatkan sebuah kesempatan acak untuk mendapatkan sebuah Telur atau Ramuan Penetas untuk menetas seekor hewean peliharaan. Setelah kamu menetaskan seekor hewan peliharaan, peliharaanmu akan muncul disini! Klik gambar peliharaan untuk memasangnya pada avatarmu. Beri mereka makan dengan makanan yang kamu temukan, dan mereka akan tumbuh cukup besar untuk bisa ditunggangi.",
"mattBochText1": "Selamat datang di kandang! Aku Matt, sang Penakluk Hewan. Setiap kali kamu menyelesaikan sebuah tugas, kamu akan mendapatkan kesempatan acak untuk mendapatkan Telur atau Ramuan Penetas untuk menetas seekor hewean peliharaan. Setelah kamu menetaskan seekor hewan peliharaan, peliharaanmu akan muncul di sini! Klik gambar peliharaan untuk memasangnya pada avatarmu. Beri mereka makan dengan makanan yang kamu temukan, dan mereka akan tumbuh cukup besar untuk bisa ditunggangi.",
"welcomeToTavern": "Selamat datang di Kedai Minuman!",
"sleepDescription": "Perlu istirahat? Tunda Kerusakan (berlokasi di Pengaturan) untuk menunda beberapa permainan dalam Habitica yang lebih sulit:",
"sleepBullet1": "Keseharian yang terlewat tidak akan menyakitimu (monster bos masih akan mengakibatkan kerusakan yang disebabkan Keseharian yang terlewat oleh anggota Party yang lain)",
+7 -2
View File
@@ -1266,7 +1266,7 @@
"headArmoireJeweledArcherHelmText": "宝石で飾られたかぶと",
"headArmoireJeweledArcherHelmNotes": "このかぶとは飾り立てて見えるかもしれません。しかし、大変に軽く強力でもあるのです。知能が<%= int %>上がります。ラッキー宝箱:宝石飾りの弓使いセット(アイテム1/3 )。",
"headArmoireVeilOfSpadesText": "スペードのベール",
"headArmoireVeilOfSpadesNotes": "影になってミステリアスなベールは、あなたの密やかさを高めるでしょう。知覚が<%= per %>上がります。ラッキー宝箱:スペードのエースセット(アイテム1/3 )。",
"headArmoireVeilOfSpadesNotes": "影になってミステリアスなベールは、あなたの密やかさを高めるでしょう。知覚が<%= per %>上がります。ラッキー宝箱:スペードのエースセットアイテム1/3。",
"offhand": "反対の手のアイテム",
"shieldBase0Text": "利き手と反対の手の装備はありません",
"shieldBase0Notes": "盾や、利き手と反対の手の装備はありません。",
@@ -3488,5 +3488,10 @@
"armorSpecialSummer2026RogueNotes": "このスーツに身に包めてもいいですが、困ったことから逃げてはいけません。探検家みたいに嵐を召喚してタスクに挑みましょう。知覚が<%= per %>上がります。2026年夏の限定装備。",
"armorSpecialSummer2026HealerText": "ツノメドリのスーツ",
"armorSpecialSummer2026MageText": "イタチザメのスーツ",
"armorSpecialSummer2026RogueText": "津波のスーツ"
"armorSpecialSummer2026RogueText": "津波のスーツ",
"weaponSpecialSummer2026MageText": "イタチザメの槍",
"weaponSpecialSummer2026MageNotes": "この鋭い諸刃の剣は海洋風コーデにぴったりです。知能が<%= int %>、知覚が<%= per %>上がります。2026年夏の限定装備。",
"weaponSpecialSummer2026WarriorNotes": "この豪華できらびやかな武器は沼風コーデにぴったりです。力が<%= str %>上がります。2026年夏の限定装備。",
"weaponArmoireBrightRainbowKiteText": "虹色の凧",
"weaponArmoirePastelRainbowKiteText": "パステルカラーの凧"
}
+2 -1
View File
@@ -151,5 +151,6 @@
"everyMonth": "毎月",
"everyXMonths": "<%= count %>ヶ月ごと",
"everyYear": "毎年",
"everyXYears": "<%= count %>年ごと"
"everyXYears": "<%= count %>年ごと",
"fifthWeekWarning": "このタスクは<%= day %>が少ない月には表示されません"
}
+3 -3
View File
@@ -18,7 +18,7 @@
"resetAccPop": "Почати заново, залишивши всі рівні, золото, спорядження, історію та завдання.",
"deleteAccount": "Видалити акаунт",
"deleteAccPop": "Видалити ваш акаунт Habitica.",
"feedback": "Якщо ви хочете залишити відгук, будь-ласка, введіть його нижче - ми будемо раді почути його. Він буде анонімним, хіба що ви самі вирішите залишити свої контактні дані! Не володієте англійською достатньо добре? Не проблема! Використовуйте мову, якій віддаєте перевагу.",
"feedback": "Ми будемо раді почути ваші відгуки! Якщо бажаєте поділитися ними, введіть їх нижче. Вони залишаться анонімними, якщо ви не вирішите вказати свої контактні дані.",
"dataExport": "Експортування даних",
"saveData": "Ось декілька варіантів для збереження ваших даних.",
"habitHistory": "Історія звичок",
@@ -47,8 +47,8 @@
"dangerZone": "Небезпечна зона",
"resetText1": "<b>Будьте обережні!</b> Це призведе до скидання багатьох частин вашого акаунта. Це вкрай небажано, але деякі люди вважають це корисним після гри з сайтом протягом короткого часу.",
"resetText2": "Іншим варіантом є використання <b>Кулі Переродження</b>, яка скине все інше, зберігаючи ваші Завдання та Спорядження.",
"deleteLocalAccountText": "<b>Ви впевнені?</b> Ваш акаунт буде видалено назавжди без можливості відновлення! Ви будете змушені створити новий акаунт, якщо захочете знову користуватись Habitica. Вартість витрачених самоцвітів та тих, що знаходяться у банку не буде компенсована. Якщо ви абсолютно впевнені, напишіть ваш пароль у текстовому полі нижче.",
"deleteSocialAccountText": "<b>Ви впевнені?</b> Ваш акаунт буде видалено назавжди без можливості відновлення! Ви будете змушені створити новий акаунт, якщо захочете знову користуватись Habitica. Вартість витрачених самоцвітів та тих, що знаходяться у банку не буде компенсована. Якщо ви абсолютно впевнені, напишіть \"<%= magicWord %>\" у текстовому полі нижче.",
"deleteLocalAccountText": "<b>Ви впевнені?</b> Ця дія є незворотною. Видалення вашого акаунта призведе до видалення всіх ваших даних, і їх неможливо буде відновити. Самоцвіти не підлягають поверненню. <br><br>Будь ласка, зачекайте до 24 годин для завершення видалення акаунта та до 30 днів для видалення аналітичних даних, якщо ви надавали на це згоду. Після завершення ви зможете зареєструвати новий акаунт у Habitica, використовуючи свої попередні облікові дані. <br><br>Щоб продовжити, введіть свій пароль нижче.",
"deleteSocialAccountText": "<b>Ви впевнені?</b> Ця дія є незворотною. Видалення вашого акаунта призведе до видалення всіх ваших даних, і їх неможливо буде відновити. Самоцвіти не підлягають поверненню.<br><br>Будь ласка, зачекайте до 24 годин для завершення видалення акаунта та до 30 днів для видалення аналітичних даних, якщо ви надавали на це згоду. Після завершення ви зможете зареєструвати новий акаунт у Habitica, використовуючи свої попередні облікові дані.<br><br>Щоб продовжити, введіть <%= magicWord %> нижче.",
"API": "API",
"APIv3": "API v3",
"APIText": "Скопіюйте це для використання в додатках сторонніх розробників. Але пам'ятайте, що API токен використовується як пароль, тож не поширюйте його публічно. Інколи вас питатимуть про ID користувача, але не публікуйте свій API Token там, де інші можуть побачити його, у тому числі на GitHub.",
+1 -1
View File
@@ -333,7 +333,7 @@ function _getMembersForItem (type) {
const escapedSearch = escapeRegExp(req.query.search);
query.$or = [
{ 'profile.name': { $regex: new RegExp(escapedSearch, 'i') } },
{ 'auth.local.username': { $regex: new RegExp(req.query.search, 'i') } },
{ 'auth.local.username': { $regex: new RegExp(escapedSearch, 'i') } },
];
}
} else if (type === 'group-invites') {
+2
View File
@@ -5,6 +5,8 @@ export default function corsMiddleware (req, res, next) {
'Access-Control-Allow-Headers': 'Authorization,Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
// Expose rate limit headers to CORS requests
'Access-Control-Expose-Headers': 'X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,Retry-After',
// Content-Security-Policy based on Helmet defaults
'Content-Security-Policy': "default-src 'self' habitica.com *.habitica.com *.amazon.com *.amazonaws.com *.amplitude.com *.loggly.com *.payments-amazon.com *.stripe.com *.stripe.network; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src * data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'",
});
if (req.method === 'OPTIONS') return res.sendStatus(200);
return next();
+7 -6
View File
@@ -1,5 +1,4 @@
import nconf from 'nconf';
import redis from 'ioredis';
import {
RateLimiterRedis,
RateLimiterMemory,
@@ -11,6 +10,7 @@ import {
import logger from '../libs/logger';
import { apiError } from '../libs/apiError';
import SERVER_STATUS from '../libs/serverStatus';
import setupRedis from '../libs/redis';
// Middleware to rate limit requests to the API
@@ -32,26 +32,27 @@ const IP_RATE_LIMIT_COST = nconf.get('RATE_LIMITER_IP_COST') || 5;
let redisClient;
if (RATE_LIMITER_ENABLED && !IS_TEST) {
redisClient = redis.createClient({
redisClient = setupRedis({
host: REDIS_HOST,
password: REDIS_PASSWORD,
port: REDIS_PORT,
enable_offline_queue: false,
}, {
enableOfflineQueue: false,
});
redisClient.on('ready', () => {
SERVER_STATUS.REDIS = true;
SERVER_STATUS.RATE_LIMITER = true;
});
redisClient.on('reconnecting', () => {
SERVER_STATUS.REDIS = false;
SERVER_STATUS.RATE_LIMITER = false;
});
redisClient.on('error', error => {
logger.error(error, 'Redis Error');
});
} else {
SERVER_STATUS.REDIS = true;
SERVER_STATUS.RATE_LIMITER = true;
}
function setResponseHeaders (res, points, rateLimiterRes) {