Compare commits

..

8 Commits

Author SHA1 Message Date
Phillip Thelen ed48e5e34c remove trailing space 2026-03-03 11:25:04 +01:00
Phillip Thelen c17db4ebcd fix lint 2026-03-03 10:43:22 +01:00
Phillip Thelen 7cc0696ee4 Add Web UI to set email if apple does not provide one 2026-02-27 16:51:41 +01:00
Phillip Thelen 4532105749 apply email passed via body if it is missing from apple profile 2026-02-25 12:06:15 +01:00
Kalista Payne 0ae19d9107 Squashed commit of the following:
commit 963b4133ec
Author: Kalista Payne <kalista@habitica.com>
Date:   Thu Feb 19 15:48:41 2026 -0600

    fix(text): clean up some gear descriptions

commit 53999a5b80
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Feb 4 17:18:07 2026 -0600

    fix(content): add seasonal set tokens

commit 4510c90e41
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Feb 4 17:10:24 2026 -0600

    feat(content): March-May 2026
2026-02-24 12:02:52 -06:00
Kalista Payne 68bfebcf30 5.45.0 2026-02-24 10:18:25 -06:00
Phillip Thelen 3e93911e70 Phillip/prod perf2 (#15596)
* Add new api call for kubernetes startup probe

* add hostname as tag for loggly

* Only listen to one change

* increase vite min chunk size

* respond gracefully to shutdown signal

* update server readiness according to mongodb and redis connection

* make larger vite chunks

* fix lint
2026-02-24 10:17:21 -06:00
Kalista Payne 4ea8636f03 fix(profile): correct stat display for class gear 2026-02-20 13:19:56 -06:00
14 changed files with 118 additions and 17 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "habitica",
"version": "5.44.3",
"version": "5.45.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "habitica",
"version": "5.44.3",
"version": "5.45.0",
"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.44.3",
"version": "5.45.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -43,6 +43,14 @@
<p class="purple-600">
{{ $t('usernameLimitations') }}
</p>
<input
v-if="needsEmailField"
id="emailInput"
v-model="email"
class="form-control dark"
type="text"
:placeholder="$t('email')"
>
<div class="custom-control custom-checkbox mb-4">
<input
id="privacyTOS"
@@ -165,6 +173,7 @@ export default {
registrationMethod: null,
username: '',
usernameIssues: [],
needsEmailField: false,
};
},
computed: {
@@ -183,22 +192,30 @@ export default {
},
},
mounted () {
if (window.sessionStorage.getItem('apple-token')) {
this.registrationMethod = 'apple';
} else if (!this.$store.state.registrationOptions.registrationMethod) {
this.$router.push('/');
} else {
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
}
this.authData = this.$store.state.registrationOptions.authData;
this.email = this.$store.state.registrationOptions.email;
this.username = this.$store.state.registrationOptions.username;
this.password = this.$store.state.registrationOptions.password;
this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm;
if (!this.email) {
if (window.sessionStorage.getItem('apple-token')) {
this.registrationMethod = 'apple';
if (!this.email) {
this.email = window.sessionStorage.getItem('apple-email');
}
} else if (!this.$store.state.registrationOptions.registrationMethod) {
this.$router.push('/');
} else {
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
}
if (!this.email && this.registrationMethod !== 'apple') {
return;
}
if ((!this.email || this.email === '') && this.registrationMethod === 'apple') {
this.needsEmailField = true;
}
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
this.$store.dispatch('auth:verifyUsername', {
username: usernameToCheck,
@@ -237,6 +254,7 @@ export default {
idToken: window.sessionStorage.getItem('apple-token'),
name: window.sessionStorage.getItem('apple-name'),
username: this.username,
email: this.email,
allowRegister: true,
});
} else {
@@ -37,6 +37,7 @@ export default {
window.location.href = '/';
} else {
window.sessionStorage.setItem('apple-token', response.idToken);
window.sessionStorage.setItem('apple-email', response.email);
window.location.href = '/username';
}
},
@@ -43,7 +43,7 @@
<strong>{{ $t('equipment') }}:</strong>
<span :class="{ 'positive-stat': statsComputed.gearBonus[stat] !== 0 }">
{{ statsComputed.gearBonus[stat] !== 0 ? '+' : '' }}{{
statsComputed.gearBonus[stat]
statsComputed.gearBonus[stat] + statsComputed.classBonus[stat]
}}
</span>
</li>
+1 -1
View File
@@ -122,7 +122,7 @@ export default defineConfig({
},
rollupOptions: {
output: {
experimentalMinChunkSize: 1000
experimentalMinChunkSize: 20000
}
}
},
@@ -0,0 +1,39 @@
import {
disableCache,
} from '../../middlewares/cache';
import SERVER_STATUS from '../../libs/serverStatus';
const api = {};
/**
* @api {get} /api/v3/ready Get Habitica's Server readiness status
* @apiName GetReady
* @apiGroup Status
*
* @apiSuccess {String} data.status 'ready' if everything is ok
*
* @apiSuccessExample {JSON} Server is Ready
* {
* 'status': 'ready',
* }
*/
api.getReady = {
method: 'GET',
url: '/ready',
// explicitly disable caching so that the server is always checked
middlewares: [disableCache],
async handler (req, res) {
// This allows kubernetes to determine if the server is ready to receive traffic
if (!SERVER_STATUS.MONGODB || !SERVER_STATUS.REDIS || !SERVER_STATUS.EXPRESS) {
res.respond(503, {
status: 'not ready',
});
} else {
res.respond(200, {
status: 'ready',
});
}
},
};
export default api;
+5 -1
View File
@@ -44,9 +44,13 @@ export async function appleProfile (req) {
const verifiedPayload = await jwt.verify(idToken, applePublicKey, { algorithms: 'RS256' });
let { email } = verifiedPayload;
if ((!email || email === '') && req.body.email) {
email = req.body.email;
}
return {
id: verifiedPayload.sub,
emails: [{ value: verifiedPayload.email }],
emails: [{ value: email }],
name: verifiedPayload.name || req.body.name || req.query.name,
idToken,
};
+2 -2
View File
@@ -3,6 +3,7 @@ import winston from 'winston';
import { Loggly } from 'winston-loggly-bulk';
import nconf from 'nconf';
import _ from 'lodash';
import os from 'os';
import {
CustomError,
} from './errors';
@@ -65,9 +66,8 @@ if (IS_PROD) {
),
}));
}
if (LOGGLY_TOKEN && LOGGLY_SUBDOMAIN) {
const tags = ['Winston-NodeJS'];
const tags = ['Winston-NodeJS', os.hostname()];
if (nconf.get('SERVER_EMOJI')) {
tags.push(nconf.get('SERVER_EMOJI'));
}
+8
View File
@@ -5,6 +5,7 @@ import {
getDevelopmentConnectionUrl,
getDefaultConnectionOptions,
} from './mongodb';
import SERVER_STATUS from './serverStatus';
const IS_PROD = nconf.get('IS_PROD');
const MAINTENANCE_MODE = nconf.get('MAINTENANCE_MODE');
@@ -24,6 +25,13 @@ const connectionUrl = IS_PROD ? DB_URI : getDevelopmentConnectionUrl(DB_URI);
export default async function connectToMongoDB () {
// Do not connect to MongoDB when in maintenance mode
if (MAINTENANCE_MODE !== 'true') {
mongoose.connection.on('open', () => {
SERVER_STATUS.MONGODB = true;
});
mongoose.connection.on('disconnected', () => {
SERVER_STATUS.MONGODB = false;
});
return mongoose.connect(connectionUrl, mongooseOptions).then(() => {
logger.info('Connected with Mongoose.');
});
+7
View File
@@ -0,0 +1,7 @@
const SERVER_STATUS = {
MONGODB: false,
REDIS: false,
EXPRESS: false,
};
export default SERVER_STATUS;
+11
View File
@@ -10,6 +10,7 @@ import {
} from '../libs/errors';
import logger from '../libs/logger';
import { apiError } from '../libs/apiError';
import SERVER_STATUS from '../libs/serverStatus';
// Middleware to rate limit requests to the API
@@ -47,6 +48,14 @@ if (RATE_LIMITER_ENABLED) {
enable_offline_queue: false,
});
redisClient.on('ready', () => {
SERVER_STATUS.REDIS = true;
});
redisClient.on('reconnecting', () => {
SERVER_STATUS.REDIS = false;
});
redisClient.on('error', error => {
logger.error(error, 'Redis Error');
});
@@ -56,6 +65,8 @@ if (RATE_LIMITER_ENABLED) {
storeClient: redisClient,
});
}
} else {
SERVER_STATUS.REDIS = true;
}
function setResponseHeaders (res, rateLimiterRes) {
@@ -41,7 +41,7 @@ export const logRequestData = (req, res, next) => {
export const logSlowRequests = (req, res, next) => {
req.requestStartTime = Date.now();
req.on('close', () => {
req.once('close', () => {
const requestTime = Date.now() - req.requestStartTime;
if (requestTime > SLOW_REQUEST_THRESHOLD) {
const data = buildBaseLogData(req);
+13
View File
@@ -1,6 +1,8 @@
import nconf from 'nconf';
import express from 'express';
import http from 'http';
import mongoose from 'mongoose';
import redis from 'redis';
import logger from './libs/logger';
// Setup translations
@@ -18,12 +20,22 @@ import './libs/setupFirebase';
import './models/challenge';
import './models/group';
import './models/user';
import SERVER_STATUS from './libs/serverStatus';
connectToMongoDB();
const server = http.createServer();
const app = express();
process.on('SIGTERM', async () => {
console.log('SIGTERM signal received: closing HTTP server');
server.close(async () => {
await mongoose.disconnect();
await redis.quit();
process.exit(0);
});
});
app.set('port', nconf.get('PORT'));
attachMiddlewares(app, server);
@@ -31,6 +43,7 @@ attachMiddlewares(app, server);
server.on('request', app);
server.listen(app.get('port'), () => {
logger.info(`Express server listening on port ${app.get('port')}`);
SERVER_STATUS.EXPRESS = true;
});
export default server;