diff --git a/frontend/src/components/dialog/tag-name.js b/frontend/src/components/dialog/tag-name.js
index c0fb4e78919..1fbc1061507 100644
--- a/frontend/src/components/dialog/tag-name.js
+++ b/frontend/src/components/dialog/tag-name.js
@@ -22,6 +22,14 @@ class TagName extends React.Component {
this.input = React.createRef();
}
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (nextProps.tag.name !== this.props.tag.name) {
+ this.setState({
+ tagName: nextProps.tag.name,
+ });
+ }
+ }
+
toggleMode = () => {
this.setState({
isEditing: !this.state.isEditing
@@ -51,6 +59,10 @@ class TagName extends React.Component {
this.toggleMode();
this.updateTagName(e);
}
+ else if (e.key == 'Escape') {
+ e.nativeEvent.stopImmediatePropagation();
+ this.toggleMode();
+ }
};
onInputBlur = (e) => {
diff --git a/frontend/src/components/dirent-grid-view/dirent-grid-view.js b/frontend/src/components/dirent-grid-view/dirent-grid-view.js
index e40b92efcc0..4c02422f7da 100644
--- a/frontend/src/components/dirent-grid-view/dirent-grid-view.js
+++ b/frontend/src/components/dirent-grid-view/dirent-grid-view.js
@@ -48,8 +48,8 @@ const propTypes = {
onAddFolder: PropTypes.func.isRequired,
showDirentDetail: PropTypes.func.isRequired,
onItemRename: PropTypes.func.isRequired,
- posX: PropTypes.number.isRequired,
- posY: PropTypes.number.isRequired,
+ posX: PropTypes.number,
+ posY: PropTypes.number,
};
class DirentGridView extends React.Component {
diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js
index dfab5b9bb8f..b7f2a62749a 100644
--- a/frontend/src/components/dirent-list-view/dirent-list-item.js
+++ b/frontend/src/components/dirent-list-view/dirent-list-item.js
@@ -54,7 +54,7 @@ const propTypes = {
showDirentDetail: PropTypes.func.isRequired,
onItemsMove: PropTypes.func.isRequired,
onShowDirentsDraggablePreview: PropTypes.func,
- loadDirentList: PropTypes.func.isRequired,
+ loadDirentList: PropTypes.func,
};
class DirentListItem extends React.Component {
diff --git a/frontend/src/components/popover/list-tag-popover.css b/frontend/src/components/popover/list-tag-popover.css
new file mode 100644
index 00000000000..0ef871a2ed0
--- /dev/null
+++ b/frontend/src/components/popover/list-tag-popover.css
@@ -0,0 +1,31 @@
+.list-tag-popover .popover {
+ width: 500px;
+ max-width: 500px;
+}
+
+.list-tag-popover .add-tag-link {
+ cursor: pointer;
+}
+
+.list-tag-popover .tag-list-footer {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: flex-end;
+ padding: 1rem;
+ border-top: 1px solid #dedede;
+}
+
+.list-tag-popover .tag-list-footer .item-text {
+ color: #ff8000;
+ cursor: pointer;
+}
+
+.list-tag-popover .tag-list-footer a:hover {
+ text-decoration: none;
+}
+
+.list-tag-popover .tag-color {
+ width: 20px;
+ height: 20px;
+}
diff --git a/frontend/src/components/popover/list-tag-popover.js b/frontend/src/components/popover/list-tag-popover.js
new file mode 100644
index 00000000000..3eadd4c2b68
--- /dev/null
+++ b/frontend/src/components/popover/list-tag-popover.js
@@ -0,0 +1,154 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { v4 as uuidv4 } from 'uuid';
+import { gettext } from '../../utils/constants';
+import { seafileAPI } from '../../utils/seafile-api';
+import { Utils } from '../../utils/utils';
+import toaster from '../toast';
+import RepoTag from '../../models/repo-tag';
+import TagListItem from './tag-list-item';
+import VirtualTagListItem from './virtual-tag-list-item';
+import TagListFooter from './tag-list-footer';
+import { TAG_COLORS } from '../../constants/';
+
+import '../../css/repo-tag.css';
+import './list-tag-popover.css';
+
+export default class ListTagPopover extends React.Component {
+
+ static propTypes = {
+ repoID: PropTypes.string.isRequired,
+ onListTagCancel: PropTypes.func.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ repotagList: []
+ };
+ }
+
+ componentDidMount() {
+ this.loadTags();
+ }
+
+ loadTags = () => {
+ seafileAPI.listRepoTags(this.props.repoID).then(res => {
+ let repotagList = [];
+ res.data.repo_tags.forEach(item => {
+ let repo_tag = new RepoTag(item);
+ repotagList.push(repo_tag);
+ });
+ this.setState({ repotagList });
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ };
+
+ updateTags = (repotagList) => {
+ this.setState({ repotagList });
+ };
+
+ onDeleteTag = (tag) => {
+ const { repoID } = this.props;
+ const { id: targetTagID } = tag;
+ seafileAPI.deleteRepoTag(repoID, targetTagID).then((res) => {
+ this.setState({
+ repotagList: this.state.repotagList.filter(tag => tag.id != targetTagID)
+ });
+ }).catch((error) => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ };
+
+ createVirtualTag = (e) => {
+ e.preventDefault();
+ let { repotagList } = this.state;
+ let virtual_repo_tag = {
+ name: '',
+ color: TAG_COLORS[Math.floor(Math.random() * TAG_COLORS.length)], // generate random tag color for virtual tag
+ id: `virtual-tag-${uuidv4()}`,
+ is_virtual: true,
+ };
+ repotagList.push(virtual_repo_tag);
+ this.setState({ repotagList });
+ };
+
+ deleteVirtualTag = (virtualTag) => {
+ let { repotagList } = this.state;
+ let index = repotagList.findIndex(item => item.id === virtualTag.id);
+ repotagList.splice(index, 1);
+ this.setState({ repotagList });
+ };
+
+ updateVirtualTag = (virtualTag, data) => {
+ const repoID = this.props.repoID;
+ const { repotagList } = this.state;
+ const index = repotagList.findIndex(item => item.id === virtualTag.id);
+ if (index < 0) return null;
+
+ // If virtual tag color is updated and virtual tag name is empty, it will be saved to local state, don't save it to the server
+ if (data.color) {
+ virtualTag.color = data.color;
+ repotagList[index] = virtualTag;
+ this.setState({ repotagList });
+ return;
+ }
+
+ // If virtual tag name is updated and name is not empty, virtual tag color use default, save it to the server
+ if (data.name && data.name.length > 0) {
+ let color = virtualTag.color;
+ let name = data.name;
+ seafileAPI.createRepoTag(repoID, name, color).then((res) => {
+ // After saving sag to the server, replace the virtual tag with newly created tag
+ repotagList[index] = new RepoTag(res.data.repo_tag);
+ this.setState({ repotagList });
+ }).catch((error) => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ }
+ };
+
+ render() {
+ return (
+
+
+ {this.state.repotagList.map((repoTag, index) => {
+ if (repoTag.is_virtual) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ })}
+
+
+ {gettext('Create a new tag')}
+
+
+
+ );
+ }
+}
diff --git a/frontend/src/components/popover/tag-list-footer.js b/frontend/src/components/popover/tag-list-footer.js
new file mode 100644
index 00000000000..e250aeecee2
--- /dev/null
+++ b/frontend/src/components/popover/tag-list-footer.js
@@ -0,0 +1,137 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { Tooltip } from 'reactstrap';
+import { seafileAPI } from '../../utils/seafile-api';
+import { gettext } from '../../utils/constants';
+import { Utils } from '../../utils/utils';
+import RepoTag from '../../models/repo-tag';
+import toaster from '../toast';
+
+export default class TagListFooter extends Component {
+
+ static propTypes = {
+ repoID: PropTypes.string.isRequired,
+ toggle: PropTypes.func.isRequired,
+ repotagList: PropTypes.array.isRequired,
+ updateTags: PropTypes.func.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ showTooltip: false,
+ };
+ }
+
+ toggleTooltip = () => {
+ this.setState({showTooltip: !this.state.showTooltip});
+ };
+
+ onClickImport = () => {
+ this.importOptionsInput.click();
+ };
+
+ importTagsInputChange = () => {
+ if (!this.importOptionsInput.files || !this.importOptionsInput.files.length) {
+ toaster.warning(gettext('Please select a file'));
+ return;
+ }
+ const fileReader = new FileReader();
+ fileReader.onload = this.onImportTags.bind(this);
+ fileReader.onerror = this.onImportTagsError.bind(this);
+ fileReader.readAsText(this.importOptionsInput.files[0]);
+ };
+
+ getValidTags = (tags) => {
+ let validTags = [];
+ let tagNameMap = {};
+ this.props.repotagList.forEach(tag => tagNameMap[tag.name] = true);
+ for (let i = 0; i < tags.length; i++) {
+ if (!tags[i] || typeof tags[i] !== 'object' || !tags[i].name || !tags[i].color) {
+ continue;
+ }
+ if (!tagNameMap[tags[i].name]) {
+ validTags.push(
+ {
+ name: tags[i].name,
+ color: tags[i].color,
+ }
+ );
+ tagNameMap[tags[i].name] = true;
+ }
+ }
+ return validTags;
+ };
+
+ onImportTags = (event) => {
+ let tags = [];
+ try {
+ tags = JSON.parse(event.target.result); // handle JSON file format is error
+ } catch (error) {
+ toaster.danger(gettext('The imported tags are invalid'));
+ return;
+ }
+ if (!Array.isArray(tags) || tags.length === 0) {
+ toaster.danger(gettext('The imported tags are invalid'));
+ return;
+ }
+ let validTags = this.getValidTags(tags);
+ if (validTags.length === 0) {
+ toaster.warning(gettext('The imported tag already exists'));
+ return;
+ }
+ seafileAPI.createRepoTags(this.props.repoID, validTags).then((res) => {
+ toaster.success(gettext('Tags imported'));
+ let repotagList = [];
+ res.data.repo_tags.forEach(item => {
+ let repo_tag = new RepoTag(item);
+ repotagList.push(repo_tag);
+ });
+ this.props.updateTags(repotagList);
+ }).catch(error => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ this.importOptionsInput.value = null;
+ };
+
+ onImportTagsError = () => {
+ toaster.success(gettext('Failed to import tags. Please reupload.'));
+ };
+
+ getDownloadUrl = () => {
+ const tags = this.props.repotagList.map(item => {
+ return { name: item.name, color: item.color };
+ });
+ return `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(tags))}`;
+ };
+
+ render() {
+ return (
+
+
+
+ {gettext('Use the import/export function to transfer tags quickly to another library. (The export is in JSON format.)')}
+
+
this.importOptionsInput = ref}
+ accept='.json'
+ className="d-none"
+ onChange={this.importTagsInputChange}
+ />
+
{gettext('Import tags')}
+
|
+
+ {gettext('Export tags')}
+
+
+ );
+ }
+}
diff --git a/frontend/src/components/popover/tag-list-item.js b/frontend/src/components/popover/tag-list-item.js
new file mode 100644
index 00000000000..69109e4a2cd
--- /dev/null
+++ b/frontend/src/components/popover/tag-list-item.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { gettext } from '../../utils/constants';
+import TagColor from '../dialog/tag-color';
+import TagName from '../dialog/tag-name';
+
+import '../../css/repo-tag.css';
+import './list-tag-popover.css';
+
+const tagListItemPropTypes = {
+ item: PropTypes.object.isRequired,
+ repoID: PropTypes.string.isRequired,
+ onDeleteTag : PropTypes.func.isRequired
+};
+
+class TagListItem extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isTagHighlighted: false
+ };
+ }
+
+ onMouseOver = () => {
+ this.setState({
+ isTagHighlighted: true
+ });
+ };
+
+ onMouseOut = () => {
+ this.setState({
+ isTagHighlighted: false
+ });
+ };
+
+ deleteTag = () => {
+ this.props.onDeleteTag(this.props.item);
+ };
+
+ render() {
+ const { isTagHighlighted } = this.state;
+ const { item, repoID } = this.props;
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+TagListItem.propTypes = tagListItemPropTypes;
+
+export default TagListItem;
diff --git a/frontend/src/components/popover/virtual-tag-color.js b/frontend/src/components/popover/virtual-tag-color.js
new file mode 100644
index 00000000000..74348884890
--- /dev/null
+++ b/frontend/src/components/popover/virtual-tag-color.js
@@ -0,0 +1,96 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Popover, PopoverBody } from 'reactstrap';
+import { TAG_COLORS } from '../../constants';
+
+import '../../css/repo-tag.css';
+
+export default class VirtualTagColor extends React.Component {
+
+ static propTypes = {
+ updateVirtualTag: PropTypes.func.isRequired,
+ tag: PropTypes.object.isRequired,
+ repoID: PropTypes.string.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ tagColor: this.props.tag.color,
+ isPopoverOpen: false
+ };
+ }
+
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (nextProps.tag.color !== this.props.tag.color) {
+ this.setState({
+ tagColor: nextProps.tag.color,
+ });
+ }
+ }
+
+ togglePopover = () => {
+ this.setState({
+ isPopoverOpen: !this.state.isPopoverOpen
+ });
+ };
+
+ selectTagColor = (e) => {
+ const newColor = e.target.value;
+ this.props.updateVirtualTag(this.props.tag, { color: newColor });
+ this.setState({
+ tagColor: newColor,
+ isPopoverOpen: !this.state.isPopoverOpen,
+ });
+ };
+
+ render() {
+ const { isPopoverOpen, tagColor } = this.state;
+ const { tag } = this.props;
+ const { id, color } = tag;
+
+ let colorList = [...TAG_COLORS];
+ // for color from previous color options
+ if (colorList.indexOf(color) == -1) {
+ colorList.unshift(color);
+ }
+
+ return (
+
+
+
+
+
+
+
+ {colorList.map((item, index)=>{
+ return (
+
+
+
+
+
+
+
+
+ );
+ })
+ }
+
+
+
+
+ );
+ }
+}
diff --git a/frontend/src/components/popover/virtual-tag-list-item.js b/frontend/src/components/popover/virtual-tag-list-item.js
new file mode 100644
index 00000000000..6334c4ff4da
--- /dev/null
+++ b/frontend/src/components/popover/virtual-tag-list-item.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { gettext } from '../../utils/constants';
+import VirtualTagColor from './virtual-tag-color';
+import VirtualTagName from './virtual-tag-name';
+
+import '../../css/repo-tag.css';
+import './list-tag-popover.css';
+
+export default class VirtualTagListItem extends React.Component {
+
+ static propTypes = {
+ item: PropTypes.object.isRequired,
+ repoID: PropTypes.string.isRequired,
+ deleteVirtualTag: PropTypes.func.isRequired,
+ updateVirtualTag: PropTypes.func.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isTagHighlighted: false
+ };
+ }
+
+ onMouseOver = () => {
+ this.setState({ isTagHighlighted: true });
+ };
+
+ onMouseOut = () => {
+ this.setState({ isTagHighlighted: false });
+ };
+
+ deleteVirtualTag = () => {
+ this.props.deleteVirtualTag(this.props.item);
+ };
+
+ render() {
+ const { isTagHighlighted } = this.state;
+ const { item, repoID } = this.props;
+ return (
+
+
+
+
+
+ );
+ }
+}
diff --git a/frontend/src/components/popover/virtual-tag-name.js b/frontend/src/components/popover/virtual-tag-name.js
new file mode 100644
index 00000000000..e22b909e800
--- /dev/null
+++ b/frontend/src/components/popover/virtual-tag-name.js
@@ -0,0 +1,89 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import '../../css/repo-tag.css';
+
+export default class VirtualTagName extends React.Component {
+
+ static propTypes = {
+ updateVirtualTag: PropTypes.func.isRequired,
+ tag: PropTypes.object.isRequired,
+ repoID: PropTypes.string.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ tagName: this.props.tag.name,
+ isEditing: true,
+ };
+ this.input = React.createRef();
+ }
+
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (nextProps.tag.name !== this.props.tag.name) {
+ this.setState({
+ tagName: nextProps.tag.name,
+ });
+ }
+ }
+
+ componentDidMount() {
+ setTimeout(() => {
+ this.input.current.focus();
+ }, 1);
+ }
+
+ toggleMode = () => {
+ this.setState({
+ isEditing: !this.state.isEditing
+ });
+ };
+
+ updateTagName = (e) => {
+ const newName = e.target.value;
+ this.props.updateVirtualTag(this.props.tag, { name: newName });
+ this.setState({
+ tagName: newName
+ });
+ };
+
+ onInputKeyDown = (e) => {
+ if (e.key == 'Enter') {
+ this.toggleMode();
+ this.updateTagName(e);
+ }
+ else if (e.key == 'Escape') {
+ e.nativeEvent.stopImmediatePropagation();
+ this.toggleMode();
+ }
+ };
+
+ onInputBlur = (e) => {
+ this.toggleMode();
+ this.updateTagName(e);
+ };
+
+ render() {
+ const { isEditing, tagName } = this.state;
+ return (
+
+ {isEditing ?
+ :
+ {tagName}
+ }
+
+ );
+ }
+}
diff --git a/frontend/src/components/toast/alert.js b/frontend/src/components/toast/alert.js
index dbfe69ef45d..7e51ad501a3 100644
--- a/frontend/src/components/toast/alert.js
+++ b/frontend/src/components/toast/alert.js
@@ -105,7 +105,7 @@ class Alert extends React.PureComponent {
Alert.propTypes = {
onRemove: PropTypes.func.isRequired,
- children: PropTypes.any.isRequired,
+ children: PropTypes.any,
title: PropTypes.string.isRequired,
intent: PropTypes.string.isRequired,
};
diff --git a/frontend/src/components/tree-view/tree-view.js b/frontend/src/components/tree-view/tree-view.js
index f0b8393f0ff..4812080d60a 100644
--- a/frontend/src/components/tree-view/tree-view.js
+++ b/frontend/src/components/tree-view/tree-view.js
@@ -19,8 +19,8 @@ const propTypes = {
currentRepoInfo: PropTypes.object,
selectedDirentList: PropTypes.array,
onItemsMove: PropTypes.func,
- posX: PropTypes.number.isRequired,
- posY: PropTypes.number.isRequired,
+ posX: PropTypes.number,
+ posY: PropTypes.number,
};
const PADDING_LEFT = 20;
diff --git a/frontend/src/css/repo-tag.css b/frontend/src/css/repo-tag.css
index e5f5f588744..76381988e91 100644
--- a/frontend/src/css/repo-tag.css
+++ b/frontend/src/css/repo-tag.css
@@ -58,3 +58,22 @@
.tag-color-option .colorinput-input:checked ~ .colorinput-color .color-selected {
opacity: 1;
}
+
+/* tag-color */
+.tag-color-popover .popover {
+ max-width: 360px;
+}
+
+.tag-color-popover .tag-color {
+ width: 20px;
+ height: 20px;
+}
+
+.tag-color-popover .colorinput-color {
+ width: 20px;
+ height: 20px;
+}
+
+.tag-color-popover .tag-color-option .colorinput-input:checked ~ .colorinput-color .color-selected {
+ font-size: 12px;
+}
diff --git a/frontend/src/pages/lib-content-view/lib-content-container.js b/frontend/src/pages/lib-content-view/lib-content-container.js
index 1d7bee64ed7..dc753ab001e 100644
--- a/frontend/src/pages/lib-content-view/lib-content-container.js
+++ b/frontend/src/pages/lib-content-view/lib-content-container.js
@@ -87,8 +87,8 @@ const propTypes = {
onListContainerScroll: PropTypes.func.isRequired,
onDirentClick: PropTypes.func.isRequired,
direntDetailPanelTab: PropTypes.string,
- loadDirentList: PropTypes.func.isRequired,
- fullDirentList: PropTypes.array.isRequired,
+ loadDirentList: PropTypes.func,
+ fullDirentList: PropTypes.array,
};
class LibContentContainer extends React.Component {
diff --git a/frontend/src/pages/markdown-editor/rich-markdown-editor/index.js b/frontend/src/pages/markdown-editor/rich-markdown-editor/index.js
index 90fb0917ac2..0f35fc7365e 100644
--- a/frontend/src/pages/markdown-editor/rich-markdown-editor/index.js
+++ b/frontend/src/pages/markdown-editor/rich-markdown-editor/index.js
@@ -86,8 +86,6 @@ class RichMarkdownEditor extends React.Component {
editorApi={this.props.editorApi}
onChange={this.props.onChange}
resetRichValue={this.props.resetRichValue}
- isSupportComment={false}
- onAddComment={() => {}}
/>
diff --git a/seahub/api2/endpoints/repo_tags.py b/seahub/api2/endpoints/repo_tags.py
index 3871ca5c084..f54038fee7c 100644
--- a/seahub/api2/endpoints/repo_tags.py
+++ b/seahub/api2/endpoints/repo_tags.py
@@ -117,6 +117,54 @@ def post(self, request, repo_id):
return Response({"repo_tag": repo_tag.to_dict()}, status=status.HTTP_201_CREATED)
+ def put(self, request, repo_id):
+ """bulk add repo_tags.
+ """
+
+ # argument check
+ tags = request.data.get('tags')
+ if not tags:
+ error_msg = 'tags invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # resource check
+ repo = seafile_api.get_repo(repo_id)
+ if not repo:
+ error_msg = 'Library %s not found.' % repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # permission check
+ if check_folder_permission(request, repo_id, '/') != PERMISSION_READ_WRITE:
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ tag_objs = list()
+ try:
+ for tag in tags:
+ name = tag.get('name' ,'')
+ color = tag.get('color', '')
+ if name and color:
+ obj = RepoTags(repo_id=repo_id, name=name, color=color)
+ tag_objs.append(obj)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'tags invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ repo_tag_list = RepoTags.objects.bulk_create(tag_objs)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ repo_tags = list()
+ for repo_tag in repo_tag_list:
+ res = repo_tag.to_dict()
+ repo_tags.append(res)
+
+ return Response({"repo_tags": repo_tags}, status=status.HTTP_200_OK)
+
class RepoTagView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)