You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-09-02 20:46:21 +02:00
Compare commits
20 Commits
server-v2.
...
server-v2.
Author | SHA1 | Date | |
---|---|---|---|
|
8c56cf98be | ||
|
18965494d9 | ||
|
ecd1602658 | ||
|
3c181906c2 | ||
|
9e1e144311 | ||
|
757c125bd3 | ||
|
2867b66cf1 | ||
|
5c6fd93753 | ||
|
ea65313bdb | ||
|
1711f7ec88 | ||
|
e0b5ef6630 | ||
|
4bbb3d1d58 | ||
|
fd769945b1 | ||
|
6e91d2784f | ||
|
881b2f17b1 | ||
|
e83cc58ea6 | ||
|
77def9f782 | ||
|
b23cc5d30a | ||
|
d8119bcf07 | ||
|
8bce259dc9 |
@@ -36,7 +36,7 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.8.5/Jo
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
---|---|---
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.7.5/joplin-v1.7.5.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.7.5/joplin-v1.7.5-32bit.apk)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.0.4/joplin-v2.0.4.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.0.4/joplin-v2.0.4-32bit.apk)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
|
2
packages/app-desktop/package-lock.json
generated
2
packages/app-desktop/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.0.9",
|
||||
"version": "2.0.10",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.0.9",
|
||||
"version": "2.0.10",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
@@ -65,4 +65,4 @@ if [[ $COMMANDS != "" ]]; then
|
||||
fi
|
||||
|
||||
cd "$ROOT_DIR/packages/app-desktop"
|
||||
npm start -- --env dev --profile "$PROFILE_DIR"
|
||||
npm start -- --profile "$PROFILE_DIR"
|
||||
|
@@ -141,8 +141,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097632
|
||||
versionName "2.0.1"
|
||||
versionCode 2097635
|
||||
versionName "2.0.4"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -486,13 +486,13 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
MARKETING_VERSION = 12.0.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -514,12 +514,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
MARKETING_VERSION = 12.0.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -659,14 +659,14 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
MARKETING_VERSION = 12.0.2;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
@@ -690,14 +690,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
MARKETING_VERSION = 12.0.2;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@@ -46,6 +46,8 @@ function isCannotSyncError(error: any): boolean {
|
||||
|
||||
export default class Synchronizer {
|
||||
|
||||
public static verboseMode: boolean = true;
|
||||
|
||||
private db_: any;
|
||||
private api_: any;
|
||||
private appType_: string;
|
||||
@@ -195,7 +197,11 @@ export default class Synchronizer {
|
||||
line.push(`(Remote ${s.join(', ')})`);
|
||||
}
|
||||
|
||||
this.logger().debug(line.join(': '));
|
||||
if (Synchronizer.verboseMode) {
|
||||
this.logger().info(line.join(': '));
|
||||
} else {
|
||||
this.logger().debug(line.join(': '));
|
||||
}
|
||||
|
||||
if (!this.progressReport_[action]) this.progressReport_[action] = 0;
|
||||
this.progressReport_[action] += actionCount;
|
||||
@@ -516,6 +522,40 @@ export default class Synchronizer {
|
||||
if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || action === 'updateRemote' || (action == 'itemConflict' && remote))) {
|
||||
const localState = await Resource.localState(local.id);
|
||||
if (localState.fetch_status !== Resource.FETCH_STATUS_DONE) {
|
||||
// This condition normally shouldn't happen
|
||||
// because the normal cases are as follow:
|
||||
//
|
||||
// - User creates a resource locally - in that
|
||||
// case the fetch status is DONE, so we cannot
|
||||
// end up here.
|
||||
//
|
||||
// - User fetches a new resource metadata, but
|
||||
// not the blob - in that case fetch status is
|
||||
// IDLE. However in that case, we cannot end
|
||||
// up in this place either, because the action
|
||||
// cannot be createRemote (because the
|
||||
// resource has not been created locally) or
|
||||
// updateRemote (because a resouce cannot be
|
||||
// modified locally unless the blob is present
|
||||
// too).
|
||||
//
|
||||
// Possibly the only case we can end up here is
|
||||
// if a resource metadata has been downloaded,
|
||||
// but not the blob yet. Then the sync target is
|
||||
// switched to a different one. In that case, we
|
||||
// can have a fetch status IDLE, with an
|
||||
// "updateRemote" action, if the timestamp of
|
||||
// the server resource is before the timestamp
|
||||
// of the local resource.
|
||||
//
|
||||
// In that case we can't do much so we mark the
|
||||
// resource as "cannot sync". Otherwise it will
|
||||
// throw the error "Processing a path that has
|
||||
// already been done" on the next loop, and sync
|
||||
// will never finish because we'll always end up
|
||||
// here.
|
||||
this.logger().info(`Need to upload a resource, but blob is not present: ${path}`);
|
||||
await handleCannotSyncItem(ItemClass, syncTargetId, local, 'Trying to upload resource, but only metadata is present.');
|
||||
action = null;
|
||||
} else {
|
||||
try {
|
||||
|
@@ -10,6 +10,7 @@ import Note from '../../models/Note';
|
||||
import Resource from '../../models/Resource';
|
||||
import ResourceFetcher from '../../services/ResourceFetcher';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
import { ModelType } from '../../BaseModel';
|
||||
|
||||
let insideBeforeEach = false;
|
||||
|
||||
@@ -333,6 +334,13 @@ describe('Synchronizer.resources', function() {
|
||||
await Resource.setLocalState(resource.id, { fetch_status: Resource.FETCH_STATUS_DONE });
|
||||
await synchronizerStart();
|
||||
|
||||
// At first, the resource is marked as cannot sync, so even after
|
||||
// synchronisation, nothing should happen.
|
||||
expect((await remoteResources()).length).toBe(0);
|
||||
|
||||
// The user can retry the item, in which case sync should happen.
|
||||
await BaseItem.saveSyncEnabled(ModelType.Resource, resource.id);
|
||||
await synchronizerStart();
|
||||
expect((await remoteResources()).length).toBe(1);
|
||||
}));
|
||||
|
||||
|
2
packages/server/package-lock.json
generated
2
packages/server/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.0.12",
|
||||
"version": "2.0.13",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "2.0.12",
|
||||
"version": "2.0.13",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
|
||||
|
@@ -38,6 +38,7 @@ export interface EnvVariables {
|
||||
|
||||
SIGNUP_ENABLED?: string;
|
||||
TERMS_ENABLED?: string;
|
||||
ACCOUNT_TYPES_ENABLED?: string;
|
||||
|
||||
ERROR_STACK_TRACES?: string;
|
||||
}
|
||||
@@ -152,6 +153,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
|
||||
userContentBaseUrl: env.USER_CONTENT_BASE_URL ? env.USER_CONTENT_BASE_URL : baseUrl,
|
||||
signupEnabled: env.SIGNUP_ENABLED === '1',
|
||||
termsEnabled: env.TERMS_ENABLED === '1',
|
||||
accountTypesEnabled: env.ACCOUNT_TYPES_ENABLED === '1',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
@@ -309,13 +309,15 @@ export default class ItemModel extends BaseModel<Item> {
|
||||
item.jop_encryption_applied = joplinItem.encryption_applied || 0;
|
||||
item.jop_share_id = joplinItem.share_id || '';
|
||||
|
||||
delete joplinItem.id;
|
||||
delete joplinItem.parent_id;
|
||||
delete joplinItem.share_id;
|
||||
delete joplinItem.type_;
|
||||
delete joplinItem.encryption_applied;
|
||||
const joplinItemToSave = { ...joplinItem };
|
||||
|
||||
item.content = Buffer.from(JSON.stringify(joplinItem));
|
||||
delete joplinItemToSave.id;
|
||||
delete joplinItemToSave.parent_id;
|
||||
delete joplinItemToSave.share_id;
|
||||
delete joplinItemToSave.type_;
|
||||
delete joplinItemToSave.encryption_applied;
|
||||
|
||||
item.content = Buffer.from(JSON.stringify(joplinItemToSave));
|
||||
} else {
|
||||
item.content = buffer;
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import { formatBytes, MB } from '../utils/bytes';
|
||||
|
||||
export enum AccountType {
|
||||
Default = 0,
|
||||
Free = 1,
|
||||
Basic = 1,
|
||||
Pro = 2,
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ interface AccountTypeProperties {
|
||||
max_item_size: number;
|
||||
}
|
||||
|
||||
interface AccountTypeSelectOptions {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function accountTypeProperties(accountType: AccountType): AccountTypeProperties {
|
||||
const types: AccountTypeProperties[] = [
|
||||
{
|
||||
@@ -26,7 +31,7 @@ export function accountTypeProperties(accountType: AccountType): AccountTypeProp
|
||||
max_item_size: 0,
|
||||
},
|
||||
{
|
||||
account_type: AccountType.Free,
|
||||
account_type: AccountType.Basic,
|
||||
can_share: 0,
|
||||
max_item_size: 10 * MB,
|
||||
},
|
||||
@@ -37,7 +42,26 @@ export function accountTypeProperties(accountType: AccountType): AccountTypeProp
|
||||
},
|
||||
];
|
||||
|
||||
return types.find(a => a.account_type === accountType);
|
||||
const type = types.find(a => a.account_type === accountType);
|
||||
if (!type) throw new Error(`Invalid account type: ${accountType}`);
|
||||
return type;
|
||||
}
|
||||
|
||||
export function accountTypeOptions(): AccountTypeSelectOptions[] {
|
||||
return [
|
||||
{
|
||||
value: AccountType.Default,
|
||||
label: 'Default',
|
||||
},
|
||||
{
|
||||
value: AccountType.Basic,
|
||||
label: 'Basic',
|
||||
},
|
||||
{
|
||||
value: AccountType.Pro,
|
||||
label: 'Pro',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default class UserModel extends BaseModel<User> {
|
||||
@@ -68,6 +92,8 @@ export default class UserModel extends BaseModel<User> {
|
||||
if ('full_name' in object) user.full_name = object.full_name;
|
||||
if ('max_item_size' in object) user.max_item_size = object.max_item_size;
|
||||
if ('can_share' in object) user.can_share = object.can_share;
|
||||
if ('account_type' in object) user.account_type = object.account_type;
|
||||
if ('must_set_password' in object) user.must_set_password = object.must_set_password;
|
||||
|
||||
return user;
|
||||
}
|
||||
@@ -94,7 +120,10 @@ export default class UserModel extends BaseModel<User> {
|
||||
if (!user.is_admin && resource.id !== user.id) throw new ErrorForbidden('non-admin user cannot modify another user');
|
||||
if (!user.is_admin && 'is_admin' in resource) throw new ErrorForbidden('non-admin user cannot make themselves an admin');
|
||||
if (user.is_admin && user.id === resource.id && 'is_admin' in resource && !resource.is_admin) throw new ErrorForbidden('admin user cannot make themselves a non-admin');
|
||||
if (!user.is_admin && resource.max_item_size !== previousResource.max_item_size) throw new ErrorForbidden('non-admin user cannot change max_item_size');
|
||||
if ('max_item_size' in resource && !user.is_admin && resource.max_item_size !== previousResource.max_item_size) throw new ErrorForbidden('non-admin user cannot change max_item_size');
|
||||
if ('can_share' in resource && !user.is_admin && resource.can_share !== previousResource.can_share) throw new ErrorForbidden('non-admin user cannot change can_share');
|
||||
if ('account_type' in resource && !user.is_admin && resource.account_type !== previousResource.account_type) throw new ErrorForbidden('non-admin user cannot change account_type');
|
||||
if ('must_set_password' in resource && !user.is_admin && resource.must_set_password !== previousResource.must_set_password) throw new ErrorForbidden('non-admin user cannot change must_set_password');
|
||||
}
|
||||
|
||||
if (action === AclAction.Delete) {
|
||||
@@ -108,17 +137,17 @@ export default class UserModel extends BaseModel<User> {
|
||||
}
|
||||
|
||||
public async checkMaxItemSizeLimit(user: User, buffer: Buffer, item: Item, joplinItem: any) {
|
||||
const itemTitle = joplinItem ? joplinItem.title || '' : '';
|
||||
const isNote = joplinItem && joplinItem.type_ === ModelType.Note;
|
||||
|
||||
// If the item is encrypted, we apply a multipler because encrypted
|
||||
// items can be much larger (seems to be up to twice the size but for
|
||||
// safety let's go with 2.2).
|
||||
const maxSize = user.max_item_size * (item.jop_encryption_applied ? 2.2 : 1);
|
||||
if (maxSize && buffer.byteLength > maxSize) {
|
||||
const itemTitle = joplinItem ? joplinItem.title || '' : '';
|
||||
const isNote = joplinItem && joplinItem.type_ === ModelType.Note;
|
||||
|
||||
throw new ErrorPayloadTooLarge(_('Cannot save %s "%s" because it is larger than than the allowed limit (%s)',
|
||||
isNote ? _('note') : _('attachment'),
|
||||
itemTitle ? itemTitle : name,
|
||||
itemTitle ? itemTitle : item.name,
|
||||
formatBytes(user.max_item_size)
|
||||
));
|
||||
}
|
||||
@@ -147,7 +176,7 @@ export default class UserModel extends BaseModel<User> {
|
||||
|
||||
if (options.isNew) {
|
||||
if (!user.email) throw new ErrorUnprocessableEntity('email must be set');
|
||||
if (!user.password) throw new ErrorUnprocessableEntity('password must be set');
|
||||
if (!user.password && !user.must_set_password) throw new ErrorUnprocessableEntity('password must be set');
|
||||
} else {
|
||||
if ('email' in user && !user.email) throw new ErrorUnprocessableEntity('email must be set');
|
||||
if ('password' in user && !user.password) throw new ErrorUnprocessableEntity('password must be set');
|
||||
@@ -196,6 +225,17 @@ export default class UserModel extends BaseModel<User> {
|
||||
await this.save({ id: user.id, email_confirmed: 1 });
|
||||
}
|
||||
|
||||
// public async saveWithAccountType(accountType:AccountType, user: User, options: SaveOptions = {}): Promise<User> {
|
||||
// if (accountType !== AccountType.Default) {
|
||||
// user = {
|
||||
// ...user,
|
||||
// ...accountTypeProperties(accountType),
|
||||
// };
|
||||
// }
|
||||
|
||||
// return this.save(user, options);
|
||||
// }
|
||||
|
||||
// Note that when the "password" property is provided, it is going to be
|
||||
// hashed automatically. It means that it is not safe to do:
|
||||
//
|
||||
|
@@ -41,7 +41,7 @@ describe('index_signup', function() {
|
||||
// Check that the user has been created
|
||||
const user = await models().user().loadByEmail('toto@example.com');
|
||||
expect(user).toBeTruthy();
|
||||
expect(user.account_type).toBe(AccountType.Free);
|
||||
expect(user.account_type).toBe(AccountType.Basic);
|
||||
expect(user.email_confirmed).toBe(0);
|
||||
expect(user.can_share).toBe(0);
|
||||
expect(user.max_item_size).toBe(10 * MB);
|
||||
|
@@ -44,7 +44,7 @@ router.post('signup', async (_path: SubPath, ctx: AppContext) => {
|
||||
const password = checkPassword(formUser, true);
|
||||
|
||||
const user = await ctx.models.user().save({
|
||||
...accountTypeProperties(AccountType.Free),
|
||||
...accountTypeProperties(AccountType.Basic),
|
||||
email: formUser.email,
|
||||
full_name: formUser.full_name,
|
||||
password,
|
||||
|
@@ -85,6 +85,7 @@ describe('index_users', function() {
|
||||
expect(!!newUser.is_admin).toBe(false);
|
||||
expect(!!newUser.email).toBe(true);
|
||||
expect(newUser.max_item_size).toBe(0);
|
||||
expect(newUser.must_set_password).toBe(0);
|
||||
|
||||
const userModel = models().user();
|
||||
const userFromModel: User = await userModel.load(newUser.id);
|
||||
@@ -93,6 +94,18 @@ describe('index_users', function() {
|
||||
expect(userFromModel.password === '123456').toBe(false); // Password has been hashed
|
||||
});
|
||||
|
||||
test('should ask user to set password if not set on creation', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
await postUser(session.id, 'test@example.com', '', {
|
||||
max_item_size: '',
|
||||
});
|
||||
const newUser = await models().user().loadByEmail('test@example.com');
|
||||
|
||||
expect(newUser.must_set_password).toBe(1);
|
||||
expect(!!newUser.password).toBe(true);
|
||||
});
|
||||
|
||||
test('new user should be able to login', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
@@ -123,7 +136,7 @@ describe('index_users', function() {
|
||||
});
|
||||
|
||||
test('should change user properties', async function() {
|
||||
const { user, session } = await createUserAndSession(1, true);
|
||||
const { user, session } = await createUserAndSession(1, false);
|
||||
|
||||
const userModel = models().user();
|
||||
|
||||
@@ -298,7 +311,11 @@ describe('index_users', function() {
|
||||
await expectHttpError(async () => execRequest(adminSession.id, 'POST', `users/${admin.id}`, { delete_button: true }), ErrorForbidden.httpCode);
|
||||
|
||||
// non-admin cannot change max_item_size
|
||||
await expectHttpError(async () => patchUser(session1.id, { id: admin.id, max_item_size: 1000 }), ErrorForbidden.httpCode);
|
||||
await expectHttpError(async () => patchUser(session1.id, { id: user1.id, max_item_size: 1000 }), ErrorForbidden.httpCode);
|
||||
|
||||
// non-admin cannot change can_share
|
||||
await models().user().save({ id: user1.id, can_share: 0 });
|
||||
await expectHttpError(async () => patchUser(session1.id, { id: user1.id, can_share: 1 }), ErrorForbidden.httpCode);
|
||||
});
|
||||
|
||||
|
||||
|
@@ -11,6 +11,8 @@ import defaultView from '../../utils/defaultView';
|
||||
import { AclAction } from '../../models/BaseModel';
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
import { formatBytes } from '../../utils/bytes';
|
||||
import { accountTypeOptions, accountTypeProperties } from '../../models/UserModel';
|
||||
import uuidgen from '../../utils/uuidgen';
|
||||
|
||||
interface CheckPasswordInput {
|
||||
password: string;
|
||||
@@ -29,25 +31,39 @@ export function checkPassword(fields: CheckPasswordInput, required: boolean): st
|
||||
}
|
||||
|
||||
function makeUser(isNew: boolean, fields: any): User {
|
||||
const user: User = {};
|
||||
let user: User = {};
|
||||
|
||||
if ('email' in fields) user.email = fields.email;
|
||||
if ('full_name' in fields) user.full_name = fields.full_name;
|
||||
if ('is_admin' in fields) user.is_admin = fields.is_admin;
|
||||
if ('max_item_size' in fields) user.max_item_size = fields.max_item_size || 0;
|
||||
user.can_share = fields.can_share ? 1 : 0;
|
||||
if ('can_share' in fields) user.can_share = fields.can_share ? 1 : 0;
|
||||
|
||||
if ('account_type' in fields) {
|
||||
user.account_type = Number(fields.account_type);
|
||||
user = {
|
||||
...user,
|
||||
...accountTypeProperties(user.account_type),
|
||||
};
|
||||
}
|
||||
|
||||
const password = checkPassword(fields, false);
|
||||
if (password) user.password = password;
|
||||
|
||||
if (!isNew) user.id = fields.id;
|
||||
|
||||
if (isNew) {
|
||||
user.must_set_password = user.password ? 0 : 1;
|
||||
user.password = user.password ? user.password : uuidgen();
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
function defaultUser(): User {
|
||||
return {
|
||||
can_share: 1,
|
||||
max_item_size: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,6 +123,14 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null
|
||||
view.content.postUrl = postUrl;
|
||||
view.content.showDeleteButton = !isNew && !!owner.is_admin && owner.id !== user.id;
|
||||
|
||||
if (config().accountTypesEnabled) {
|
||||
view.content.showAccountTypes = true;
|
||||
view.content.accountTypes = accountTypeOptions().map((o: any) => {
|
||||
o.selected = user.account_type === o.value;
|
||||
return o;
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
});
|
||||
|
||||
|
@@ -80,6 +80,7 @@ export interface Config {
|
||||
userContentBaseUrl: string;
|
||||
signupEnabled: boolean;
|
||||
termsEnabled: boolean;
|
||||
accountTypesEnabled: boolean;
|
||||
showErrorStackTraces: boolean;
|
||||
database: DatabaseConfig;
|
||||
mailer: MailerConfig;
|
||||
|
@@ -15,6 +15,20 @@
|
||||
</div>
|
||||
</div>
|
||||
{{#global.owner.is_admin}}
|
||||
{{#showAccountTypes}}
|
||||
<div class="field">
|
||||
<label class="label">Account type</label>
|
||||
<div class="select">
|
||||
<select name="account_type">
|
||||
{{#accountTypes}}
|
||||
<option value="{{value}}" {{#selected}}selected{{/selected}}>{{label}}</option>
|
||||
{{/accountTypes}}
|
||||
</select>
|
||||
</div>
|
||||
<p class="help">If the account type is anything other than Default, the account-specific properties will be applied.</p>
|
||||
</div>
|
||||
{{/showAccountTypes}}
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Max item size</label>
|
||||
<div class="control">
|
||||
@@ -39,7 +53,10 @@
|
||||
<div class="control">
|
||||
<input class="input" type="password" name="password2" autocomplete="new-password"/>
|
||||
</div>
|
||||
</div>
|
||||
{{#global.owner.is_admin}}
|
||||
<p class="help">When creating a new user, if no password is specified the user will have to set it by following the link in their email.</p>
|
||||
{{/global.owner.is_admin}}
|
||||
</div>
|
||||
<div class="control">
|
||||
<input type="submit" name="post_button" class="button is-primary" value="{{buttonTitle}}" />
|
||||
{{#showDeleteButton}}
|
||||
|
@@ -351,6 +351,7 @@ async function main() {
|
||||
|
||||
let publishFormat = 'full';
|
||||
if (['android', 'ios'].indexOf(platform) >= 0) publishFormat = 'simple';
|
||||
if (argv.publishFormat) publishFormat = argv.publishFormat;
|
||||
let changelog = createChangeLog(filteredLogs, { publishFormat: publishFormat });
|
||||
|
||||
const changelogFixes = [];
|
||||
|
@@ -58,12 +58,12 @@ async function updatePluginGeneratorTemplateVersion(manifestPath, majorMinorVers
|
||||
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'));
|
||||
}
|
||||
|
||||
// Need this hack to transform 1.x.x into 10.x.x due to some mistake
|
||||
// Need this hack to transform x.y.z into 1x.y.z due to some mistake
|
||||
// on one of the release and the App Store won't allow decreasing
|
||||
// the major version number.
|
||||
function iosVersionHack(majorMinorVersion) {
|
||||
const p = majorMinorVersion.split('.');
|
||||
p[0] = `${p[0]}0`;
|
||||
p[0] = `1${p[0]}`;
|
||||
return p.join('.');
|
||||
}
|
||||
|
||||
|
@@ -65,7 +65,7 @@ async function insertChangelog(tag: string, changelogPath: string, changelog: st
|
||||
}
|
||||
|
||||
export async function completeReleaseWithChangelog(changelogPath: string, newVersion: string, newTag: string, appName: string, isPreRelease: boolean) {
|
||||
const changelog = (await execCommand2(`node ${rootDir}/packages/tools/git-changelog ${newTag}`, { })).trim();
|
||||
const changelog = (await execCommand2(`node ${rootDir}/packages/tools/git-changelog ${newTag} --publish-format full`, { })).trim();
|
||||
|
||||
const newChangelog = await insertChangelog(newTag, changelogPath, changelog, isPreRelease);
|
||||
|
||||
|
@@ -1 +1,22 @@
|
||||
# Joplin Android app changelog
|
||||
|
||||
## [android-v2.0.4](https://github.com/laurent22/joplin/releases/tag/android-v2.0.4) - 2021-06-16T12:15:56Z
|
||||
|
||||
- Improved: Prevent sync process from being stuck when the download state of a resource is invalid (5c6fd93)
|
||||
|
||||
## [android-v2.0.3](https://github.com/laurent22/joplin/releases/tag/android-v2.0.3) (Pre-release) - 2021-06-16T09:48:58Z
|
||||
|
||||
- Improved: Verbose mode for synchronizer (4bbb3d1)
|
||||
|
||||
## [android-v2.0.2](https://github.com/laurent22/joplin/releases/tag/android-v2.0.2) - 2021-06-15T20:03:21Z
|
||||
|
||||
- Improved: Conflict notes will now populate a new field with the ID of the conflict note. (#5049 by [@Ahmad45123](https://github.com/Ahmad45123))
|
||||
- Improved: Filter out form elements from note body to prevent potential XSS (thanks to Dmytro Vdovychinskiy for the PoC) (feaecf7)
|
||||
- Improved: Focus note editor where tapped instead of scrolling to the end (#4998) (#4216 by Roman Musin)
|
||||
- Improved: Improve search with Asian scripts (#5018) (#4613 by [@mablin7](https://github.com/mablin7))
|
||||
- Fixed: Fixed and improved alarm notifications (#4984) (#4912 by Roman Musin)
|
||||
- Fixed: Fixed opening URLs that contain non-alphabetical characters (#4494)
|
||||
- Fixed: Fixed user content URLs when sharing note via Joplin Server (2cf7067)
|
||||
- Fixed: Inline Katex gets broken when editing in Rich Text editor (#5052) (#5025 by [@Subhra264](https://github.com/Subhra264))
|
||||
- Fixed: Items are filtered in the API search (#5017) (#5007 by [@JackGruber](https://github.com/JackGruber))
|
||||
- Fixed: Wrong field removed in API search (#5066 by [@JackGruber](https://github.com/JackGruber))
|
||||
|
@@ -1,5 +1,12 @@
|
||||
# Joplin Server Changelog
|
||||
|
||||
## [server-v2.0.13](https://github.com/laurent22/joplin/releases/tag/server-v2.0.13) - 2021-06-16T14:28:20Z
|
||||
|
||||
- Improved: Allow creating a new user with no password, which must be set via email confirmation (1896549)
|
||||
- Improved: Allow creating a user with a specific account type from admin UI (ecd1602)
|
||||
- Fixed: Fixed error message when item is over the limit (ea65313)
|
||||
- Fixed: Fixed issue with user not being able to modify own profile (3c18190)
|
||||
|
||||
## [server-v2.0.12](https://github.com/laurent22/joplin/releases/tag/server-v2.0.12) - 2021-06-15T16:24:42Z
|
||||
|
||||
- Fixed: Fixed handling of user content URL (31121c8)
|
||||
|
Reference in New Issue
Block a user