Commit 45232aa1 authored by Bilal Elmoussaoui's avatar Bilal Elmoussaoui

Annotate properties and remove uneeded widgets

parent 9bbc478f
Pipeline #92005 failed with stages
in 3 minutes and 24 seconds
......@@ -251,7 +251,8 @@
],
"sources": [{
"type": "git",
"url": "https://source.puri.sm/Librem5/libhandy.git"
"url": "https://source.puri.sm/Librem5/libhandy.git",
"tag": "v0.0.10"
}]
},
{
......
......@@ -5,7 +5,7 @@
<metadata_license>CC0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>Authenticator</name>
<summary>Two-factor authentication code generator</summary>
<summary>A Two-Factor Authentication application</summary>
<description>
<p>Simple application that generates a two-factor authentication code, created for GNOME.</p>
<p>Features:</p>
......
[Desktop Entry]
Name=Authenticator
GenericName=Two-factor authentication
Comment=Two-factor authentication code generator
GenericName=Two-factor Authentication
Comment=A Two-Factor Authentication application
Type=Application
Exec=authenticator
Terminal=false
......
......@@ -7,7 +7,7 @@
<property name="type_hint">dialog</property>
<property name="program_name">Authenticator</property>
<property name="version">@VERSION@</property>
<property name="comments" translatable="yes">Two-factor authentication code generator.</property>
<property name="comments" translatable="yes">A Two-Factor Authentication application</property>
<property name="website">https://gitlab.gnome.org/World/Authenticator</property>
<property name="authors">Bilal Elmoussaoui</property>
<property name="translator_credits" translatable="yes">translator-credits</property>
......
......@@ -2,6 +2,7 @@
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<requires lib="libhandy" version="0.0"/>
<template class="AddAccountWindow" parent="GtkWindow">
<property name="can_focus">False</property>
<property name="modal">True</property>
......@@ -13,7 +14,7 @@
<property name="can_focus">False</property>
<property name="title" translatable="yes">Add a new account</property>
<child>
<object class="GtkButton" id="close_btn">
<object class="GtkButton" id="back_btn">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
......@@ -67,9 +68,11 @@
</object>
</child>
<child>
<object class="GtkOverlay" id="overlay">
<object class="HdyColumn" id="column">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="maximum_width">600</property>
<property name="linear_growth_width">600</property>
<child>
<placeholder/>
</child>
......
......@@ -21,46 +21,22 @@
</attributes>
</child>
</object>
<template class="AccountConfig" parent="GtkBox">
<template class="AccountConfig" parent="GtkOverlay">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkRevealer" id="notification">
<object class="GtkBox" id="main_container">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">start</property>
<property name="margin_top">42</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkOverlay">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="notification_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<style>
<class name="app-notification"/>
</style>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
</object>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">36</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
......@@ -99,7 +75,6 @@
<child internal-child="entry">
<object class="GtkEntry" id="provider_entry">
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="placeholder_text" translatable="yes">Provider</property>
<property name="completion">provider_completion</property>
</object>
......@@ -118,6 +93,7 @@
<property name="visibility">False</property>
<property name="secondary_icon_name">dialog-information-symbolic</property>
<property name="secondary_icon_tooltip_text" translatable="yes">Enable 2FA for this account</property>
<property name="secondary_icon_tooltip_markup" translatable="yes">Enable 2FA for this account</property>
<property name="placeholder_text" translatable="yes">2FA Token</property>
<property name="input_purpose">pin</property>
<signal name="changed" handler="account_edited" swapped="no"/>
......@@ -144,17 +120,15 @@
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">6</property>
<property name="position">2</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="index">-1</property>
</packing>
</child>
</template>
......
......@@ -12,7 +12,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButton" id="close_btn">
<object class="GtkButton" id="back_btn">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
......
......@@ -9,7 +9,11 @@
<property name="can_focus">False</property>
<property name="default_width">350</property>
<property name="default_height">500</property>
<child type="titlebar">
<property name="icon_name">@APP_ID@</property>
<style>
<class name="theme"/>
</style>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
......
......@@ -20,7 +20,7 @@ from gettext import gettext as _
from gi.repository import Gtk, GLib, Gio, Gdk, GObject
from Authenticator.widgets import Window, WindowView
from Authenticator.models import Database, Settings, Clipboard, Logger, Keyring
from Authenticator.models import Database, Settings, Logger, Keyring
class Application(Gtk.Application):
......@@ -281,7 +281,8 @@ class Application(Gtk.Application):
Close the application, stops all threads
and clear clipboard for safety reasons
"""
Clipboard.clear()
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.clear()
Window.get_default().close()
self.quit()
......@@ -16,8 +16,6 @@
You should have received a copy of the GNU General Public License
along with Authenticator. If not, see <http://www.gnu.org/licenses/>.
"""
from .clipboard import Clipboard
from .keyring import Keyring
from .logger import Logger
from .otp import OTP
......
......@@ -16,22 +16,35 @@
You should have received a copy of the GNU General Public License
along with Authenticator. If not, see <http://www.gnu.org/licenses/>.
"""
from hashlib import sha256
from gettext import gettext as _
from gi.repository import GObject
from Authenticator.models import Clipboard, Database, Keyring, Logger, OTP, Provider
from gi.repository import GObject, Gtk, Gdk
from hashlib import sha256
from typing import Union
from Authenticator.models import Database, Keyring, Logger, OTP, Provider
class Account(GObject.GObject):
__gsignals__ = {
'otp_out_of_date': (GObject.SignalFlags.RUN_LAST, None, ()),
'otp_updated': (GObject.SignalFlags.RUN_LAST, None, (str,)),
'removed': (GObject.SignalFlags.RUN_LAST, None, ()),
'otp_out_of_date': (
GObject.SignalFlags.RUN_LAST,
None,
()
),
'otp_updated': (
GObject.SignalFlags.RUN_LAST,
None,
(str, )
),
'removed': (
GObject.SignalFlags.RUN_LAST,
None,
()
),
}
_provider = None
_provider: Provider = None
def __init__(self, _id, username, token_id, provider):
def __init__(self, _id: str, username: str, token_id: str, provider: int):
GObject.GObject.__init__(self)
self.id = _id
self.username = username
......@@ -49,7 +62,7 @@ class Account(GObject.GObject):
"the keyring keys were reset manually")
@staticmethod
def create(username, token, provider):
def create(username: str, token: str, provider: int) -> 'Account':
"""
Create a new Account.
:param username: the account's username
......@@ -65,7 +78,7 @@ class Account(GObject.GObject):
return Account(obj.id, username, token_id, provider)
@staticmethod
def create_from_json(json_obj):
def create_from_json(json_obj: dict) -> 'Account':
tags = json_obj["tags"]
if not tags:
provider_name = _("Default")
......@@ -77,22 +90,22 @@ class Account(GObject.GObject):
return Account.create(json_obj["label"], json_obj["secret"], provider.provider_id)
@staticmethod
def get_by_id(id_):
def get_by_id(id_: int) -> 'Account':
obj = Database.get_default().account_by_id(id_)
return Account(obj.id, obj.username, obj.token_id, obj.provider)
@property
def provider(self):
def provider(self) -> 'Provider':
return self._provider
@provider.setter
def provider(self, provider):
def provider(self, provider: Union[int, 'Provider']) -> 'Provider':
if isinstance(provider, int):
self._provider = Provider.get_by_id(provider)
else:
self._provider = provider
def update(self, username, provider):
def update(self, username: str, provider: Provider):
"""
Update the account name and/or provider.
:param username: the account's username
......@@ -118,7 +131,9 @@ class Account(GObject.GObject):
def copy_pin(self):
"""Copy the OTP to the clipboard."""
Clipboard.set(self.otp.pin)
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.set_text(self.otp.pin, -1)
def _on_otp_out_of_date(self, *_):
if self._code_generated:
......@@ -139,4 +154,4 @@ class Account(GObject.GObject):
"last_used": 0,
"tags": [self.provider.name]
}
return None
return {}
......@@ -17,14 +17,24 @@
along with Authenticator. If not, see <http://www.gnu.org/licenses/>.
"""
from gi.repository import GObject, GLib
from typing import Iterable
from .account import Account
from .database import Database
from .provider import Provider
class AccountsManager(GObject.GObject):
__gsignals__ = {
'counter_updated': (GObject.SignalFlags.RUN_LAST, None, (int,)),
'counter_updated': (
GObject.SignalFlags.RUN_LAST,
None,
(int,)
),
}
instance = None
empty = GObject.Property(type=bool, default=False)
instance: 'AccountsManager' = None
empty: GObject.Property = GObject.Property(type=bool, default=False)
# A list that contains a tuple (provider, accounts)
__accounts = []
......@@ -34,18 +44,18 @@ class AccountsManager(GObject.GObject):
self._accounts_per_provider = []
self._alive = True
self.__fill_accounts()
self._timeout_id = 0
self.counter_max = 30
self.counter = self.counter_max
GLib.timeout_add_seconds(1, self.__update_counter, None)
self._start_progress_countdown()
@staticmethod
def get_default():
def get_default() -> 'AccountsManager':
if AccountsManager.instance is None:
AccountsManager.instance = AccountsManager()
return AccountsManager.instance
def add(self, provider, account):
def add(self, provider: 'Provider', account: 'Account'):
added = False
for _provider, accounts in self._accounts_per_provider:
if provider == _provider:
......@@ -54,9 +64,11 @@ class AccountsManager(GObject.GObject):
break
if not added:
self._accounts_per_provider.append((provider, [account]))
self.props.empty = len(self._accounts_per_provider) == 0
self.props.empty = False
if self._timeout_id == 0:
self._start_progress_countdown()
def delete(self, account):
def delete(self, account: 'Account'):
provider_index = 0
_accounts = None
for provider, accounts in self._accounts_per_provider:
......@@ -69,11 +81,10 @@ class AccountsManager(GObject.GObject):
if not len(_accounts):
del self._accounts_per_provider[provider_index]
self.props.empty = len(self._accounts_per_provider) == 0
if self.props.empty:
self._stop_progress_countdown()
def search(self, terms):
from .database import Database
from .account import Account
def search(self, terms: Iterable[str]):
accounts = Database.get_default().search_accounts(terms)
_accounts = []
for account in accounts:
......@@ -100,19 +111,14 @@ class AccountsManager(GObject.GObject):
count += len(accounts)
return count
def clear(self):
self._accounts_per_provider = []
def kill(self):
self._alive = False
self._stop_progress_countdown()
def update_childes(self, signal, data=None):
def update_childes(self, signal: str):
for _, accounts in self._accounts_per_provider:
for account in accounts:
if data:
account.emit(signal, data)
else:
account.emit(signal)
account.emit(signal)
def __update_counter(self, *args):
if self._alive:
......@@ -125,10 +131,6 @@ class AccountsManager(GObject.GObject):
return False
def __fill_accounts(self):
from .database import Database
from .account import Account
from .provider import Provider
providers = Database.get_default().get_providers(only_used=True)
for provider in providers:
accounts = Database.get_default().accounts_by_provider(provider.id)
......@@ -140,3 +142,14 @@ class AccountsManager(GObject.GObject):
_accounts.append(account)
self._accounts_per_provider.append((provider, _accounts))
self.props.empty = len(self._accounts_per_provider) == 0
def _start_progress_countdown(self):
self._stop_progress_countdown()
self._timeout_id = GLib.timeout_add_seconds(1, self.__update_counter,
None)
def _stop_progress_countdown(self):
if self._timeout_id > 0:
GLib.Source.remove(self._timeout_id)
self._timeout_id = 0
self.counter = self.counter_max
......@@ -20,6 +20,7 @@ import json
from gi.repository import Gio
from Authenticator.models import Account, AccountsManager, Logger
from Authenticator.widgets import AccountsWidget
class Backup:
......@@ -28,8 +29,7 @@ class Backup:
pass
@staticmethod
def import_accounts(accounts):
from Authenticator.widgets import AccountsWidget
def import_accounts(accounts: [dict]):
accounts_widget = AccountsWidget.get_default()
accounts_manager = AccountsManager.get_default()
for account in accounts:
......@@ -42,13 +42,12 @@ class Backup:
Logger.error(str(e))
@staticmethod
def export_accounts():
def export_accounts() -> [dict]:
accounts = AccountsManager.get_default().accounts
exported_accounts = []
for account in accounts:
json_account = account.to_json()
if json_account:
exported_accounts.append(json_account)
exported_accounts.append(json_account)
return exported_accounts
......@@ -58,7 +57,7 @@ class BackupJSON:
pass
@staticmethod
def export_file(uri):
def export_file(uri: str):
accounts = Backup.export_accounts()
gfile = Gio.File.new_for_uri(uri)
stream = gfile.replace(None,
......@@ -70,7 +69,7 @@ class BackupJSON:
stream.close()
@staticmethod
def import_file(uri):
def import_file(uri: str):
gfile = Gio.File.new_for_uri(uri)
accounts = gfile.load_contents()[1].decode("utf-8")
Backup.import_accounts(json.loads(accounts))
"""
Copyright © 2017 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
This file is part of Authenticator.
Authenticator is free software: you can redistribute it and/or
modify it under the terms of the GNU General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Authenticator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Authenticator. If not, see <http://www.gnu.org/licenses/>.
"""
from gi.repository import Gdk, Gtk
class Clipboard:
"""Clipboard handler."""
def __init__(self):
pass
@staticmethod
def clear():
"""Clear the clipboard."""
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.clear()
@staticmethod
def set(string):
"""
Copy a string to the clipboard.
:param string: the string to copy.
:type string: str
"""
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.set_text(string, -1)
......@@ -21,6 +21,8 @@ from os import path, makedirs
from gi.repository import GLib
from collections import namedtuple
from shutil import move
from typing import Iterable
from Authenticator.models import Logger
......@@ -34,7 +36,7 @@ class Database:
# Default instance
instance = None
# Database version number
db_version = 7
db_version: int = 7
def __init__(self):
self.migrations_dir = path.join(path.dirname(__file__), '../migrations')
......@@ -51,17 +53,17 @@ class Database:
return Database.instance
@property
def db_dir(self):
def db_dir(self) -> str:
return path.join(GLib.get_user_config_dir(),
'Authenticator')
@property
def db_file(self):
def db_file(self) -> str:
return path.join(self.db_dir,
'database-{}.db'.format(str(Database.db_version))
)
def insert_account(self, username, token_id, provider):
def insert_account(self, username: str, token_id: str, provider: str):
"""
Insert a new account to the database
:param username: Account name
......@@ -78,7 +80,8 @@ class Database:
Logger.error("[SQL] Couldn't add a new account")
Logger.error(str(error))
def insert_provider(self, name, website, doc_url=None, image=None):
def insert_provider(self, name: str, website: str,
doc_url: str = None, image: str = None):
"""
Insert a new provider to the database
:param name: The provider name
......@@ -96,7 +99,7 @@ class Database:
Logger.error("[SQL] Couldn't add a new account")
Logger.error(str(error))
def account_by_id(self, id_):
def account_by_id(self, id_: int) -> Account:
"""
Get an account by the ID
:param id_: int the account id
......@@ -111,13 +114,13 @@ class Database:
Logger.error(str(error))
return None
def accounts_by_provider(self, provider_id):
def accounts_by_provider(self, provider_id: int) -> Iterable[Account]:
query = "SElECT * FROM accounts WHERE provider=?"
query_d = self.conn.execute(query, (provider_id, ))
accounts = query_d.fetchall()
return [Account(*account) for account in accounts]
def provider_by_id(self, id_):
def provider_by_id(self, id_: int) -> Provider:
"""
Get a provider by the ID
:param id_: int the provider id
......@@ -132,7 +135,7 @@ class Database:
Logger.error(str(error))
return None
def provider_by_name(self, provider_name):
def provider_by_name(self, provider_name: str) -> Provider:
"""
Get a provider by the ID
:param id_: int the provider id
......@@ -148,7 +151,7 @@ class Database:
Logger.error(str(error))
return None
def delete_account(self, id_):
def delete_account(self, id_: int):
"""
Remove an account by ID.
......@@ -157,7 +160,7 @@ class Database:
"""
self.__delete("accounts", id_)
def delete_provider(self, id_):
def delete_provider(self, id_: int):
"""
Remove a provider by ID.
......@@ -166,17 +169,17 @@ class Database:
"""
self.__delete("providers", id_)
def update_account(self, account_data, id_):
def update_account(self, account_data: dict, id_: int):
"""
Update an account by id
"""
self.__update_by_id("accounts", account_data, id_)
def update_provider(self, provider_data, id_):
def update_provider(self, provider_data: dict, id_: int):
# Update a provider by id
self.__update_by_id("providers", provider_data, id_)
def search_accounts(self, terms):
def search_accounts(self, terms: Iterable[str]) -> Iterable[Account]:
if terms:
filters = " ".join(terms)
if filters:
......@@ -218,7 +221,7 @@ class Database:
"""
return self.__count("providers")
def get_providers(self, **kwargs):
def get_providers(self, **kwargs) -> Iterable[Provider]:
only_used = kwargs.get("only_used",)
query = "SELECT * FROM providers"
if only_used:
......@@ -233,7 +236,7 @@ class Database:
return None
@property
def accounts(self):
def accounts(self) -> [Account]:
"""
Retrieve the list of accounts.
......@@ -279,7 +282,7 @@ class Database:
with backend.lock():
backend.apply_migrations(backend.to_apply(migrations))
def __count(self, table_name):
def __count(self, table_name: str) -> int:
query = "SELECT COUNT(id) AS count FROM " + table_name
try:
data = self.conn.cursor().execute(query)
......@@ -289,7 +292,7 @@ class Database:
Logger.error(str(error))
return None
def __delete(self, table_name, id_):
def __delete(self, table_name: str, id_: int):
"""
Remove a row by ID.
......@@ -304,7 +307,7 @@ class Database:
Logger.error("[SQL] Couldn't remove the row '{}'".format(id_))
Logger.error(str(error))
def __update_by_id(self, table_name, data, id_):
def __update_by_id(self, table_name: str, data: dict, id_: int):
query = "UPDATE {} SET ".format(table_name)
resources = []
i = 0
......
......@@ -20,12 +20,12 @@ from gi.repository import GObject, Secret
class Keyring(GObject.GObject):
ID = "com.github.bilelmoussaoui.Authenticator"
PasswordID = "com.github.bilelmoussaoui.Authenticator.Login"
PasswordState = "com.github.bilelmoussaoui.Authenticator.State"
instance = None
ID: str = "com.github.bilelmoussaoui.Authenticator"