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 { Notification } from '../services/database/types';
|
||||
import { Notification, UserFlagType } from '../services/database/types';
|
||||
import { defaultAdminEmail, defaultAdminPassword } from '../db';
|
||||
import notificationHandler from './notificationHandler';
|
||||
|
||||
|
@ -76,4 +76,15 @@ describe('notificationHandler', function() {
|
|||
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 { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import * as MarkdownIt from 'markdown-it';
|
||||
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');
|
||||
|
||||
|
@ -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) {
|
||||
// if (!ctx.joplin.owner.is_admin) return;
|
||||
|
||||
|
@ -49,15 +78,13 @@ function levelClassName(level: NotificationLevel): string {
|
|||
}
|
||||
|
||||
async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[]> {
|
||||
const markdownIt = new MarkdownIt();
|
||||
|
||||
const notificationModel = ctx.joplin.models.notification();
|
||||
const notifications = await notificationModel.allUnreadByUserId(ctx.joplin.owner.id);
|
||||
const views: NotificationView[] = [];
|
||||
for (const n of notifications) {
|
||||
views.push({
|
||||
id: n.id,
|
||||
messageHtml: markdownIt.render(n.message),
|
||||
messageHtml: renderMarkdown(n.message),
|
||||
levelClassName: levelClassName(n.level),
|
||||
closeUrl: notificationModel.closeUrl(n.id),
|
||||
});
|
||||
|
@ -78,7 +105,12 @@ export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
|||
|
||||
await handleChangeAdminPasswordNotification(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) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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';
|
||||
|
||||
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> {
|
||||
|
||||
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
|
||||
// flags should be set, and this function will derive the enabled/can_upload
|
||||
// properties from them.
|
||||
private async updateUserFromFlags(userId: Uuid) {
|
||||
public async updateUserFromFlags(userId: Uuid) {
|
||||
const flags = await this.allByUserId(userId);
|
||||
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 { bodyFields, formParse } from '../../utils/requestUtils';
|
||||
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 { View } from '../../services/MustacheService';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
|
@ -21,6 +21,7 @@ import { createCsrfTag } from '../../utils/csrf';
|
|||
import { formatDateTime } from '../../utils/time';
|
||||
import { cookieSet } from '../../utils/cookies';
|
||||
import { startImpersonating, stopImpersonating } from './utils/users/impersonate';
|
||||
import { userFlagToString } from '../../models/UserFlagModel';
|
||||
|
||||
export interface CheckRepeatPasswordInput {
|
||||
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 => {
|
||||
return `${formatDateTime(f.created_time)}: ${userFlagTypeToLabel(f.type)}`;
|
||||
return userFlagToString(f);
|
||||
});
|
||||
|
||||
if (!userFlags || !userFlags.length || !owner.is_admin) userFlags = null;
|
||||
|
|
|
@ -5,6 +5,7 @@ export function markdownBodyToPlainText(md: string): string {
|
|||
return md.replace(/\[.*\]\((.*)\)/g, '$1');
|
||||
}
|
||||
|
||||
// TODO: replace with renderMarkdown()
|
||||
export function markdownBodyToHtml(md: string): string {
|
||||
const markdownIt = new MarkdownIt({
|
||||
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.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`;
|
||||
}
|
||||
|
||||
|
||||
export function profileUrl(): string {
|
||||
return `${config().baseUrl}/users/me`;
|
||||
}
|
||||
|
||||
export function helpUrl(): string {
|
||||
return `${config().baseUrl}/help`;
|
||||
}
|
||||
|
||||
export function confirmUrl(userId: Uuid, validationToken: string): string {
|
||||
return `${config().baseUrl}/users/${userId}/confirm?token=${validationToken}`;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{{#global.hasNotifications}}
|
||||
{{#global.notifications}}
|
||||
<div class="notification {{levelClassName}}" id="notification-{{id}}">
|
||||
<button data-close-url="{{closeUrl}}" data-id="{{id}}" class="delete close-notification-button"></button>
|
||||
<div class="notification {{levelClassName}} content" id="notification-{{id}}">
|
||||
{{#closeUrl}}
|
||||
<button data-close-url="{{closeUrl}}" data-id="{{id}}" class="delete close-notification-button"></button>
|
||||
{{/closeUrl}}
|
||||
{{{messageHtml}}}
|
||||
</div>
|
||||
{{/global.notifications}}
|
||||
|
|
Loading…
Reference in New Issue