mirror of https://github.com/laurent22/joplin.git
Server: Display banner when an account is disabled and provide reason
parent
6fec2a93fc
commit
8c9331cf61
|
@ -1,5 +1,5 @@
|
||||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, koaAppContext, koaNext } from '../utils/testing/testUtils';
|
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, koaAppContext, koaNext } from '../utils/testing/testUtils';
|
||||||
import { Notification } from '../services/database/types';
|
import { Notification, UserFlagType } from '../services/database/types';
|
||||||
import { defaultAdminEmail, defaultAdminPassword } from '../db';
|
import { defaultAdminEmail, defaultAdminPassword } from '../db';
|
||||||
import notificationHandler from './notificationHandler';
|
import notificationHandler from './notificationHandler';
|
||||||
|
|
||||||
|
@ -76,4 +76,15 @@ describe('notificationHandler', function() {
|
||||||
expect(notifications.length).toBe(0);
|
expect(notifications.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should display a banner if the account is disabled', async function() {
|
||||||
|
const { session, user } = await createUserAndSession(1);
|
||||||
|
|
||||||
|
await models().userFlag().add(user.id, UserFlagType.FailedPaymentFinal);
|
||||||
|
|
||||||
|
const ctx = await koaAppContext({ sessionId: session.id });
|
||||||
|
await notificationHandler(ctx, koaNext);
|
||||||
|
|
||||||
|
expect(ctx.joplin.notifications.find(v => v.id === 'accountDisabled')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,9 +4,10 @@ import { NotificationLevel } from '../services/database/types';
|
||||||
import { defaultAdminEmail, defaultAdminPassword } from '../db';
|
import { defaultAdminEmail, defaultAdminPassword } from '../db';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import Logger from '@joplin/lib/Logger';
|
import Logger from '@joplin/lib/Logger';
|
||||||
import * as MarkdownIt from 'markdown-it';
|
|
||||||
import { NotificationKey } from '../models/NotificationModel';
|
import { NotificationKey } from '../models/NotificationModel';
|
||||||
import { profileUrl } from '../utils/urlUtils';
|
import { helpUrl, profileUrl } from '../utils/urlUtils';
|
||||||
|
import { userFlagToString } from '../models/UserFlagModel';
|
||||||
|
import renderMarkdown from '../utils/renderMarkdown';
|
||||||
|
|
||||||
const logger = Logger.create('notificationHandler');
|
const logger = Logger.create('notificationHandler');
|
||||||
|
|
||||||
|
@ -28,6 +29,34 @@ async function handleChangeAdminPasswordNotification(ctx: AppContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special notification that cannot be dismissed.
|
||||||
|
async function handleUserFlags(ctx: AppContext): Promise<NotificationView> {
|
||||||
|
const user = ctx.joplin.owner;
|
||||||
|
|
||||||
|
const flags = await ctx.joplin.models.userFlag().allByUserId(ctx.joplin.owner.id);
|
||||||
|
const flagStrings = flags.map(f => `- ${userFlagToString(f)}`);
|
||||||
|
|
||||||
|
if (!user.enabled || !user.can_upload) {
|
||||||
|
return {
|
||||||
|
id: 'accountDisabled',
|
||||||
|
messageHtml: renderMarkdown(`Your account is disabled for the following reason(s):\n\n${flagStrings}\n\nPlease check the [help section](${helpUrl()}) for further information or contact support.`),
|
||||||
|
levelClassName: levelClassName(NotificationLevel.Error),
|
||||||
|
closeUrl: '',
|
||||||
|
};
|
||||||
|
} else if (flags.length) {
|
||||||
|
// Actually currently all flags result in either disabled upload or
|
||||||
|
// disabled account, but keeping that here anyway just in case.
|
||||||
|
return {
|
||||||
|
id: 'accountFlags',
|
||||||
|
messageHtml: renderMarkdown(`The following issues have been detected on your account:\n\n${flagStrings}\n\nPlease check the [help section](${helpUrl()}) for further information or contact support.`),
|
||||||
|
levelClassName: levelClassName(NotificationLevel.Important),
|
||||||
|
closeUrl: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// async function handleSqliteInProdNotification(ctx: AppContext) {
|
// async function handleSqliteInProdNotification(ctx: AppContext) {
|
||||||
// if (!ctx.joplin.owner.is_admin) return;
|
// if (!ctx.joplin.owner.is_admin) return;
|
||||||
|
|
||||||
|
@ -49,15 +78,13 @@ function levelClassName(level: NotificationLevel): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[]> {
|
async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[]> {
|
||||||
const markdownIt = new MarkdownIt();
|
|
||||||
|
|
||||||
const notificationModel = ctx.joplin.models.notification();
|
const notificationModel = ctx.joplin.models.notification();
|
||||||
const notifications = await notificationModel.allUnreadByUserId(ctx.joplin.owner.id);
|
const notifications = await notificationModel.allUnreadByUserId(ctx.joplin.owner.id);
|
||||||
const views: NotificationView[] = [];
|
const views: NotificationView[] = [];
|
||||||
for (const n of notifications) {
|
for (const n of notifications) {
|
||||||
views.push({
|
views.push({
|
||||||
id: n.id,
|
id: n.id,
|
||||||
messageHtml: markdownIt.render(n.message),
|
messageHtml: renderMarkdown(n.message),
|
||||||
levelClassName: levelClassName(n.level),
|
levelClassName: levelClassName(n.level),
|
||||||
closeUrl: notificationModel.closeUrl(n.id),
|
closeUrl: notificationModel.closeUrl(n.id),
|
||||||
});
|
});
|
||||||
|
@ -78,7 +105,12 @@ export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||||
|
|
||||||
await handleChangeAdminPasswordNotification(ctx);
|
await handleChangeAdminPasswordNotification(ctx);
|
||||||
// await handleSqliteInProdNotification(ctx);
|
// await handleSqliteInProdNotification(ctx);
|
||||||
ctx.joplin.notifications = await makeNotificationViews(ctx);
|
const notificationViews = await makeNotificationViews(ctx);
|
||||||
|
|
||||||
|
const userFlagView = await handleUserFlags(ctx);
|
||||||
|
if (userFlagView) notificationViews.push(userFlagView);
|
||||||
|
|
||||||
|
ctx.joplin.notifications = notificationViews;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { isUniqueConstraintError } from '../db';
|
import { isUniqueConstraintError } from '../db';
|
||||||
import { User, UserFlag, UserFlagType, Uuid } from '../services/database/types';
|
import { User, UserFlag, UserFlagType, userFlagTypeToLabel, Uuid } from '../services/database/types';
|
||||||
|
import { formatDateTime } from '../utils/time';
|
||||||
import BaseModel from './BaseModel';
|
import BaseModel from './BaseModel';
|
||||||
|
|
||||||
interface AddRemoveOptions {
|
interface AddRemoveOptions {
|
||||||
|
@ -12,6 +13,10 @@ function defaultAddRemoveOptions(): AddRemoveOptions {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function userFlagToString(flag: UserFlag): string {
|
||||||
|
return `${userFlagTypeToLabel(flag.type)} on ${formatDateTime(flag.created_time)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export default class UserFlagModels extends BaseModel<UserFlag> {
|
export default class UserFlagModels extends BaseModel<UserFlag> {
|
||||||
|
|
||||||
public get tableName(): string {
|
public get tableName(): string {
|
||||||
|
@ -86,7 +91,7 @@ export default class UserFlagModels extends BaseModel<UserFlag> {
|
||||||
// be set directly (except maybe in tests) - instead the appropriate user
|
// be set directly (except maybe in tests) - instead the appropriate user
|
||||||
// flags should be set, and this function will derive the enabled/can_upload
|
// flags should be set, and this function will derive the enabled/can_upload
|
||||||
// properties from them.
|
// properties from them.
|
||||||
private async updateUserFromFlags(userId: Uuid) {
|
public async updateUserFromFlags(userId: Uuid) {
|
||||||
const flags = await this.allByUserId(userId);
|
const flags = await this.allByUserId(userId);
|
||||||
const user = await this.models().user().load(userId, { fields: ['id', 'can_upload', 'enabled'] });
|
const user = await this.models().user().load(userId, { fields: ['id', 'can_upload', 'enabled'] });
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { RouteType } from '../../utils/types';
|
||||||
import { AppContext, HttpMethod } from '../../utils/types';
|
import { AppContext, HttpMethod } from '../../utils/types';
|
||||||
import { bodyFields, formParse } from '../../utils/requestUtils';
|
import { bodyFields, formParse } from '../../utils/requestUtils';
|
||||||
import { ErrorForbidden, ErrorUnprocessableEntity } from '../../utils/errors';
|
import { ErrorForbidden, ErrorUnprocessableEntity } from '../../utils/errors';
|
||||||
import { User, UserFlagType, userFlagTypeToLabel, Uuid } from '../../services/database/types';
|
import { User, UserFlagType, Uuid } from '../../services/database/types';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { View } from '../../services/MustacheService';
|
import { View } from '../../services/MustacheService';
|
||||||
import defaultView from '../../utils/defaultView';
|
import defaultView from '../../utils/defaultView';
|
||||||
|
@ -21,6 +21,7 @@ import { createCsrfTag } from '../../utils/csrf';
|
||||||
import { formatDateTime } from '../../utils/time';
|
import { formatDateTime } from '../../utils/time';
|
||||||
import { cookieSet } from '../../utils/cookies';
|
import { cookieSet } from '../../utils/cookies';
|
||||||
import { startImpersonating, stopImpersonating } from './utils/users/impersonate';
|
import { startImpersonating, stopImpersonating } from './utils/users/impersonate';
|
||||||
|
import { userFlagToString } from '../../models/UserFlagModel';
|
||||||
|
|
||||||
export interface CheckRepeatPasswordInput {
|
export interface CheckRepeatPasswordInput {
|
||||||
password: string;
|
password: string;
|
||||||
|
@ -146,7 +147,7 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null
|
||||||
}
|
}
|
||||||
|
|
||||||
let userFlags: string[] = isNew ? null : (await models.userFlag().allByUserId(user.id)).map(f => {
|
let userFlags: string[] = isNew ? null : (await models.userFlag().allByUserId(user.id)).map(f => {
|
||||||
return `${formatDateTime(f.created_time)}: ${userFlagTypeToLabel(f.type)}`;
|
return userFlagToString(f);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userFlags || !userFlags.length || !owner.is_admin) userFlags = null;
|
if (!userFlags || !userFlags.length || !owner.is_admin) userFlags = null;
|
||||||
|
|
|
@ -5,6 +5,7 @@ export function markdownBodyToPlainText(md: string): string {
|
||||||
return md.replace(/\[.*\]\((.*)\)/g, '$1');
|
return md.replace(/\[.*\]\((.*)\)/g, '$1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: replace with renderMarkdown()
|
||||||
export function markdownBodyToHtml(md: string): string {
|
export function markdownBodyToHtml(md: string): string {
|
||||||
const markdownIt = new MarkdownIt({
|
const markdownIt = new MarkdownIt({
|
||||||
linkify: true,
|
linkify: true,
|
||||||
|
|
|
@ -82,6 +82,7 @@ export async function createTestUsers(db: DbConnection, config: Config, options:
|
||||||
);
|
);
|
||||||
await models.user().save({ id: user.id, password });
|
await models.user().save({ id: user.id, password });
|
||||||
await models.subscription().handlePayment(subscription.stripe_subscription_id, false);
|
await models.subscription().handlePayment(subscription.stripe_subscription_id, false);
|
||||||
|
await models.userFlag().add(user.id, UserFlagType.FailedPaymentWarning);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import MarkdownIt = require('markdown-it');
|
||||||
|
|
||||||
|
export default function(md: string): string {
|
||||||
|
const markdownIt = new MarkdownIt({
|
||||||
|
linkify: true,
|
||||||
|
});
|
||||||
|
return markdownIt.render(md);
|
||||||
|
}
|
|
@ -22,11 +22,14 @@ export function forgotPasswordUrl(): string {
|
||||||
return `${config().baseUrl}/password/forgot`;
|
return `${config().baseUrl}/password/forgot`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function profileUrl(): string {
|
export function profileUrl(): string {
|
||||||
return `${config().baseUrl}/users/me`;
|
return `${config().baseUrl}/users/me`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function helpUrl(): string {
|
||||||
|
return `${config().baseUrl}/help`;
|
||||||
|
}
|
||||||
|
|
||||||
export function confirmUrl(userId: Uuid, validationToken: string): string {
|
export function confirmUrl(userId: Uuid, validationToken: string): string {
|
||||||
return `${config().baseUrl}/users/${userId}/confirm?token=${validationToken}`;
|
return `${config().baseUrl}/users/${userId}/confirm?token=${validationToken}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
{{#global.hasNotifications}}
|
{{#global.hasNotifications}}
|
||||||
{{#global.notifications}}
|
{{#global.notifications}}
|
||||||
<div class="notification {{levelClassName}}" id="notification-{{id}}">
|
<div class="notification {{levelClassName}} content" id="notification-{{id}}">
|
||||||
<button data-close-url="{{closeUrl}}" data-id="{{id}}" class="delete close-notification-button"></button>
|
{{#closeUrl}}
|
||||||
|
<button data-close-url="{{closeUrl}}" data-id="{{id}}" class="delete close-notification-button"></button>
|
||||||
|
{{/closeUrl}}
|
||||||
{{{messageHtml}}}
|
{{{messageHtml}}}
|
||||||
</div>
|
</div>
|
||||||
{{/global.notifications}}
|
{{/global.notifications}}
|
||||||
|
|
Loading…
Reference in New Issue