mirror of
https://github.com/laurent22/joplin.git
synced 2025-04-04 21:35:03 +02:00
Server: Allow manually deleting a user flag
This commit is contained in:
parent
5da820aa0a
commit
3a11885705
packages/server
public/css/index
src
middleware
models
routes/index
utils
views/index
@ -1,3 +1,7 @@
|
|||||||
#user_cancel_subscription_link {
|
#user_cancel_subscription_link {
|
||||||
display: none;
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-flags ul > li {
|
||||||
|
list-style-type: none;
|
||||||
}
|
}
|
@ -34,7 +34,7 @@ async function handleUserFlags(ctx: AppContext): Promise<NotificationView> {
|
|||||||
const user = ctx.joplin.owner;
|
const user = ctx.joplin.owner;
|
||||||
|
|
||||||
const flags = await ctx.joplin.models.userFlag().allByUserId(ctx.joplin.owner.id);
|
const flags = await ctx.joplin.models.userFlag().allByUserId(ctx.joplin.owner.id);
|
||||||
const flagStrings = flags.map(f => `- ${userFlagToString(f)}`);
|
const flagStrings = flags.map(f => `- ${userFlagToString(f)}`).join('\n');
|
||||||
|
|
||||||
if (!user.enabled || !user.can_upload) {
|
if (!user.enabled || !user.can_upload) {
|
||||||
return {
|
return {
|
||||||
|
@ -79,6 +79,8 @@ export default class UserFlagModels extends BaseModel<UserFlag> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async removeMulti(userId: Uuid, flagTypes: UserFlagType[]) {
|
public async removeMulti(userId: Uuid, flagTypes: UserFlagType[]) {
|
||||||
|
if (!flagTypes.length) return;
|
||||||
|
|
||||||
await this.withTransaction(async () => {
|
await this.withTransaction(async () => {
|
||||||
for (const flagType of flagTypes) {
|
for (const flagType of flagTypes) {
|
||||||
await this.remove(userId, flagType, { updateUser: false });
|
await this.remove(userId, flagType, { updateUser: false });
|
||||||
|
@ -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, Uuid } from '../../services/database/types';
|
import { User, UserFlag, 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';
|
||||||
@ -146,11 +146,18 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null
|
|||||||
postUrl = `${config().baseUrl}/users/${user.id}`;
|
postUrl = `${config().baseUrl}/users/${user.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let userFlags: string[] = isNew ? [] : (await models.userFlag().allByUserId(user.id)).map(f => {
|
interface UserFlagView extends UserFlag {
|
||||||
return userFlagToString(f);
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let userFlagViews: UserFlagView[] = isNew ? [] : (await models.userFlag().allByUserId(user.id)).map(f => {
|
||||||
|
return {
|
||||||
|
...f,
|
||||||
|
message: userFlagToString(f),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!owner.is_admin) userFlags = [];
|
if (!owner.is_admin) userFlagViews = [];
|
||||||
|
|
||||||
const subscription = !isNew ? await ctx.joplin.models.subscription().byUserId(userId) : null;
|
const subscription = !isNew ? await ctx.joplin.models.subscription().byUserId(userId) : null;
|
||||||
|
|
||||||
@ -179,8 +186,8 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null
|
|||||||
view.content.showResetPasswordButton = !isNew && owner.is_admin && user.enabled;
|
view.content.showResetPasswordButton = !isNew && owner.is_admin && user.enabled;
|
||||||
view.content.canShareFolderOptions = yesNoDefaultOptions(user, 'can_share_folder');
|
view.content.canShareFolderOptions = yesNoDefaultOptions(user, 'can_share_folder');
|
||||||
view.content.canUploadOptions = yesNoOptions(user, 'can_upload');
|
view.content.canUploadOptions = yesNoOptions(user, 'can_upload');
|
||||||
view.content.hasFlags = !!userFlags.length;
|
view.content.hasFlags = !!userFlagViews.length;
|
||||||
view.content.userFlags = userFlags;
|
view.content.userFlagViews = userFlagViews;
|
||||||
view.content.stripePortalUrl = stripePortalUrl();
|
view.content.stripePortalUrl = stripePortalUrl();
|
||||||
|
|
||||||
view.jsFiles.push('zxcvbn');
|
view.jsFiles.push('zxcvbn');
|
||||||
@ -296,6 +303,7 @@ interface FormFields {
|
|||||||
// user_cancel_subscription_button: string;
|
// user_cancel_subscription_button: string;
|
||||||
impersonate_button: string;
|
impersonate_button: string;
|
||||||
stop_impersonate_button: string;
|
stop_impersonate_button: string;
|
||||||
|
delete_user_flags: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.post('users', async (path: SubPath, ctx: AppContext) => {
|
router.post('users', async (path: SubPath, ctx: AppContext) => {
|
||||||
@ -326,13 +334,14 @@ router.post('users', async (path: SubPath, ctx: AppContext) => {
|
|||||||
|
|
||||||
await models.user().save(userToSave, { isNew: false });
|
await models.user().save(userToSave, { isNew: false });
|
||||||
}
|
}
|
||||||
// } else if (fields.user_cancel_subscription_button) {
|
// } else if (fields.user_cancel_subscription_button) {
|
||||||
// await cancelSubscriptionByUserId(models, userId);
|
// await cancelSubscriptionByUserId(models, userId);
|
||||||
// const sessionId = contextSessionId(ctx, false);
|
// const sessionId = contextSessionId(ctx, false);
|
||||||
// if (sessionId) {
|
// if (sessionId) {
|
||||||
// await models.session().logout(sessionId);
|
// await models.session().logout(sessionId);
|
||||||
// return redirect(ctx, config().baseUrl);
|
// return redirect(ctx, config().baseUrl);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
} else if (fields.stop_impersonate_button) {
|
} else if (fields.stop_impersonate_button) {
|
||||||
await stopImpersonating(ctx);
|
await stopImpersonating(ctx);
|
||||||
return redirect(ctx, config().baseUrl);
|
return redirect(ctx, config().baseUrl);
|
||||||
@ -354,6 +363,16 @@ router.post('users', async (path: SubPath, ctx: AppContext) => {
|
|||||||
await updateSubscriptionType(models, userId, AccountType.Basic);
|
await updateSubscriptionType(models, userId, AccountType.Basic);
|
||||||
} else if (fields.update_subscription_pro_button) {
|
} else if (fields.update_subscription_pro_button) {
|
||||||
await updateSubscriptionType(models, userId, AccountType.Pro);
|
await updateSubscriptionType(models, userId, AccountType.Pro);
|
||||||
|
} else if (fields.delete_user_flags) {
|
||||||
|
const userFlagTypes: UserFlagType[] = [];
|
||||||
|
for (const key of Object.keys(fields)) {
|
||||||
|
if (key.startsWith('user_flag_')) {
|
||||||
|
const type = Number(key.substr(10));
|
||||||
|
userFlagTypes.push(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await models.userFlag().removeMulti(userId, userFlagTypes);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid form button');
|
throw new Error('Invalid form button');
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ export async function csrfCheck(ctx: AppContext, isPublicRoute: boolean) {
|
|||||||
|
|
||||||
const fields = await bodyFields<BodyWithCsrfToken>(ctx.req);
|
const fields = await bodyFields<BodyWithCsrfToken>(ctx.req);
|
||||||
if (!fields._csrf) throw new ErrorForbidden('CSRF token is missing');
|
if (!fields._csrf) throw new ErrorForbidden('CSRF token is missing');
|
||||||
|
if (Array.isArray(fields._csrf)) throw new Error('Multiple CSRF tokens inside the form!');
|
||||||
|
|
||||||
if (!(await ctx.joplin.models.token().isValid(userId, fields._csrf))) {
|
if (!(await ctx.joplin.models.token().isValid(userId, fields._csrf))) {
|
||||||
throw new ErrorForbidden(`Invalid CSRF token: ${fields._csrf}`);
|
throw new ErrorForbidden(`Invalid CSRF token: ${fields._csrf}`);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<h1 class="title">Your profile</h1>
|
<h1 class="title">Your profile</h1>
|
||||||
|
|
||||||
<form id="user_form" action="{{{postUrl}}}" method="POST">
|
<form id="user_form" action="{{{postUrl}}}" method="POST" class="block">
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{{> errorBanner}}
|
{{> errorBanner}}
|
||||||
@ -140,19 +140,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/subscription}}
|
{{/subscription}}
|
||||||
|
|
||||||
{{#hasFlags}}
|
|
||||||
<div class="content">
|
|
||||||
<h1 class="title">Flags</h1>
|
|
||||||
{{#userFlags}}
|
|
||||||
<ul>
|
|
||||||
<li>{{.}}</li>
|
|
||||||
</ul>
|
|
||||||
{{/userFlags}}
|
|
||||||
</div>
|
|
||||||
{{/hasFlags}}
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{{#hasFlags}}
|
||||||
|
<div class="content user-flags block">
|
||||||
|
<h1 class="title">Flags</h1>
|
||||||
|
<form action="{{{postUrl}}}" method="POST">
|
||||||
|
{{{csrfTag}}}
|
||||||
|
{{#userFlagViews}}
|
||||||
|
<ul>
|
||||||
|
<li><label class="checkbox"><input type="checkbox" name="user_flag_{{type}}"> {{message}}</label></li>
|
||||||
|
</ul>
|
||||||
|
{{/userFlagViews}}
|
||||||
|
<input type="submit" name="delete_user_flags" class="button is-warning" value="Delete selected flags" />
|
||||||
|
<p class="help">Note: normally it should not be needed to manually delete a flag because that's automatically handled by the system. So if it's necessary it means there's a bug that should be fixed.</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{/hasFlags}}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(() => {
|
$(() => {
|
||||||
document.getElementById("user_form").addEventListener('submit', function(event) {
|
document.getElementById("user_form").addEventListener('submit', function(event) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user