From f75fad7b7a7fd92cf3877594e8842c4ecce39f0f Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sat, 6 Dec 2025 09:23:18 +0100 Subject: [PATCH 1/3] feat: Add option to change log levels of username already exists message --- resources/buildConfigDefinitions.js | 2 ++ spec/ParseUser.spec.js | 53 +++++++++++++++++++++++++++++ src/Config.js | 16 +++++++++ src/Options/Definitions.js | 15 ++++++++ src/Options/docs.js | 6 ++++ src/Options/index.js | 10 ++++++ src/middlewares.js | 13 ++++++- types/Options/index.d.ts | 4 +++ 8 files changed, 118 insertions(+), 1 deletion(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 0b7dcdac3d..2733016fba 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -25,6 +25,7 @@ const nestedOptionTypes = [ 'SecurityOptions', 'SchemaOptions', 'LogLevels', + 'LogEvents', ]; /** The prefix of environment variables for nested options. */ @@ -39,6 +40,7 @@ const nestedOptionEnvPrefix = { LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_', LogLevel: 'PARSE_SERVER_LOG_LEVEL_', LogLevels: 'PARSE_SERVER_LOG_LEVELS_', + LogEvents: 'PARSE_SERVER_LOG_EVENTS_', PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_', PagesOptions: 'PARSE_SERVER_PAGES_', PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_', diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 0380589057..a3743f75d8 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -81,6 +81,59 @@ describe('Parse.User testing', () => { } }); + it('logs username taken with configured log level', async () => { + await reconfigureServer({ logEvents: { usernameAlreadyExists: 'warn' } }); + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + const loggerWarnSpy = spyOn(logger, 'warn').and.callThrough(); + + const user = new Parse.User(); + user.setUsername('dupUser'); + user.setPassword('pass'); + await user.signUp(); + + const user2 = new Parse.User(); + user2.setUsername('dupUser'); + user2.setPassword('pass2'); + + expect(loggerWarnSpy).not.toHaveBeenCalled(); + + try { + await user2.signUp(); + fail('should have thrown'); + } catch (e) { + expect(e.code).toBe(Parse.Error.USERNAME_TAKEN); + } + + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + expect(loggerErrorSpy.calls.count()).toBe(0); + }); + + it('can silence username taken log event', async () => { + await reconfigureServer({ logEvents: { usernameAlreadyExists: 'silent' } }); + const logger = require('../lib/logger').default; + loggerErrorSpy = spyOn(logger, 'error').and.callThrough(); + const loggerWarnSpy = spyOn(logger, 'warn').and.callThrough(); + + const user = new Parse.User(); + user.setUsername('dupUser'); + user.setPassword('pass'); + await user.signUp(); + + const user2 = new Parse.User(); + user2.setUsername('dupUser'); + user2.setPassword('pass2'); + try { + await user2.signUp(); + fail('should have thrown'); + } catch (e) { + expect(e.code).toBe(Parse.Error.USERNAME_TAKEN); + } + + expect(loggerWarnSpy).not.toHaveBeenCalled(); + expect(loggerErrorSpy.calls.count()).toBe(0); + }); + it('user login with context', async () => { let hit = 0; const context = { foo: 'bar' }; diff --git a/src/Config.js b/src/Config.js index 241edf9771..9e982a3b82 100644 --- a/src/Config.js +++ b/src/Config.js @@ -14,6 +14,7 @@ import { FileUploadOptions, IdempotencyOptions, LogLevels, + LogEvents, PagesOptions, ParseServerOptions, SchemaOptions, @@ -128,6 +129,7 @@ export class Config { requestKeywordDenylist, allowExpiredAuthDataToken, logLevels, + logEvents, rateLimit, databaseOptions, extendSessionOnUse, @@ -170,6 +172,7 @@ export class Config { this.validateRequestKeywordDenylist(requestKeywordDenylist); this.validateRateLimit(rateLimit); this.validateLogLevels(logLevels); + this.validateLogEvents(logEvents); this.validateDatabaseOptions(databaseOptions); this.validateCustomPages(customPages); this.validateAllowClientClassCreation(allowClientClassCreation); @@ -641,6 +644,19 @@ export class Config { } } + static validateLogEvents(logEvents) { + for (const key of Object.keys(LogEvents)) { + if (logEvents[key]) { + // We validate that each configured event uses a valid log *level* (same list as logLevels). + if (validLogLevels.indexOf(logEvents[key]) === -1) { + throw `'${key}' must be one of ${JSON.stringify(validLogLevels)}`; + } + } else { + logEvents[key] = LogEvents[key].default; + } + } + } + static validateDatabaseOptions(databaseOptions) { if (databaseOptions == undefined) { return; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 66c1d8bcea..0aa2962b95 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -352,6 +352,13 @@ module.exports.ParseServerOptions = { action: parsers.objectParser, type: 'LiveQueryServerOptions', }, + logEvents: { + env: 'PARSE_SERVER_LOG_EVENTS', + help: '(Optional) Overrides the log levels used by specific log events.', + action: parsers.objectParser, + type: 'LogEvents', + default: {}, + }, loggerAdapter: { env: 'PARSE_SERVER_LOGGER_ADAPTER', help: 'Adapter module for the logging sub-system', @@ -1507,3 +1514,11 @@ module.exports.LogLevels = { default: 'info', }, }; +module.exports.LogEvents = { + usernameAlreadyExists: { + env: 'PARSE_SERVER_LOG_EVENTS_USERNAME_ALREADY_EXISTS', + help: + 'Log level used when a sign-up fails because the username already exists. Default is `error`. See [LogLevel](LogLevel.html) for available values.', + default: 'error', + }, +}; diff --git a/src/Options/docs.js b/src/Options/docs.js index 9569239ef7..5283309ae3 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -63,6 +63,7 @@ * @property {Boolean} jsonLogs Log as structured JSON objects * @property {LiveQueryOptions} liveQuery parse-server's LiveQuery configuration object * @property {LiveQueryServerOptions} liveQueryServerOptions Live query server configuration options (will start the liveQuery server) + * @property {LogEvents} logEvents (Optional) Overrides the log levels used by specific log events. * @property {Adapter} loggerAdapter Adapter module for the logging sub-system * @property {String} logLevel Sets the level for logs * @property {LogLevels} logLevels (Optional) Overrides the log levels used internally by Parse Server to log events. @@ -329,3 +330,8 @@ * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. */ + +/** + * @interface LogEvents + * @property {String} usernameAlreadyExists Log level used when a sign-up fails because the username already exists. Default is `error`. See [LogLevel](LogLevel.html) for available values. + */ diff --git a/src/Options/index.js b/src/Options/index.js index cdeb7cd846..4f33ad98a6 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -98,6 +98,9 @@ export interface ParseServerOptions { /* (Optional) Overrides the log levels used internally by Parse Server to log events. :DEFAULT: {} */ logLevels: ?LogLevels; + /* (Optional) Overrides the log levels used by specific log events. + :DEFAULT: {} */ + logEvents: ?LogEvents; /* Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null) */ maxLogFiles: ?NumberOrString; /* Disables console output @@ -790,3 +793,10 @@ export interface LogLevels { */ cloudFunctionError: ?string; } + +export interface LogEvents { + /* Log level used when a sign-up fails because the username already exists. Default is `error`. See [LogLevel](LogLevel.html) for available values. + :DEFAULT: error + */ + usernameAlreadyExists: ?string; +} diff --git a/src/middlewares.js b/src/middlewares.js index 2fedce8f08..12c8ba979b 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -466,6 +466,7 @@ export function handleParseErrors(err, req, res, next) { if (req.config && req.config.enableExpressErrorHandler) { return next(err); } + const usernameAlreadyExistsLevel = req.config?.logEvents?.usernameAlreadyExists || 'error'; let httpStatus; // TODO: fill out this mapping switch (err.code) { @@ -480,7 +481,17 @@ export function handleParseErrors(err, req, res, next) { } res.status(httpStatus); res.json({ code: err.code, error: err.message }); - log.error('Parse error: ', err); + if (err.code === Parse.Error.USERNAME_TAKEN) { + if (usernameAlreadyExistsLevel !== 'silent') { + const loggerMethod = + typeof log[usernameAlreadyExistsLevel] === 'function' + ? log[usernameAlreadyExistsLevel].bind(log) + : log.error.bind(log); + loggerMethod('Parse error: ', err); + } + } else { + log.error('Parse error: ', err); + } } else if (err.status && err.message) { res.status(err.status); res.json({ error: err.message }); diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index ad11050648..cc17583c4d 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -47,6 +47,7 @@ export interface ParseServerOptions { verbose?: boolean; logLevel?: string; logLevels?: LogLevels; + logEvents?: LogEvents; maxLogFiles?: NumberOrString; silent?: boolean; databaseURI: string; @@ -298,4 +299,7 @@ export interface LogLevels { cloudFunctionSuccess?: string; cloudFunctionError?: string; } +export interface LogEvents { + usernameAlreadyExists?: string; +} export {}; From cd24f6db690dbe222204827ea0db7ade4dc9843f Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Mon, 8 Dec 2025 08:27:52 +0100 Subject: [PATCH 2/3] fix: feedbacks --- resources/buildConfigDefinitions.js | 1 - src/Config.js | 5 ++--- src/Options/Definitions.js | 6 ++---- src/Options/docs.js | 6 +----- src/Options/index.js | 5 +---- src/middlewares.js | 3 ++- types/Options/index.d.ts | 4 +--- 7 files changed, 9 insertions(+), 21 deletions(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 2733016fba..2814394735 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -40,7 +40,6 @@ const nestedOptionEnvPrefix = { LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_', LogLevel: 'PARSE_SERVER_LOG_LEVEL_', LogLevels: 'PARSE_SERVER_LOG_LEVELS_', - LogEvents: 'PARSE_SERVER_LOG_EVENTS_', PagesCustomUrlsOptions: 'PARSE_SERVER_PAGES_CUSTOM_URL_', PagesOptions: 'PARSE_SERVER_PAGES_', PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_', diff --git a/src/Config.js b/src/Config.js index 9e982a3b82..3e218c6e19 100644 --- a/src/Config.js +++ b/src/Config.js @@ -14,7 +14,6 @@ import { FileUploadOptions, IdempotencyOptions, LogLevels, - LogEvents, PagesOptions, ParseServerOptions, SchemaOptions, @@ -645,14 +644,14 @@ export class Config { } static validateLogEvents(logEvents) { - for (const key of Object.keys(LogEvents)) { + for (const key of Object.keys(LogLevels)) { if (logEvents[key]) { // We validate that each configured event uses a valid log *level* (same list as logLevels). if (validLogLevels.indexOf(logEvents[key]) === -1) { throw `'${key}' must be one of ${JSON.stringify(validLogLevels)}`; } } else { - logEvents[key] = LogEvents[key].default; + logEvents[key] = LogLevels[key].default; } } } diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 0aa2962b95..3d0a7d2085 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -356,7 +356,7 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_LOG_EVENTS', help: '(Optional) Overrides the log levels used by specific log events.', action: parsers.objectParser, - type: 'LogEvents', + type: 'LogLevels', default: {}, }, loggerAdapter: { @@ -1513,10 +1513,8 @@ module.exports.LogLevels = { 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values.', default: 'info', }, -}; -module.exports.LogEvents = { usernameAlreadyExists: { - env: 'PARSE_SERVER_LOG_EVENTS_USERNAME_ALREADY_EXISTS', + env: 'PARSE_SERVER_LOG_LEVELS_USERNAME_ALREADY_EXISTS', help: 'Log level used when a sign-up fails because the username already exists. Default is `error`. See [LogLevel](LogLevel.html) for available values.', default: 'error', diff --git a/src/Options/docs.js b/src/Options/docs.js index 5283309ae3..ac9abc603d 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -63,7 +63,7 @@ * @property {Boolean} jsonLogs Log as structured JSON objects * @property {LiveQueryOptions} liveQuery parse-server's LiveQuery configuration object * @property {LiveQueryServerOptions} liveQueryServerOptions Live query server configuration options (will start the liveQuery server) - * @property {LogEvents} logEvents (Optional) Overrides the log levels used by specific log events. + * @property {LogLevels} logEvents (Optional) Overrides the log levels used by specific log events. * @property {Adapter} loggerAdapter Adapter module for the logging sub-system * @property {String} logLevel Sets the level for logs * @property {LogLevels} logLevels (Optional) Overrides the log levels used internally by Parse Server to log events. @@ -329,9 +329,5 @@ * @property {String} triggerAfter Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`. See [LogLevel](LogLevel.html) for available values. * @property {String} triggerBeforeError Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`. See [LogLevel](LogLevel.html) for available values. * @property {String} triggerBeforeSuccess Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`. See [LogLevel](LogLevel.html) for available values. - */ - -/** - * @interface LogEvents * @property {String} usernameAlreadyExists Log level used when a sign-up fails because the username already exists. Default is `error`. See [LogLevel](LogLevel.html) for available values. */ diff --git a/src/Options/index.js b/src/Options/index.js index 4f33ad98a6..f442c6b643 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -100,7 +100,7 @@ export interface ParseServerOptions { logLevels: ?LogLevels; /* (Optional) Overrides the log levels used by specific log events. :DEFAULT: {} */ - logEvents: ?LogEvents; + logEvents: ?LogLevels; /* Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null) */ maxLogFiles: ?NumberOrString; /* Disables console output @@ -792,9 +792,6 @@ export interface LogLevels { :DEFAULT: error */ cloudFunctionError: ?string; -} - -export interface LogEvents { /* Log level used when a sign-up fails because the username already exists. Default is `error`. See [LogLevel](LogLevel.html) for available values. :DEFAULT: error */ diff --git a/src/middlewares.js b/src/middlewares.js index 12c8ba979b..edcf96bca6 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -466,7 +466,8 @@ export function handleParseErrors(err, req, res, next) { if (req.config && req.config.enableExpressErrorHandler) { return next(err); } - const usernameAlreadyExistsLevel = req.config?.logEvents?.usernameAlreadyExists || 'error'; + const usernameAlreadyExistsLevel = + req.config?.logEvents?.usernameAlreadyExists || 'info'; let httpStatus; // TODO: fill out this mapping switch (err.code) { diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index cc17583c4d..e6618e65bf 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -47,7 +47,7 @@ export interface ParseServerOptions { verbose?: boolean; logLevel?: string; logLevels?: LogLevels; - logEvents?: LogEvents; + logEvents?: LogLevels; maxLogFiles?: NumberOrString; silent?: boolean; databaseURI: string; @@ -298,8 +298,6 @@ export interface LogLevels { triggerBeforeError?: string; cloudFunctionSuccess?: string; cloudFunctionError?: string; -} -export interface LogEvents { usernameAlreadyExists?: string; } export {}; From 4526b0a3e700381e90f4b7ad2ab999a7e103ad43 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Mon, 8 Dec 2025 08:28:56 +0100 Subject: [PATCH 3/3] fix: remove log events --- resources/buildConfigDefinitions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 2814394735..0b7dcdac3d 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -25,7 +25,6 @@ const nestedOptionTypes = [ 'SecurityOptions', 'SchemaOptions', 'LogLevels', - 'LogEvents', ]; /** The prefix of environment variables for nested options. */