mirror of
https://github.com/QRouland/UTPass.git
synced 2024-12-22 18:02:40 +00:00
UP
This commit is contained in:
parent
7dc320f8ce
commit
2390cc983b
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,6 @@
|
|||||||
cmake-build-debug
|
cmake-build-debug
|
||||||
build
|
build
|
||||||
|
taglib-build
|
||||||
.clickable
|
.clickable
|
||||||
.idea
|
.idea
|
||||||
local
|
local
|
||||||
|
|
||||||
|
@ -45,8 +45,6 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/manifest.json DESTINATION ${CMAKE_INST
|
|||||||
install(FILES ${PROJECT_NAME}.apparmor DESTINATION ${DATA_DIR})
|
install(FILES ${PROJECT_NAME}.apparmor DESTINATION ${DATA_DIR})
|
||||||
install(DIRECTORY qml DESTINATION ${DATA_DIR})
|
install(DIRECTORY qml DESTINATION ${DATA_DIR})
|
||||||
install(DIRECTORY assets DESTINATION ${DATA_DIR})
|
install(DIRECTORY assets DESTINATION ${DATA_DIR})
|
||||||
install(DIRECTORY password-store DESTINATION ${DATA_DIR})
|
|
||||||
install(DIRECTORY gpghome DESTINATION ${DATA_DIR})
|
|
||||||
install(DIRECTORY local/bin DESTINATION ${DATA_DIR})
|
install(DIRECTORY local/bin DESTINATION ${DATA_DIR})
|
||||||
file(GLOB_RECURSE BIN_FILES
|
file(GLOB_RECURSE BIN_FILES
|
||||||
"local/bin/*")
|
"local/bin/*")
|
||||||
@ -97,18 +95,3 @@ file(GLOB_RECURSE PROJECT_SRC_FILES
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_custom_target(${PROJECT_NAME}_FILES ALL SOURCES ${PROJECT_SRC_FILES})
|
add_custom_target(${PROJECT_NAME}_FILES ALL SOURCES ${PROJECT_SRC_FILES})
|
||||||
|
|
||||||
# Tests
|
|
||||||
enable_testing(true)
|
|
||||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
|
||||||
include_directories(tests/plugins)
|
|
||||||
|
|
||||||
find_package(Qt5Test)
|
|
||||||
|
|
||||||
add_executable(TestGpg tests/plugins/TestGpg.cpp)
|
|
||||||
add_executable(TestPass tests/plugins/TestPass.cpp)
|
|
||||||
target_link_libraries(TestGpg Qt5::Test)
|
|
||||||
target_link_libraries(TestPass Qt5::Test)
|
|
||||||
|
|
||||||
add_test(TestGpg TestGpg)
|
|
||||||
add_test(TestPass TestPass)
|
|
||||||
|
10
README.md
10
README.md
@ -11,15 +11,15 @@ A password management app for Ubuntu Touch aiming to be compatible with [ZX2C4
|
|||||||
For more options/details see the [clickable documentation](http://clickable.bhdouglass.com/en/latest/index.html)
|
For more options/details see the [clickable documentation](http://clickable.bhdouglass.com/en/latest/index.html)
|
||||||
|
|
||||||
## Custom clickable command
|
## Custom clickable command
|
||||||
* ```clickable test ``` : run test for all plugins
|
* ```clickable test ``` : build and run test for all plugins
|
||||||
* ```clickable test_gpg ``` : run test for gpg plugin
|
* ```clickable test_gpg ``` : build and run test for gpg plugin
|
||||||
* ```clickable test_pass``` : run test for pass plugin
|
* ```clickable test_pass``` : build and run test for pass plugin
|
||||||
* ```clickable style ``` : reformat the code (Required : [astyle](astyle.sourceforge.ne) & [https://github.com/jesperhh/qmlfmt](https://github.com/jesperhh/qmlfmt))
|
* ```clickable style ``` : reformat the code (Required : [astyle](astyle.sourceforge.ne) & [qmlfmt](https://github.com/jesperhh/qmlfmt))
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Any contributions are welcome using the github issue & pull request system.
|
Any contributions are welcome using the github issue & pull request system.
|
||||||
Please try to respect the code style format by runnning ```clickable style``` before committing.
|
Please try to respect the code style format by running ```clickable style``` before committing.
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
"kill": "UTPass",
|
"kill": "UTPass",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "third/clean.sh && rm -rf build",
|
"clean": "third/clean.sh && rm -rf build",
|
||||||
"style": "astyle --options=.astylerc \"plugins/*.cpp,*.h\" && qmlfmt -w qml",
|
"style": "astyle --options=.astylerc \"plugins/*.cpp,*.h\" && astyle --options=.astylerc \"tests/*.cpp,*.h\" && qmlfmt -w qml",
|
||||||
"test": "clickable run \"cd build && make test\"",
|
"test": "clickable build --arch amd64 && clickable run \"cd build && make test\"",
|
||||||
"test_gpg": "clickable run \"cd build && ./TestGpg\"",
|
"test_gpg": "clickable build --arch amd64 && clickable run \"cd build && ./TestGpg\"",
|
||||||
"test_pass": "clickable run \"cd build && ./TestPass\""
|
"test_pass": "clickable build --arch amd64 && clickable run \"cd build && ./TestPass\""
|
||||||
},
|
},
|
||||||
"dependencies_build": [
|
"dependencies_build": [
|
||||||
"texinfo",
|
"texinfo",
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
mQENBFyNdOEBCADl2oWeYkmVBDWoWgZdkpbV5VRJJFATLsu5aHBuQO/C1mn2RlL2
|
|
||||||
jIpIzI5mwviAw9RN0KnLdHvp3n3JkJPZ8tB3Sk9SD8qhr6ae2DbIpySMscYC9+Go
|
|
||||||
t0mrGyB3w+Y5etfZ/1dRNx6/vYaWYIG6bKfJettt/zLJcjpkIKcrN4OKyN2wXz3y
|
|
||||||
EiAiJvMntdgLslURl93RyNuVR6UaE4TchtDqRc2KvXAxrf6NUYd4KxvUgUd0TFPs
|
|
||||||
s3SRs+cAcRmTzxv/c40sBw3z0B9rBB7T7oPgGUA6NhErvBwpF9MLN+6ucZ1HHLLH
|
|
||||||
dmCd7q2OT7wZ9L6zILmKvJcK13V4FyO9zOALABEBAAG0I1Rlc3QgKFRlc3QgZ3Bn
|
|
||||||
IGtleSkgPHRlc3RAdGVzdC5vcmc+iQFOBBMBCAA4FiEE6JXydyCXAQnkfMmTuV1W
|
|
||||||
R67EDnIFAlyNdOECGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQuV1WR67E
|
|
||||||
DnIchAgAvV5q4/Hktlu3RIgo8KkGksMOS5XhJrr6fZ8bUgqpwL3oEfZ3Se5aS7yN
|
|
||||||
5M7NT8foB2zK2moBICMYpxBxQoGjxFosv94FqX9+XMiRc2Di6MwLwKkWfu0HoPEi
|
|
||||||
e701iTo53r6K84TIKNrRsKyg6C/pRqNNwSp1YcvG11eUnG5teZMcb1xsMfx4O2/s
|
|
||||||
mcrySo0ZjAfYnh2poxf4yy9xTryPrDnaY/EFj+4uBMLH8jG3QQiAH1N6wHhi/vwj
|
|
||||||
FaaaBtRxcVU/obGDg2LHTVxItv81xzSbLf8JdIGOKjwFed+DjaoSlEzPnuaEZf/M
|
|
||||||
7mE6fiGbIhFgUlwEGomptZDC1fg3M7kBDQRcjXThAQgAmuSsbRoLfiSoij5CWiP4
|
|
||||||
UUvhIEt5d4KkMRRvuWXkJo4FWs2tmNWIb1tiXuKhX+puLjP06LwfEyNT1jz75pgO
|
|
||||||
tSQ0Yz55Hn25CWOcyWF/iIoIjjw3WhQ0a59Ajk8tVdVrTEhlcQ+m7dH1igyMO1vv
|
|
||||||
iH1eTu+TXqWDF1+oYTZH0iTMYreCNbz2RcFHZQKdWK8GI1DE/qeKLHf+XYGTVQH4
|
|
||||||
fRnGaX7T5DdnklHKVGi4iILOKn5aofTIg14roS9yfDMK6vmNr7BkzqAe9+WfYC0K
|
|
||||||
TU3hX1z5SrjnYnBb531MaaCotUEI3DbNeoNsuH3Hx0WLHR1Q55Hh+KAxhMUKTR4P
|
|
||||||
uwARAQABiQE2BBgBCAAgFiEE6JXydyCXAQnkfMmTuV1WR67EDnIFAlyNdOECGwwA
|
|
||||||
CgkQuV1WR67EDnKjKwgA3yexUAoTe9sDRKO710MSWhPAn3DZ8qMo8EqmNegG86PO
|
|
||||||
/mD6BPKo9503pqGGXYoFBcqsmFX07uvy0evCsqO15xuDWwOhNX5fm2LeSsNEkhhC
|
|
||||||
2wvJVQPdekj9KmOrRRRcr6DlR0Yl7+BJX1+zF8tYwtU4tiY+bCOVRoa1KvTXUwcy
|
|
||||||
sRTQ9xWguCP8Ai1GyZS0P8lEU0nCS2KrgU/XKQVW7o2OtBiywJbmVCDw15vIq3kN
|
|
||||||
akRrU5DvYCelUjjzgj+HC3MEE5fV4UsuFLKw3QMmekzFfa6OagRb/FYYZ5ZL+tGI
|
|
||||||
cf2W57AJOHpgYvxqrY5M1UfKLf8kCYPt3AG0XiD5mw==
|
|
||||||
=OHyE
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
@ -43,7 +43,7 @@ Gpg::Gpg()
|
|||||||
qDebug() << "GpgME Engine Version :" << engineInfo(OpenPGP).version();
|
qDebug() << "GpgME Engine Version :" << engineInfo(OpenPGP).version();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Gpg::list_dir()
|
void Gpg::listDir()
|
||||||
{
|
{
|
||||||
qDebug() << "hello world!";
|
qDebug() << "hello world!";
|
||||||
}
|
}
|
||||||
@ -64,7 +64,7 @@ QString Gpg::decrypt(const QByteArray cipherText)
|
|||||||
return QString::fromUtf8(plainText);*/
|
return QString::fromUtf8(plainText);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Gpg::decrypt_file(const QString path)
|
QString Gpg::decryptFile(QString path)
|
||||||
{
|
{
|
||||||
/*QFile file(path);
|
/*QFile file(path);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
@ -91,7 +91,7 @@ QByteArray Gpg::encrypt(const QString str)
|
|||||||
return cipherText;*/
|
return cipherText;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Gpg::encrypt_file(const QString str, const QString path)
|
bool Gpg::encryptFile(QString str, QString path)
|
||||||
{
|
{
|
||||||
/*QFile file(path);
|
/*QFile file(path);
|
||||||
if (!file.open(QIODevice::WriteOnly)) {
|
if (!file.open(QIODevice::WriteOnly)) {
|
||||||
@ -102,7 +102,7 @@ bool Gpg::encrypt_file(const QString str, const QString path)
|
|||||||
return true;*/
|
return true;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Gpg::get_key_id(QString uid)
|
QString Gpg::getKeyId(QString uid)
|
||||||
{
|
{
|
||||||
qDebug() << "Getting the key id " << uid;
|
qDebug() << "Getting the key id " << uid;
|
||||||
auto *job = openpgp()->keyListJob(false, false, false);
|
auto *job = openpgp()->keyListJob(false, false, false);
|
||||||
@ -119,7 +119,7 @@ QString Gpg::get_key_id(QString uid)
|
|||||||
return kId;
|
return kId;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList Gpg::get_all_keys_id()
|
QStringList Gpg::getAllKeysId()
|
||||||
{
|
{
|
||||||
qDebug() << "Show all available key";
|
qDebug() << "Show all available key";
|
||||||
auto job = openpgp()->keyListJob(false, false, false);
|
auto job = openpgp()->keyListJob(false, false, false);
|
||||||
@ -140,7 +140,7 @@ QStringList Gpg::get_all_keys_id()
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Gpg::import_key(QString path)
|
bool Gpg::importKey(QString path)
|
||||||
{
|
{
|
||||||
qDebug() << "Importing the key file" << path;
|
qDebug() << "Importing the key file" << path;
|
||||||
QFile file(path);
|
QFile file(path);
|
||||||
|
@ -9,16 +9,16 @@ class Gpg : public QObject
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
Gpg();
|
Gpg();
|
||||||
~Gpg() = default;
|
~Gpg() override = default;
|
||||||
|
|
||||||
Q_INVOKABLE void list_dir();
|
Q_INVOKABLE void listDir();
|
||||||
Q_INVOKABLE QString get_key_id(QString uid);
|
Q_INVOKABLE QString getKeyId(QString uid);
|
||||||
Q_INVOKABLE QStringList get_all_keys_id();
|
Q_INVOKABLE QStringList getAllKeysId();
|
||||||
Q_INVOKABLE bool import_key(QString path);
|
Q_INVOKABLE bool importKey(QString path);
|
||||||
Q_INVOKABLE QString decrypt(QByteArray plainText);
|
Q_INVOKABLE QString decrypt(QByteArray plainText);
|
||||||
Q_INVOKABLE QString decrypt_file(QString path);
|
Q_INVOKABLE QString decryptFile(QString path);
|
||||||
Q_INVOKABLE QByteArray encrypt(QString str);
|
Q_INVOKABLE QByteArray encrypt(QString str);
|
||||||
Q_INVOKABLE bool encrypt_file(QString str, QString path);
|
Q_INVOKABLE bool encryptFile(QString str, QString path);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
#include "plugin.h"
|
#include "plugin.h"
|
||||||
#include "gpg.h"
|
#include "gpg.h"
|
||||||
|
|
||||||
void FileSystemPlugin::registerTypes(const char *uri)
|
void GpgPlugin::registerTypes(const char *uri)
|
||||||
{
|
{
|
||||||
//@uri Pass
|
//@uri Pass
|
||||||
qmlRegisterSingletonType<Gpg>(uri, 1, 0, "Gpg", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Gpg; });
|
qmlRegisterSingletonType<Gpg>(uri, 1, 0, "Gpg", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Gpg; });
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
#ifndef PASSPLUGIN_H
|
#ifndef GPGPLUGIN_H
|
||||||
#define PASSPLUGIN_H
|
#define GPGPLUGIN_H
|
||||||
|
|
||||||
#include <QQmlExtensionPlugin>
|
#include <QQmlExtensionPlugin>
|
||||||
|
|
||||||
class FileSystemPlugin : public QQmlExtensionPlugin
|
class GpgPlugin : public QQmlExtensionPlugin
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PLUGIN_METADATA(IID
|
Q_PLUGIN_METADATA(IID
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QtCore/QStandardPaths>
|
||||||
|
#include <QtCore/QDir>
|
||||||
|
|
||||||
#include "pass.h"
|
#include "pass.h"
|
||||||
|
|
||||||
Pass::Pass()
|
Pass::Pass(){
|
||||||
{
|
pass_store = QStandardPaths::writableLocation(
|
||||||
|
QStandardPaths::AppDataLocation).append("/.password-store");
|
||||||
|
QDir dir(pass_store);
|
||||||
|
if (!dir.exists())
|
||||||
|
dir.mkpath(".");
|
||||||
|
qDebug() << "Password Store is :" << pass_store;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Pass::speak()
|
void Pass::speak()
|
||||||
{
|
{
|
||||||
qDebug() << "Starting app from main.cpp";
|
qDebug() << "Starting app from main.cpp";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
class Pass : public QObject
|
class Pass : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QString gpgHome;
|
QString pass_store;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Pass();
|
Pass();
|
||||||
|
@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: utpass.qrouland\n"
|
"Project-Id-Version: utpass.qrouland\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2019-03-21 16:44+0000\n"
|
"POT-Creation-Date: 2019-03-21 21:15+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -17,10 +17,6 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
"Content-Type: text/plain; charset=CHARSET\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
#: ../qml/pages/Settings.qml:8 ../qml/pages/headers/MainHeader.qml:49
|
|
||||||
msgid "Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../qml/pages/PasswordList.qml:16
|
#: ../qml/pages/PasswordList.qml:16
|
||||||
msgid "Back"
|
msgid "Back"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -29,20 +25,24 @@ msgstr ""
|
|||||||
msgid "No password found in the current folder"
|
msgid "No password found in the current folder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../qml/pages/Info.qml:9 ../qml/pages/headers/MainHeader.qml:56
|
#: ../qml/pages/Settings.qml:8 ../qml/pages/headers/MainHeader.qml:49
|
||||||
msgid "Info"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../qml/pages/Info.qml:43 ../qml/pages/headers/MainHeader.qml:8
|
#: ../qml/pages/headers/MainHeader.qml:8 ../qml/pages/headers/StackHeader.qml:8
|
||||||
#: ../qml/pages/headers/StackHeader.qml:8 UTPass.desktop.in.h:1
|
#: ../qml/pages/Info.qml:43 UTPass.desktop.in.h:1
|
||||||
msgid "UTPass"
|
msgid "UTPass"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../qml/pages/Info.qml:53
|
|
||||||
msgid "Suggest improvement(s) or report a bug(s)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: ../qml/pages/headers/MainHeader.qml:23
|
#: ../qml/pages/headers/MainHeader.qml:23
|
||||||
#: ../qml/pages/headers/MainHeader.qml:38
|
#: ../qml/pages/headers/MainHeader.qml:38
|
||||||
msgid "Search"
|
msgid "Search"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: ../qml/pages/headers/MainHeader.qml:56 ../qml/pages/Info.qml:9
|
||||||
|
msgid "Info"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: ../qml/pages/Info.qml:53
|
||||||
|
msgid "Suggest improvement(s) or report a bug(s)"
|
||||||
|
msgstr ""
|
||||||
|
@ -23,7 +23,7 @@ MainView {
|
|||||||
"pages/PasswordList.qml")))
|
"pages/PasswordList.qml")))
|
||||||
}
|
}
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
Gpg.import_key("password-store/public.key")
|
Gpg.importKey("password-store/public.key")
|
||||||
Gpg.get_all_keys_id()
|
Gpg.getAllKeysId()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
#include <QtTest/QtTest>
|
|
||||||
|
|
||||||
class TestGpg: public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
private slots:
|
|
||||||
void toUpper();
|
|
||||||
};
|
|
||||||
|
|
||||||
void TestGpg::toUpper()
|
|
||||||
{
|
|
||||||
QString str = "Hello";
|
|
||||||
QCOMPARE(str.toUpper(), QString("HELLO"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QTEST_MAIN(TestGpg)
|
|
||||||
#include "TestGpg.moc"
|
|
@ -1,17 +0,0 @@
|
|||||||
#include <QtTest/QtTest>
|
|
||||||
|
|
||||||
class TestPass: public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
private slots:
|
|
||||||
void toUpper();
|
|
||||||
};
|
|
||||||
|
|
||||||
void TestPass::toUpper()
|
|
||||||
{
|
|
||||||
QString str = "Hello";
|
|
||||||
QCOMPARE(str.toUpper(), QString("HELLO"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QTEST_MAIN(TestPass)
|
|
||||||
#include "TestPass.moc"
|
|
10
tests/unit/tst_gpg.qml
Normal file
10
tests/unit/tst_gpg.qml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import QtTest 1.0
|
||||||
|
import Ubuntu.Test 1.0
|
||||||
|
import Gpg 1.0
|
||||||
|
|
||||||
|
UbuntuTestCase {
|
||||||
|
name: "GpgTests"
|
||||||
|
function test_empty_gnuhome() {
|
||||||
|
Gpg::getListIds().empty()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user