Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 5739-dockerRefactor
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Alex Moreno 2023-11-21 11:52:59 +01:00
commit 55e5c04300
25 changed files with 331 additions and 143 deletions

View File

@ -16,10 +16,15 @@ RUN apt-get update \
libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \ libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \ libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \
libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \ fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
&& npm -g install pm2 \
&& apt-get clean \ # Extra dependencies
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
samba-common-bin samba-dsdb-modules\
&& rm -rf /var/lib/apt/lists/* \
&& npm -g install pm2
# Salix # Salix

View File

@ -51,7 +51,7 @@ module.exports = Self => {
} }
const validateLogin = await Self.validateLogin(user, password); const validateLogin = await Self.validateLogin(user, password);
await Self.app.models.SignInLog.create({ await Self.app.models.SignInLog.create({
id: validateLogin.token, token: validateLogin.token,
userFk: vnUser.id, userFk: vnUser.id,
ip: ctx.req.ip ip: ctx.req.ip
}); });

View File

@ -12,8 +12,21 @@ describe('VnUser Sign-in()', () => {
}, },
args: {} args: {}
}; };
const {VnUser, AccessToken} = models; const {VnUser, AccessToken, SignInLog} = models;
describe('when credentials are correct', () => { describe('when credentials are correct', () => {
it('should return the token if user uses email', async() => {
let login = await VnUser.signIn(unauthCtx, 'salesAssistant@mydomain.com', 'nightmare');
let accessToken = await AccessToken.findById(login.token);
let ctx = {req: {accessToken: accessToken}};
let signInLog = await SignInLog.find({where: {token: accessToken.id}});
expect(signInLog.length).toEqual(1);
expect(signInLog[0].userFk).toEqual(accessToken.userId);
expect(login.token).toBeDefined();
await VnUser.logout(ctx.req.accessToken.id);
});
it('should return the token', async() => { it('should return the token', async() => {
let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare'); let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare');
let accessToken = await AccessToken.findById(login.token); let accessToken = await AccessToken.findById(login.token);

View File

@ -124,17 +124,20 @@ module.exports = function(Self) {
return email.send(); return email.send();
}); });
Self.signInValidate = (user, userToken) => {
const [[key, value]] = Object.entries(Self.userUses(user));
if (userToken[key].toLowerCase().trim() !== value.toLowerCase().trim()) {
console.error('ERROR!!! - Signin with other user', userToken, user);
throw new UserError('Try again');
}
};
Self.validateLogin = async function(user, password) { Self.validateLogin = async function(user, password) {
const loginInfo = Object.assign({password}, Self.userUses(user)); const loginInfo = Object.assign({password}, Self.userUses(user));
const token = await Self.login(loginInfo, 'user'); const token = await Self.login(loginInfo, 'user');
const userToken = await token.user.get(); const userToken = await token.user.get();
Self.signInValidate(user, userToken);
if (userToken.username.toLowerCase() !== user.toLowerCase()) {
console.error('ERROR!!! - Signin with other user', userToken, user);
throw new UserError('Try again');
}
try { try {
await Self.app.models.Account.sync(userToken.name, password); await Self.app.models.Account.sync(userToken.name, password);

View File

@ -2,17 +2,18 @@
-- --
-- Table structure for table `signInLog` -- Table structure for table `signInLog`
-- Description: log to debug cross-login error
-- --
DROP TABLE IF EXISTS `account`.`signInLog`; DROP TABLE IF EXISTS `account`.`signInLog`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8 */;
CREATE TABLE `account`.`signInLog` ( CREATE TABLE `account`.`signInLog` (
`id` varchar(10) NOT NULL , id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`token` varchar(255) NOT NULL ,
`userFk` int(10) unsigned DEFAULT NULL, `userFk` int(10) unsigned DEFAULT NULL,
`creationDate` timestamp NULL DEFAULT current_timestamp(), `creationDate` timestamp NULL DEFAULT current_timestamp(),
`ip` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, `ip` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
PRIMARY KEY (`id`),
KEY `userFk` (`userFk`), KEY `userFk` (`userFk`),
CONSTRAINT `signInLog_ibfk_1` FOREIGN KEY (`userFk`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE CONSTRAINT `signInLog_ibfk_1` FOREIGN KEY (`userFk`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
); );

View File

@ -0,0 +1,2 @@
ALTER TABLE account.sambaConfig
ADD userDn varchar(255) NOT NULL COMMENT 'Base DN for users without domain DN part';

View File

@ -2887,7 +2887,9 @@ INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
INSERT INTO `vn`.`payDemDetail` (`id`, `detail`) INSERT INTO `vn`.`payDemDetail` (`id`, `detail`)
VALUES VALUES
(1, 1); (1, 1),
(2, 20),
(7, 1);
INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`, `payMethodFk`, `businessTypeFk`) INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`, `payMethodFk`, `businessTypeFk`)
VALUES VALUES

View File

@ -10,6 +10,7 @@ services:
build: build:
context: . context: .
dockerfile: front/Dockerfile dockerfile: front/Dockerfile
target: development
ports: ports:
- 5000:80 - 5000:80
links: links:
@ -26,6 +27,8 @@ services:
- NODE_ENV - NODE_ENV
depends_on: depends_on:
- db - db
local:
image:
networks: networks:
salix-stack-network: salix-stack-network:

View File

@ -1,3 +1,4 @@
const ForbiddenError = require('vn-loopback/util/forbiddenError');
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('sync', { Self.remoteMethod('sync', {
@ -32,9 +33,13 @@ module.exports = Self => {
const models = Self.app.models; const models = Self.app.models;
const user = await models.VnUser.findOne({ const user = await models.VnUser.findOne({
fields: ['id'], fields: ['id', 'password'],
where: {name: userName} where: {name: userName}
}, myOptions); }, myOptions);
if (user && password && !await user.hasPassword(password))
throw new ForbiddenError('Wrong password');
const isSync = !await models.UserSync.exists(userName, myOptions); const isSync = !await models.UserSync.exists(userName, myOptions);
if (!force && isSync && user) return; if (!force && isSync && user) return;
@ -42,4 +47,3 @@ module.exports = Self => {
await models.UserSync.destroyById(userName, myOptions); await models.UserSync.destroyById(userName, myOptions);
}; };
}; };

View File

@ -5,7 +5,7 @@ const crypto = require('crypto');
const nthash = require('smbhash').nthash; const nthash = require('smbhash').nthash;
module.exports = Self => { module.exports = Self => {
const shouldSync = process.env.NODE_ENV === 'production'; const shouldSync = process.env.NODE_ENV !== 'test';
Self.getSynchronizer = async function() { Self.getSynchronizer = async function() {
return await Self.findOne({ return await Self.findOne({
@ -140,6 +140,7 @@ module.exports = Self => {
try { try {
if (shouldSync) if (shouldSync)
await client.del(dn); await client.del(dn);
// eslint-disable-next-line no-console
console.log(` -> User '${userName}' removed from LDAP`); console.log(` -> User '${userName}' removed from LDAP`);
} catch (e) { } catch (e) {
if (e.name !== 'NoSuchObjectError') throw e; if (e.name !== 'NoSuchObjectError') throw e;

View File

@ -27,8 +27,7 @@ module.exports = Self => {
const [row] = await Self.rawSql( const [row] = await Self.rawSql(
`SELECT COUNT(*) AS nRows `SELECT COUNT(*) AS nRows
FROM mysql.user FROM mysql.user
WHERE User = ? WHERE User = ? AND Host = ?`,
AND Host = ?`,
[mysqlUser, this.userHost] [mysqlUser, this.userHost]
); );
let userExists = row.nRows > 0; let userExists = row.nRows > 0;
@ -38,8 +37,7 @@ module.exports = Self => {
const [row] = await Self.rawSql( const [row] = await Self.rawSql(
`SELECT Priv AS priv `SELECT Priv AS priv
FROM mysql.global_priv FROM mysql.global_priv
WHERE User = ? WHERE User = ? AND Host = ?`,
AND Host = ?`,
[mysqlUser, this.userHost] [mysqlUser, this.userHost]
); );
const priv = row && JSON.parse(row.priv); const priv = row && JSON.parse(row.priv);
@ -88,10 +86,18 @@ module.exports = Self => {
else else
throw err; throw err;
} }
await Self.rawSql('GRANT ? TO ?@?',
[role, mysqlUser, this.userHost]);
if (role) { const [row] = await Self.rawSql(
`SELECT COUNT(*) AS nRows
FROM mysql.user
WHERE User = ? AND Host = ''`,
[role]
);
const roleExists = row.nRows > 0;
if (roleExists) {
await Self.rawSql('GRANT ? TO ?@?',
[role, mysqlUser, this.userHost]);
await Self.rawSql('SET DEFAULT ROLE ? FOR ?@?', await Self.rawSql('SET DEFAULT ROLE ? FOR ?@?',
[role, mysqlUser, this.userHost]); [role, mysqlUser, this.userHost]);
} else { } else {

View File

@ -1,6 +1,6 @@
const ldap = require('../util/ldapjs-extra'); const ldap = require('../util/ldapjs-extra');
const ssh = require('node-ssh'); const execFile = require('child_process').execFile;
/** /**
* Summary of userAccountControl flags: * Summary of userAccountControl flags:
@ -11,6 +11,8 @@ const UserAccountControlFlags = {
}; };
module.exports = Self => { module.exports = Self => {
const shouldSync = process.env.NODE_ENV !== 'test';
Self.getSynchronizer = async function() { Self.getSynchronizer = async function() {
return await Self.findOne({ return await Self.findOne({
fields: [ fields: [
@ -19,6 +21,7 @@ module.exports = Self => {
'adController', 'adController',
'adUser', 'adUser',
'adPassword', 'adPassword',
'userDn',
'verifyCert' 'verifyCert'
] ]
}); });
@ -26,88 +29,123 @@ module.exports = Self => {
Object.assign(Self.prototype, { Object.assign(Self.prototype, {
async init() { async init() {
let sshClient = new ssh.NodeSSH(); const baseDn = this.adDomain
await sshClient.connect({ .split('.')
host: this.adController, .map(part => `dc=${part}`)
username: this.adUser, .join(',');
password: this.adPassword const bindDn = `cn=${this.adUser},cn=Users,${baseDn}`;
});
let adUser = `cn=${this.adUser},${this.usersDn()}`; const adClient = ldap.createClient({
let adClient = ldap.createClient({
url: `ldaps://${this.adController}:636`, url: `ldaps://${this.adController}:636`,
tlsOptions: {rejectUnauthorized: this.verifyCert} tlsOptions: {rejectUnauthorized: this.verifyCert}
}); });
await adClient.bind(adUser, this.adPassword); await adClient.bind(bindDn, this.adPassword);
Object.assign(this, { Object.assign(this, {
sshClient, adClient,
adClient fullUsersDn: `${this.userDn},${baseDn}`,
bindDn
}); });
}, },
async deinit() { async deinit() {
await this.sshClient.dispose();
await this.adClient.unbind(); await this.adClient.unbind();
}, },
usersDn() { async sambaTool(command, args = []) {
let dnBase = this.adDomain let authArgs = [
.split('.') '--URL', `ldaps://${this.adController}`,
.map(part => `dc=${part}`) '--simple-bind-dn', this.bindDn,
.join(','); '--password', this.adPassword
return `cn=Users,${dnBase}`; ];
if (!this.verifyCert)
authArgs.push('--option', 'tls verify peer = no_check');
const allArgs = [command].concat(
args, authArgs
);
if (!shouldSync) return;
return await new Promise((resolve, reject) => {
execFile('samba-tool', allArgs, (err, stdout, stderr) => {
if (err)
reject(err);
else
resolve({stdout, stderr});
});
});
}, },
async syncUser(userName, info, password) { async getAdUser(userName) {
let {sshClient} = this; const sambaUser = await this.adClient.searchOne(this.fullUsersDn, {
let sambaUser = await this.adClient.searchOne(this.usersDn(), {
scope: 'sub', scope: 'sub',
attributes: ['userAccountControl'], attributes: [
'dn',
'userAccountControl',
'uidNumber',
'accountExpires',
'mail'
],
filter: `(&(objectClass=user)(sAMAccountName=${userName}))` filter: `(&(objectClass=user)(sAMAccountName=${userName}))`
}); });
let isEnabled = sambaUser if (sambaUser) {
&& !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE); for (const intProp of ['uidNumber', 'userAccountControl']) {
if (sambaUser[intProp] != null)
if (process.env.NODE_ENV === 'test') sambaUser[intProp] = parseInt(sambaUser[intProp]);
return; }
}
return sambaUser;
},
async syncUser(userName, info, password) {
let sambaUser = await this.getAdUser(userName);
let entry;
if (info.hasAccount) { if (info.hasAccount) {
if (!sambaUser) { if (!sambaUser) {
await sshClient.exec('samba-tool user create', [ await this.sambaTool('user', [
userName, 'create', userName,
'--uid-number', `${info.uidNumber}`, '--userou', this.userDn,
'--mail-address', info.corporateMail,
'--random-password' '--random-password'
]); ]);
await sshClient.exec('samba-tool user setexpiry', [ sambaUser = await this.getAdUser(userName);
userName,
'--noexpiry'
]);
await sshClient.exec('mkhomedir_helper', [
userName,
'0027'
]);
}
if (!isEnabled) {
await sshClient.exec('samba-tool user enable', [
userName
]);
} }
if (password) { if (password) {
await sshClient.exec('samba-tool user setpassword', [ await this.sambaTool('user', [
userName, 'setpassword', userName,
'--newpassword', password '--newpassword', password
]); ]);
} }
} else if (isEnabled) {
await sshClient.exec('samba-tool user disable', [ entry = {
userName userAccountControl: sambaUser.userAccountControl
]); & ~UserAccountControlFlags.ACCOUNTDISABLE,
uidNumber: info.uidNumber,
accountExpires: 0,
mail: info.corporateMail
};
} else if (sambaUser) {
entry = {
userAccountControl: sambaUser.userAccountControl
| UserAccountControlFlags.ACCOUNTDISABLE
};
// eslint-disable-next-line no-console
console.log(` -> User '${userName}' disabled on Samba`); console.log(` -> User '${userName}' disabled on Samba`);
} }
if (sambaUser && entry) {
const changes = [];
for (const prop in entry) {
if (sambaUser[prop] == entry[prop]) continue;
changes.push(new ldap.Change({
operation: 'replace',
modification: {
[prop]: entry[prop]
}
}));
}
if (changes.length && shouldSync)
await this.adClient.modify(sambaUser.dn, changes);
}
}, },
/** /**
@ -117,14 +155,15 @@ module.exports = Self => {
*/ */
async getUsers(usersToSync) { async getUsers(usersToSync) {
const LDAP_MATCHING_RULE_BIT_AND = '1.2.840.113556.1.4.803'; const LDAP_MATCHING_RULE_BIT_AND = '1.2.840.113556.1.4.803';
let filter = `!(userAccountControl:${LDAP_MATCHING_RULE_BIT_AND}:=${UserAccountControlFlags.ACCOUNTDISABLE})`; const filter = `!(userAccountControl:${LDAP_MATCHING_RULE_BIT_AND}`
+ `:=${UserAccountControlFlags.ACCOUNTDISABLE})`;
let opts = { const opts = {
scope: 'sub', scope: 'sub',
attributes: ['sAMAccountName'], attributes: ['sAMAccountName'],
filter: `(&(objectClass=user)(${filter}))` filter: `(&(objectClass=user)(${filter}))`
}; };
await this.adClient.searchForeach(this.usersDn(), opts, await this.adClient.searchForeach(this.fullUsersDn, opts,
o => usersToSync.add(o.sAMAccountName)); o => usersToSync.add(o.sAMAccountName));
} }
}); });

View File

@ -28,6 +28,10 @@
"adPassword": { "adPassword": {
"type": "string" "type": "string"
}, },
"userDn": {
"type": "string",
"required": true
},
"verifyCert": { "verifyCert": {
"type": "boolean" "type": "boolean"
} }

View File

@ -8,13 +8,20 @@
}, },
"properties": { "properties": {
"id": { "id": {
"type": "number",
"id": true, "id": true,
"type": "string" "description": "Identifier"
},
"token": {
"required": true,
"type": "string",
"description": "Token's user"
}, },
"creationDate": { "creationDate": {
"type": "date" "type": "date"
}, },
"userFk": { "userFk": {
"required": true,
"type": "number" "type": "number"
}, },
"ip": { "ip": {

View File

@ -12,40 +12,40 @@
<vn-card class="vn-pa-lg" vn-focus> <vn-card class="vn-pa-lg" vn-focus>
<vn-vertical> <vn-vertical>
<vn-textfield <vn-textfield
label="Homedir base" label="Homedir base"
ng-model="$ctrl.config.homedir" ng-model="$ctrl.config.homedir"
rule="AccountConfig"> rule="AccountConfig">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Shell" label="Shell"
ng-model="$ctrl.config.shell" ng-model="$ctrl.config.shell"
rule="AccountConfig"> rule="AccountConfig">
</vn-textfield> </vn-textfield>
<vn-input-number <vn-input-number
label="User and role base id" label="User and role base id"
ng-model="$ctrl.config.idBase" ng-model="$ctrl.config.idBase"
rule="AccountConfig"> rule="AccountConfig">
</vn-input-number> </vn-input-number>
<vn-horizontal> <vn-horizontal>
<vn-input-number <vn-input-number
label="Min" label="Min"
ng-model="$ctrl.config.min" ng-model="$ctrl.config.min"
rule="AccountConfig"> rule="AccountConfig">
</vn-input-number> </vn-input-number>
<vn-input-number <vn-input-number
label="Max" label="Max"
ng-model="$ctrl.config.max" ng-model="$ctrl.config.max"
rule="AccountConfig"> rule="AccountConfig">
</vn-input-number> </vn-input-number>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-input-number <vn-input-number
label="Warn" label="Warn"
ng-model="$ctrl.config.warn" ng-model="$ctrl.config.warn"
rule="AccountConfig"> rule="AccountConfig">
</vn-input-number> </vn-input-number>
<vn-input-number <vn-input-number
label="Inact" label="Inact"
ng-model="$ctrl.config.inact" ng-model="$ctrl.config.inact"
rule="AccountConfig"> rule="AccountConfig">
</vn-input-number> </vn-input-number>
@ -61,10 +61,6 @@
label="Synchronize all" label="Synchronize all"
ng-click="$ctrl.onSynchronizeAll()"> ng-click="$ctrl.onSynchronizeAll()">
</vn-button> </vn-button>
<vn-button
label="Synchronize user"
ng-click="syncUser.show()">
</vn-button>
<vn-button <vn-button
label="Synchronize roles" label="Synchronize roles"
ng-click="$ctrl.onSynchronizeRoles()"> ng-click="$ctrl.onSynchronizeRoles()">
@ -77,25 +73,3 @@
</vn-button> </vn-button>
</vn-button-bar> </vn-button-bar>
</form> </form>
<vn-dialog
vn-id="syncUser"
on-accept="$ctrl.onUserSync()"
on-close="$ctrl.onSyncClose()">
<tpl-body>
<vn-textfield
label="Username"
ng-model="$ctrl.syncUser"
vn-focus>
</vn-textfield>
<vn-textfield
label="Password"
ng-model="$ctrl.syncPassword"
type="password"
info="If password is not specified, just user attributes are synchronized">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Synchronize</button>
</tpl-buttons>
</vn-dialog>

View File

@ -1,6 +1,5 @@
import ngModule from '../module'; import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
import UserError from 'core/lib/user-error';
export default class Controller extends Section { export default class Controller extends Section {
onSynchronizeAll() { onSynchronizeAll() {
@ -8,27 +7,10 @@ export default class Controller extends Section {
this.$http.patch(`Accounts/syncAll`); this.$http.patch(`Accounts/syncAll`);
} }
onUserSync() {
if (!this.syncUser)
throw new UserError('Please enter the username');
let params = {
password: this.syncPassword,
force: true
};
return this.$http.patch(`Accounts/${this.syncUser}/sync`, params)
.then(() => this.vnApp.showSuccess(this.$t('User synchronized!')));
}
onSynchronizeRoles() { onSynchronizeRoles() {
this.$http.patch(`RoleInherits/sync`) this.$http.patch(`RoleInherits/sync`)
.then(() => this.vnApp.showSuccess(this.$t('Roles synchronized!'))); .then(() => this.vnApp.showSuccess(this.$t('Roles synchronized!')));
} }
onSyncClose() {
this.syncUser = '';
this.syncPassword = '';
}
} }
ngModule.component('vnAccountAccounts', { ngModule.component('vnAccountAccounts', {

View File

@ -3,7 +3,6 @@ Homedir base: Directorio base para carpetas de usuario
Shell: Intérprete de línea de comandos Shell: Intérprete de línea de comandos
User and role base id: Id base usuarios y roles User and role base id: Id base usuarios y roles
Synchronize all: Sincronizar todo Synchronize all: Sincronizar todo
Synchronize user: Sincronizar usuario
Synchronize roles: Sincronizar roles Synchronize roles: Sincronizar roles
If password is not specified, just user attributes are synchronized: >- If password is not specified, just user attributes are synchronized: >-
Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario
@ -12,5 +11,4 @@ Users synchronized!: ¡Usuarios sincronizados!
Username: Nombre de usuario Username: Nombre de usuario
Synchronize: Sincronizar Synchronize: Sincronizar
Please enter the username: Por favor introduce el nombre de usuario Please enter the username: Por favor introduce el nombre de usuario
User synchronized!: ¡Usuario sincronizado!
Roles synchronized!: ¡Roles sincronizados! Roles synchronized!: ¡Roles sincronizados!

View File

@ -67,6 +67,15 @@
translate> translate>
Deactivate user Deactivate user
</vn-item> </vn-item>
<vn-item
ng-if="$ctrl.user.active"
ng-click="syncUser.show()"
name="synchronizeUser"
vn-acl="it"
vn-acl-action="remove"
translate>
Synchronize
</vn-item>
</slot-menu> </slot-menu>
<slot-body> <slot-body>
<div class="attributes"> <div class="attributes">
@ -153,6 +162,32 @@
<button response="accept" translate>Change password</button> <button response="accept" translate>Change password</button>
</tpl-buttons> </tpl-buttons>
</vn-dialog> </vn-dialog>
<vn-dialog
vn-id="syncUser"
on-accept="$ctrl.onSync()"
on-close="$ctrl.onSyncClose()">
<tpl-title ng-translate>
Do you want to synchronize user?
</tpl-title>
<tpl-body>
<vn-check
label="Synchronize password"
ng-model="$ctrl.shouldSyncPassword"
info="If password is not specified, just user attributes are synchronized"
vn-focus>
</vn-check>
<vn-textfield
label="Password"
ng-model="$ctrl.syncPassword"
type="password"
ng-if="$ctrl.shouldSyncPassword">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Synchronize</button>
</tpl-buttons>
</vn-dialog>
<vn-popup vn-id="summary"> <vn-popup vn-id="summary">
<vn-user-summary user="$ctrl.user"></vn-user-summary> <vn-user-summary user="$ctrl.user"></vn-user-summary>
</vn-popup> </vn-popup>

View File

@ -120,6 +120,20 @@ class Controller extends Descriptor {
this.vnApp.showSuccess(this.$t(message)); this.vnApp.showSuccess(this.$t(message));
}); });
} }
onSync() {
const params = {force: true};
if (this.shouldSyncPassword)
params.password = this.syncPassword;
return this.$http.patch(`Accounts/${this.user.name}/sync`, params)
.then(() => this.vnApp.showSuccess(this.$t('User synchronized!')));
}
onSyncClose() {
this.shouldSyncPassword = false;
this.syncPassword = undefined;
}
} }
ngModule.component('vnUserDescriptor', { ngModule.component('vnUserDescriptor', {

View File

@ -22,6 +22,10 @@ Old password: Contraseña antigua
New password: Nueva contraseña New password: Nueva contraseña
Repeat password: Repetir contraseña Repeat password: Repetir contraseña
Password changed succesfully!: ¡Contraseña modificada correctamente! Password changed succesfully!: ¡Contraseña modificada correctamente!
Synchronize: Sincronizar
Do you want to synchronize user?: ¿Quieres sincronizar el usuario?
Synchronize password: Sincronizar contraseña
User synchronized!: ¡Usuario sincronizado!
Role changed succesfully!: ¡Rol modificado correctamente! Role changed succesfully!: ¡Rol modificado correctamente!
Password requirements: > Password requirements: >
La contraseña debe tener al menos {{ length }} caracteres de longitud, La contraseña debe tener al menos {{ length }} caracteres de longitud,

View File

@ -12,7 +12,7 @@
<vn-card class="vn-pa-lg" vn-focus> <vn-card class="vn-pa-lg" vn-focus>
<vn-vertical> <vn-vertical>
<vn-check <vn-check
label="Enable synchronization" label="Enable synchronization"
ng-model="watcher.hasData"> ng-model="watcher.hasData">
</vn-check> </vn-check>
</vn-vertical> </vn-vertical>
@ -20,28 +20,33 @@
ng-if="watcher.hasData" ng-if="watcher.hasData"
class="vn-mt-md"> class="vn-mt-md">
<vn-textfield <vn-textfield
label="AD domain" label="AD domain"
ng-model="$ctrl.config.adDomain" ng-model="$ctrl.config.adDomain"
rule="SambaConfig"> rule="SambaConfig">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Domain controller" label="Domain controller"
ng-model="$ctrl.config.adController" ng-model="$ctrl.config.adController"
rule="SambaConfig"> rule="SambaConfig">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="AD user" label="AD user"
ng-model="$ctrl.config.adUser" ng-model="$ctrl.config.adUser"
rule="SambaConfig"> rule="SambaConfig">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="AD password" label="AD password"
ng-model="$ctrl.config.adPassword" ng-model="$ctrl.config.adPassword"
type="password" type="password"
rule="SambaConfig"> rule="SambaConfig">
</vn-textfield> </vn-textfield>
<vn-textfield
label="User DN (without domain part)"
ng-model="$ctrl.config.userDn"
rule="SambaConfig">
</vn-textfield>
<vn-check <vn-check
label="Verify certificate" label="Verify certificate"
ng-model="$ctrl.config.verifyCert"> ng-model="$ctrl.config.verifyCert">
</vn-check> </vn-check>
</vn-vertical> </vn-vertical>
@ -63,4 +68,4 @@
ng-click="watcher.loadOriginalData()"> ng-click="watcher.loadOriginalData()">
</vn-button> </vn-button>
</vn-button-bar> </vn-button-bar>
</form> </form>

View File

@ -3,6 +3,7 @@ Domain controller: Controlador de dominio
AD domain: Dominio AD AD domain: Dominio AD
AD user: Usuario AD AD user: Usuario AD
AD password: Contraseña AD AD password: Contraseña AD
User DN (without domain part): DN usuarios (sin la parte del dominio)
Verify certificate: Verificar certificado Verify certificate: Verificar certificado
Test connection: Probar conexión Test connection: Probar conexión
Samba connection established!: ¡Conexión con Samba establecida! Samba connection established!: ¡Conexión con Samba establecida!

View File

@ -0,0 +1,37 @@
module.exports = Self => {
Self.remoteMethodCtx('new', {
description: 'Creates a new invoiceIn due day',
accessType: 'WRITE',
accepts: {
arg: 'id',
type: 'number',
required: true,
description: 'The invoiceIn id',
},
http: {
path: '/new',
verb: 'POST'
}
});
Self.new = async(ctx, id, options) => {
let tx;
const myOptions = {userId: ctx.req.accessToken.userId};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
await Self.rawSql(`CALL vn.invoiceInDueDay_calculate(?)`, [id], myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,45 @@
const LoopBackContext = require('loopback-context');
const models = require('vn-loopback/server/server').models;
describe('invoiceInDueDay new()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should correctly create a new due day', async() => {
const userId = 9;
const invoiceInFk = 6;
const ctx = {
req: {
accessToken: {userId: userId},
}
};
const tx = await models.InvoiceIn.beginTransaction({});
const options = {transaction: tx};
try {
await models.InvoiceInDueDay.destroyAll({
invoiceInFk
}, options);
await models.InvoiceInDueDay.new(ctx, invoiceInFk, options);
const result = await models.InvoiceInDueDay.find({where: {invoiceInFk}}, options);
expect(result).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,3 @@
module.exports = Self => {
require('../methods/invoice-in-due-day/new')(Self);
};