Merge branch 'v8/8.1' into v8/dev
# Conflicts: # src/SolutionInfo.cs
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NPoco;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Migrations.Expressions.Common;
|
||||
using Umbraco.Core.Persistence.SqlSyntax;
|
||||
|
||||
@@ -27,31 +29,57 @@ namespace Umbraco.Core.Migrations.Expressions.Delete.KeysAndIndexes
|
||||
{
|
||||
_context.BuildingExpression = false;
|
||||
|
||||
//get a list of all constraints - this will include all PK, FK and unique constraints
|
||||
var tableConstraints = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList();
|
||||
|
||||
//get a list of defined indexes - this will include all indexes, unique indexes and unique constraint indexes
|
||||
var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList();
|
||||
|
||||
var uniqueConstraintNames = tableConstraints.Where(x => !x.Item2.InvariantStartsWith("PK_") && !x.Item2.InvariantStartsWith("FK_")).Select(x => x.Item2);
|
||||
var indexNames = indexes.Select(x => x.IndexName).ToList();
|
||||
|
||||
// drop keys
|
||||
if (DeleteLocal || DeleteForeign)
|
||||
{
|
||||
// table, constraint
|
||||
var tableKeys = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList();
|
||||
|
||||
if (DeleteForeign)
|
||||
{
|
||||
foreach (var key in tableKeys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("FK_")))
|
||||
//In some cases not all FK's are prefixed with "FK" :/ mostly with old upgraded databases so we need to check if it's either:
|
||||
// * starts with FK OR
|
||||
// * doesn't start with PK_ and doesn't exist in the list of indexes
|
||||
|
||||
foreach (var key in tableConstraints.Where(x => x.Item1 == TableName
|
||||
&& (x.Item2.InvariantStartsWith("FK_") || (!x.Item2.InvariantStartsWith("PK_") && !indexNames.InvariantContains(x.Item2)))))
|
||||
{
|
||||
Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do();
|
||||
}
|
||||
|
||||
}
|
||||
if (DeleteLocal)
|
||||
{
|
||||
foreach (var key in tableKeys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("PK_")))
|
||||
foreach (var key in tableConstraints.Where(x => x.Item1 == TableName && x.Item2.InvariantStartsWith("PK_")))
|
||||
Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do();
|
||||
|
||||
// note: we do *not* delete the DEFAULT constraints
|
||||
// note: we do *not* delete the DEFAULT constraints and if we wanted to we'd have to deal with that in interesting ways
|
||||
// since SQL server has a specific way to handle that, see SqlServerSyntaxProvider.GetDefaultConstraintsPerColumn
|
||||
}
|
||||
}
|
||||
|
||||
// drop indexes
|
||||
if (DeleteLocal)
|
||||
{
|
||||
var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList();
|
||||
{
|
||||
foreach (var index in indexes.Where(x => x.TableName == TableName))
|
||||
Delete.Index(index.IndexName).OnTable(index.TableName).Do();
|
||||
{
|
||||
//if this is a unique constraint we need to drop the constraint, else drop the index
|
||||
//to figure this out, the index must be tagged as unique and it must exist in the tableConstraints
|
||||
|
||||
if (index.IsUnique && uniqueConstraintNames.InvariantContains(index.IndexName))
|
||||
Delete.UniqueConstraint(index.IndexName).FromTable(index.TableName).Do();
|
||||
else
|
||||
Delete.Index(index.IndexName).OnTable(index.TableName).Do();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,167 +1,172 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
function ChangePasswordController($scope) {
|
||||
function ChangePasswordController($scope) {
|
||||
|
||||
function resetModel(isNew) {
|
||||
//the model config will contain an object, if it does not we'll create defaults
|
||||
//NOTE: We will not support doing the password regex on the client side because the regex on the server side
|
||||
//based on the membership provider cannot always be ported to js from .net directly.
|
||||
/*
|
||||
{
|
||||
hasPassword: true/false,
|
||||
requiresQuestionAnswer: true/false,
|
||||
enableReset: true/false,
|
||||
enablePasswordRetrieval: true/false,
|
||||
minPasswordLength: 10
|
||||
}
|
||||
*/
|
||||
var vm = this;
|
||||
|
||||
$scope.showReset = false;
|
||||
vm.$onInit = onInit;
|
||||
vm.$onDestroy = onDestroy;
|
||||
vm.doChange = doChange;
|
||||
vm.cancelChange = cancelChange;
|
||||
vm.showOldPass = showOldPass;
|
||||
vm.showCancelBtn = showCancelBtn;
|
||||
|
||||
//set defaults if they are not available
|
||||
if ($scope.config.disableToggle === undefined) {
|
||||
$scope.config.disableToggle = false;
|
||||
}
|
||||
if ($scope.config.hasPassword === undefined) {
|
||||
$scope.config.hasPassword = false;
|
||||
}
|
||||
if ($scope.config.enablePasswordRetrieval === undefined) {
|
||||
$scope.config.enablePasswordRetrieval = true;
|
||||
}
|
||||
if ($scope.config.requiresQuestionAnswer === undefined) {
|
||||
$scope.config.requiresQuestionAnswer = false;
|
||||
}
|
||||
//don't enable reset if it is new - that doesn't make sense
|
||||
if (isNew === "true") {
|
||||
$scope.config.enableReset = false;
|
||||
}
|
||||
else if ($scope.config.enableReset === undefined) {
|
||||
$scope.config.enableReset = true;
|
||||
}
|
||||
|
||||
if ($scope.config.minPasswordLength === undefined) {
|
||||
$scope.config.minPasswordLength = 0;
|
||||
}
|
||||
|
||||
//set the model defaults
|
||||
if (!angular.isObject($scope.passwordValues)) {
|
||||
//if it's not an object then just create a new one
|
||||
$scope.passwordValues = {
|
||||
newPassword: null,
|
||||
oldPassword: null,
|
||||
reset: null,
|
||||
answer: null
|
||||
};
|
||||
}
|
||||
else {
|
||||
//just reset the values
|
||||
var unsubscribe = [];
|
||||
|
||||
function resetModel(isNew) {
|
||||
//the model config will contain an object, if it does not we'll create defaults
|
||||
//NOTE: We will not support doing the password regex on the client side because the regex on the server side
|
||||
//based on the membership provider cannot always be ported to js from .net directly.
|
||||
/*
|
||||
{
|
||||
hasPassword: true/false,
|
||||
requiresQuestionAnswer: true/false,
|
||||
enableReset: true/false,
|
||||
enablePasswordRetrieval: true/false,
|
||||
minPasswordLength: 10
|
||||
}
|
||||
*/
|
||||
|
||||
vm.showReset = false;
|
||||
|
||||
//set defaults if they are not available
|
||||
if (vm.config.disableToggle === undefined) {
|
||||
vm.config.disableToggle = false;
|
||||
}
|
||||
if (vm.config.hasPassword === undefined) {
|
||||
vm.config.hasPassword = false;
|
||||
}
|
||||
if (vm.config.enablePasswordRetrieval === undefined) {
|
||||
vm.config.enablePasswordRetrieval = true;
|
||||
}
|
||||
if (vm.config.requiresQuestionAnswer === undefined) {
|
||||
vm.config.requiresQuestionAnswer = false;
|
||||
}
|
||||
//don't enable reset if it is new - that doesn't make sense
|
||||
if (isNew === "true") {
|
||||
vm.config.enableReset = false;
|
||||
}
|
||||
else if (vm.config.enableReset === undefined) {
|
||||
vm.config.enableReset = true;
|
||||
}
|
||||
|
||||
if (vm.config.minPasswordLength === undefined) {
|
||||
vm.config.minPasswordLength = 0;
|
||||
}
|
||||
|
||||
//set the model defaults
|
||||
if (!angular.isObject(vm.passwordValues)) {
|
||||
//if it's not an object then just create a new one
|
||||
vm.passwordValues = {
|
||||
newPassword: null,
|
||||
oldPassword: null,
|
||||
reset: null,
|
||||
answer: null
|
||||
};
|
||||
}
|
||||
else {
|
||||
//just reset the values
|
||||
|
||||
if (!isNew) {
|
||||
//if it is new, then leave the generated pass displayed
|
||||
vm.passwordValues.newPassword = null;
|
||||
vm.passwordValues.oldPassword = null;
|
||||
}
|
||||
vm.passwordValues.reset = null;
|
||||
vm.passwordValues.answer = null;
|
||||
}
|
||||
|
||||
//the value to compare to match passwords
|
||||
if (!isNew) {
|
||||
vm.passwordValues.confirm = "";
|
||||
}
|
||||
else if (vm.passwordValues.newPassword && vm.passwordValues.newPassword.length > 0) {
|
||||
//if it is new and a new password has been set, then set the confirm password too
|
||||
vm.passwordValues.confirm = vm.passwordValues.newPassword;
|
||||
}
|
||||
|
||||
if (!isNew) {
|
||||
//if it is new, then leave the generated pass displayed
|
||||
$scope.passwordValues.newPassword = null;
|
||||
$scope.passwordValues.oldPassword = null;
|
||||
}
|
||||
$scope.passwordValues.reset = null;
|
||||
$scope.passwordValues.answer = null;
|
||||
}
|
||||
|
||||
//the value to compare to match passwords
|
||||
if (!isNew) {
|
||||
$scope.passwordValues.confirm = "";
|
||||
}
|
||||
else if ($scope.passwordValues.newPassword && $scope.passwordValues.newPassword.length > 0) {
|
||||
//if it is new and a new password has been set, then set the confirm password too
|
||||
$scope.passwordValues.confirm = $scope.passwordValues.newPassword;
|
||||
}
|
||||
//when the scope is destroyed we need to unsubscribe
|
||||
function onDestroy() {
|
||||
for (var u in unsubscribe) {
|
||||
unsubscribe[u]();
|
||||
}
|
||||
}
|
||||
|
||||
function onInit() {
|
||||
//listen for the saved event, when that occurs we'll
|
||||
//change to changing = false;
|
||||
unsubscribe.push($scope.$on("formSubmitted", function () {
|
||||
if (vm.config.disableToggle === false) {
|
||||
vm.changing = false;
|
||||
}
|
||||
}));
|
||||
|
||||
unsubscribe.push($scope.$on("formSubmitting", function () {
|
||||
//if there was a previously generated password displaying, clear it
|
||||
if (vm.changing && vm.passwordValues) {
|
||||
vm.passwordValues.generatedPassword = null;
|
||||
}
|
||||
else if (!vm.changing) {
|
||||
//we are not changing, so the model needs to be null
|
||||
vm.passwordValues = null;
|
||||
}
|
||||
}));
|
||||
|
||||
resetModel(vm.isNew);
|
||||
|
||||
//if there is no password saved for this entity , it must be new so we do not allow toggling of the change password, it is always there
|
||||
//with validators turned on.
|
||||
vm.changing = vm.config.disableToggle === true || !vm.config.hasPassword;
|
||||
|
||||
//we're not currently changing so set the model to null
|
||||
if (!vm.changing) {
|
||||
vm.passwordValues = null;
|
||||
}
|
||||
}
|
||||
|
||||
function doChange() {
|
||||
resetModel();
|
||||
vm.changing = true;
|
||||
//if there was a previously generated password displaying, clear it
|
||||
vm.passwordValues.generatedPassword = null;
|
||||
vm.passwordValues.confirm = null;
|
||||
};
|
||||
|
||||
function cancelChange() {
|
||||
vm.changing = false;
|
||||
//set model to null
|
||||
vm.passwordValues = null;
|
||||
};
|
||||
|
||||
function showOldPass() {
|
||||
return vm.config.hasPassword &&
|
||||
!vm.config.allowManuallyChangingPassword &&
|
||||
!vm.config.enablePasswordRetrieval && !vm.showReset;
|
||||
};
|
||||
|
||||
// TODO: I don't think we need this or the cancel button, this can be up to the editor rendering this component
|
||||
function showCancelBtn() {
|
||||
return vm.config.disableToggle !== true && vm.config.hasPassword;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
resetModel($scope.isNew);
|
||||
|
||||
//if there is no password saved for this entity , it must be new so we do not allow toggling of the change password, it is always there
|
||||
//with validators turned on.
|
||||
$scope.changing = $scope.config.disableToggle === true || !$scope.config.hasPassword;
|
||||
|
||||
//we're not currently changing so set the model to null
|
||||
if (!$scope.changing) {
|
||||
$scope.passwordValues = null;
|
||||
}
|
||||
|
||||
$scope.doChange = function () {
|
||||
resetModel();
|
||||
$scope.changing = true;
|
||||
//if there was a previously generated password displaying, clear it
|
||||
$scope.passwordValues.generatedPassword = null;
|
||||
$scope.passwordValues.confirm = null;
|
||||
var component = {
|
||||
templateUrl: 'views/components/users/change-password.html',
|
||||
controller: ChangePasswordController,
|
||||
controllerAs: 'vm',
|
||||
bindings: {
|
||||
isNew: "<",
|
||||
passwordValues: "=", //TODO: Do we need bi-directional vals?
|
||||
config: "=" //TODO: Do we need bi-directional vals?
|
||||
//TODO: Do we need callbacks?
|
||||
}
|
||||
};
|
||||
|
||||
$scope.cancelChange = function () {
|
||||
$scope.changing = false;
|
||||
//set model to null
|
||||
$scope.passwordValues = null;
|
||||
};
|
||||
|
||||
var unsubscribe = [];
|
||||
|
||||
//listen for the saved event, when that occurs we'll
|
||||
//change to changing = false;
|
||||
unsubscribe.push($scope.$on("formSubmitted", function () {
|
||||
if ($scope.config.disableToggle === false) {
|
||||
$scope.changing = false;
|
||||
}
|
||||
}));
|
||||
unsubscribe.push($scope.$on("formSubmitting", function () {
|
||||
//if there was a previously generated password displaying, clear it
|
||||
if ($scope.changing && $scope.passwordValues) {
|
||||
$scope.passwordValues.generatedPassword = null;
|
||||
}
|
||||
else if (!$scope.changing) {
|
||||
//we are not changing, so the model needs to be null
|
||||
$scope.passwordValues = null;
|
||||
}
|
||||
}));
|
||||
|
||||
//when the scope is destroyed we need to unsubscribe
|
||||
$scope.$on('$destroy', function () {
|
||||
for (var u in unsubscribe) {
|
||||
unsubscribe[u]();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.showOldPass = function () {
|
||||
return $scope.config.hasPassword &&
|
||||
!$scope.config.allowManuallyChangingPassword &&
|
||||
!$scope.config.enablePasswordRetrieval && !$scope.showReset;
|
||||
};
|
||||
|
||||
// TODO: I don't think we need this or the cancel button, this can be up to the editor rendering this directive
|
||||
$scope.showCancelBtn = function () {
|
||||
return $scope.config.disableToggle !== true && $scope.config.hasPassword;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function ChangePasswordDirective() {
|
||||
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
templateUrl: 'views/components/users/change-password.html',
|
||||
controller: 'Umbraco.Editors.Users.ChangePasswordDirectiveController',
|
||||
scope: {
|
||||
isNew: "=?",
|
||||
passwordValues: "=",
|
||||
config: "="
|
||||
}
|
||||
};
|
||||
|
||||
return directive;
|
||||
|
||||
}
|
||||
|
||||
angular.module('umbraco.directives').controller('Umbraco.Editors.Users.ChangePasswordDirectiveController', ChangePasswordController);
|
||||
angular.module('umbraco.directives').directive('changePassword', ChangePasswordDirective);
|
||||
angular.module('umbraco.directives').component('changePassword', component);
|
||||
|
||||
|
||||
})();
|
||||
|
||||
@@ -491,7 +491,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
|
||||
var savedVariants = [];
|
||||
if (origContent.variants) {
|
||||
isContent = true;
|
||||
//it's contnet so assign the variants as they exist
|
||||
//it's content so assign the variants as they exist
|
||||
origVariants = origContent.variants;
|
||||
savedVariants = savedContent.variants;
|
||||
}
|
||||
@@ -517,7 +517,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
|
||||
|
||||
//special case for content, don't sync this variant if it wasn't tagged
|
||||
//for saving in the first place
|
||||
if (!origVariant.save) {
|
||||
if (isContent && !origVariant.save) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +1,69 @@
|
||||
<div>
|
||||
<div class="alert alert-success text-center" ng-hide="!passwordValues.generatedPassword">
|
||||
<div class="alert alert-success text-center" ng-hide="!vm.passwordValues.generatedPassword">
|
||||
<small>Password has been reset to:</small>
|
||||
<br />
|
||||
<strong>{{passwordValues.generatedPassword}}</strong>
|
||||
<strong>{{vm.passwordValues.generatedPassword}}</strong>
|
||||
</div>
|
||||
<div ng-switch="changing">
|
||||
<div ng-switch="vm.changing">
|
||||
<div ng-switch-when="false">
|
||||
<a href="" ng-click="doChange()" class="btn btn-small">
|
||||
<a href="" ng-click="vm.doChange()" class="btn btn-small">
|
||||
<localize key="general_changePassword">Change password</localize>
|
||||
</a>
|
||||
</div>
|
||||
<div ng-switch-when="true">
|
||||
<ng-form name="passwordForm">
|
||||
<umb-control-group alias="resetPassword" label="@user_resetPassword" ng-show="config.enableReset">
|
||||
<input type="checkbox" ng-model="passwordValues.reset"
|
||||
|
||||
<ng-form name="vm.passwordForm">
|
||||
<umb-control-group alias="resetPassword" label="@user_resetPassword" ng-show="vm.config.enableReset">
|
||||
<input type="checkbox" ng-model="vm.passwordValues.reset"
|
||||
id="Checkbox1"
|
||||
name="resetPassword"
|
||||
val-server-field="resetPassword"
|
||||
no-dirty-check
|
||||
ng-change="showReset = !showReset" />
|
||||
<span ng-messages="passwordForm.resetPassword.$error" show-validation-on-submit >
|
||||
<span class="help-inline" ng-message="valServerField">{{passwordForm.resetPassword.errorMsg}}</span>
|
||||
ng-change="vm.showReset = !vm.showReset" />
|
||||
<span ng-messages="vm.passwordForm.resetPassword.$error" show-validation-on-submit>
|
||||
<span class="help-inline" ng-message="valServerField">{{vm.passwordForm.resetPassword.errorMsg}}</span>
|
||||
</span>
|
||||
|
||||
</umb-control-group>
|
||||
|
||||
</umb-control-group>
|
||||
|
||||
<!-- we need to show the old pass field when the provider cannot retrieve the password -->
|
||||
<umb-control-group alias="oldPassword" label="@user_oldPassword" ng-if="showOldPass()" required="true">
|
||||
<input type="password" name="oldPassword" ng-model="passwordValues.oldPassword"
|
||||
<umb-control-group alias="oldPassword" label="@user_oldPassword" ng-if="vm.showOldPass()" required="true">
|
||||
<input type="password" name="oldPassword" ng-model="vm.passwordValues.oldPassword"
|
||||
class="input-block-level umb-textstring textstring"
|
||||
required
|
||||
val-server-field="oldPassword"
|
||||
no-dirty-check />
|
||||
<span ng-messages="passwordForm.oldPassword.$error" show-validation-on-submit >
|
||||
<span ng-messages="vm.passwordForm.oldPassword.$error" show-validation-on-submit>
|
||||
<span class="help-inline" ng-message="required">Required</span>
|
||||
<span class="help-inline" ng-message="valServerField">{{passwordForm.oldPassword.errorMsg}}</span>
|
||||
<span class="help-inline" ng-message="valServerField">{{vm.passwordForm.oldPassword.errorMsg}}</span>
|
||||
</span>
|
||||
</umb-control-group>
|
||||
|
||||
<umb-control-group alias="password" label="@user_newPassword" ng-if="!showReset" required="true">
|
||||
<input type="password" name="password" ng-model="passwordValues.newPassword"
|
||||
<umb-control-group alias="password" label="@user_newPassword" ng-if="!vm.showReset" required="true">
|
||||
<input type="password" name="password" ng-model="vm.passwordValues.newPassword"
|
||||
class="input-block-level umb-textstring textstring"
|
||||
required
|
||||
val-server-field="password"
|
||||
ng-minlength="{{config.minPasswordLength}}"
|
||||
ng-minlength="{{vm.config.minPasswordLength}}"
|
||||
no-dirty-check />
|
||||
<span ng-messages="passwordForm.password.$error" show-validation-on-submit >
|
||||
<span ng-messages="vm.passwordForm.password.$error" show-validation-on-submit>
|
||||
<span class="help-inline" ng-message="required">Required</span>
|
||||
<span class="help-inline" ng-message="minlength">Minimum {{config.minPasswordLength}} characters</span>
|
||||
<span class="help-inline" ng-message="valServerField">{{passwordForm.password.errorMsg}}</span>
|
||||
<span class="help-inline" ng-message="minlength">Minimum {{vm.config.minPasswordLength}} characters</span>
|
||||
<span class="help-inline" ng-message="valServerField">{{vm.passwordForm.password.errorMsg}}</span>
|
||||
</span>
|
||||
</umb-control-group>
|
||||
|
||||
<umb-control-group alias="confirmpassword" label="@user_confirmNewPassword" ng-if="!showReset" required="true">
|
||||
<input type="password" name="confirmpassword" ng-model="passwordValues.confirm"
|
||||
<umb-control-group alias="confirmpassword" label="@user_confirmNewPassword" ng-if="!vm.showReset" required="true">
|
||||
<input type="password" name="confirmpassword" ng-model="vm.passwordValues.confirm"
|
||||
class="input-block-level umb-textstring textstring"
|
||||
val-compare="password"
|
||||
no-dirty-check />
|
||||
<span ng-messages="passwordForm.confirmpassword.$error" show-validation-on-submit >
|
||||
<span ng-messages="vm.passwordForm.confirmpassword.$error" show-validation-on-submit>
|
||||
<span class="help-inline" ng-message="valCompare"><localize key="user_passwordMismatch">The confirmed password doesn't match the new password!</localize></span>
|
||||
</span>
|
||||
</umb-control-group>
|
||||
|
||||
<a href="" ng-click="cancelChange()" ng-show="showCancelBtn()" class="btn btn-small">
|
||||
<a href="" ng-click="vm.cancelChange()" ng-show="vm.showCancelBtn()" class="btn btn-small">
|
||||
<localize key="general_cancel">Cancel</localize>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -348,6 +348,9 @@
|
||||
<DevelopmentServerPort>8200</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:8200/</IISUrl>
|
||||
<DevelopmentServerPort>8130</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:8130</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>False</UseCustomServer>
|
||||
<CustomServerUrl>
|
||||
|
||||
Reference in New Issue
Block a user