test1
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
// dbRepository.js
|
||||
// Couche d’accès aux données pour l’extension.
|
||||
// Utilise le helper Python password_db.py pour manipuler SQLite.
|
||||
|
||||
import GLib from 'gi://GLib';
|
||||
import Gio from 'gi://Gio';
|
||||
import * as ExtensionUtils from 'resource:///org/gnome/shell/misc/extensionUtils.js';
|
||||
|
||||
const Me = ExtensionUtils.getCurrentExtension();
|
||||
|
||||
const APP_DATA_DIR = GLib.build_filenamev([
|
||||
GLib.get_home_dir(),
|
||||
'.local', 'share', 'gnome-shell', 'password-applet'
|
||||
]);
|
||||
|
||||
export class PasswordRepository {
|
||||
constructor() {
|
||||
this._helperPath = Me.dir.get_child('password_db.py').get_path();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!GLib.file_test(APP_DATA_DIR, GLib.FileTest.IS_DIR)) {
|
||||
GLib.mkdir_with_parents(APP_DATA_DIR, 0o700);
|
||||
}
|
||||
this._runHelperSync(['init']);
|
||||
}
|
||||
|
||||
listEntries() {
|
||||
const stdout = this._runHelperSync(['list']);
|
||||
if (!stdout)
|
||||
return [];
|
||||
|
||||
try {
|
||||
return JSON.parse(stdout);
|
||||
} catch (e) {
|
||||
logError(e, 'Erreur JSON listEntries');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
addEntry(entry) {
|
||||
const payload = JSON.stringify(entry);
|
||||
this._runHelperSync(['add'], payload);
|
||||
}
|
||||
|
||||
_runHelperSync(args, stdinText = '') {
|
||||
const argv = ['python3', this._helperPath, ...args];
|
||||
|
||||
const proc = new Gio.Subprocess({
|
||||
argv,
|
||||
flags: Gio.SubprocessFlags.STDOUT_PIPE |
|
||||
Gio.SubprocessFlags.STDERR_PIPE,
|
||||
});
|
||||
|
||||
try {
|
||||
proc.init(null);
|
||||
} catch (e) {
|
||||
logError(e, 'Impossible de lancer password_db.py');
|
||||
return '';
|
||||
}
|
||||
|
||||
const [ok, stdout, stderr] = proc.communicate_utf8(stdinText, null);
|
||||
|
||||
if (!ok || !proc.get_successful()) {
|
||||
log(`password_db.py error: ${stderr}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
return stdout.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// extension.js
|
||||
// Point d’entrée de l’extension GNOME Shell Password Applet.
|
||||
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as ExtensionUtils from 'resource:///org/gnome/shell/misc/extensionUtils.js';
|
||||
|
||||
import { PasswordRepository } from './dbRepository.js';
|
||||
import { PasswordApplet } from './uiComponents.js';
|
||||
|
||||
let applet;
|
||||
let repository;
|
||||
|
||||
export function init() {
|
||||
// Initialisation minimale
|
||||
}
|
||||
|
||||
export function enable() {
|
||||
repository = new PasswordRepository();
|
||||
repository.init();
|
||||
|
||||
applet = new PasswordApplet(repository);
|
||||
|
||||
Main.panel.addToStatusArea('password-applet', applet, 1, 'right');
|
||||
}
|
||||
|
||||
export function disable() {
|
||||
if (applet) {
|
||||
applet.destroy();
|
||||
applet = null;
|
||||
}
|
||||
repository = null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"uuid": "password-applet@gilles",
|
||||
"name": "Password Applet",
|
||||
"description": "Mini gestionnaire de mots de passe dans la barre supérieure.",
|
||||
"shell-version": ["45", "46"],
|
||||
"version": 1,
|
||||
"settings-schema": "org.gnome.shell.extensions.password-applet",
|
||||
"stylesheet": "stylesheet.css"
|
||||
}
|
||||
|
||||
122
password-applet@gilles/password_db.py
Normal file → Executable file
122
password-applet@gilles/password_db.py
Normal file → Executable file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Helper SQLite pour l’extension Password Applet.
|
||||
|
||||
Commandes :
|
||||
- init : initialise la base
|
||||
- list : renvoie les entrées
|
||||
- add : ajoute une entrée
|
||||
|
||||
Note :
|
||||
Le champ password_enc stocke le mot de passe en clair (MVP).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
APP_DATA_DIR = os.path.join(
|
||||
os.path.expanduser('~'),
|
||||
'.local', 'share', 'gnome-shell', 'password-applet'
|
||||
)
|
||||
DB_PATH = os.path.join(APP_DATA_DIR, 'passwords.db')
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
label TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
password_enc TEXT NOT NULL,
|
||||
url TEXT,
|
||||
notes TEXT,
|
||||
is_favorite INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
|
||||
def ensure_db():
|
||||
os.makedirs(APP_DATA_DIR, exist_ok=True)
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
conn.executescript(SCHEMA)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def encrypt_password(plain: str) -> str:
|
||||
return plain # Pour le MVP, rien
|
||||
|
||||
def cmd_init():
|
||||
ensure_db()
|
||||
|
||||
def cmd_list():
|
||||
ensure_db()
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"SELECT id, label, username, password_enc, url, notes, "
|
||||
"is_favorite, created_at, updated_at "
|
||||
"FROM entries ORDER BY created_at DESC"
|
||||
)
|
||||
rows = [dict(row) for row in cur.fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
print(json.dumps(rows))
|
||||
|
||||
def cmd_add():
|
||||
ensure_db()
|
||||
raw = sys.stdin.read()
|
||||
data = json.loads(raw or '{}')
|
||||
|
||||
label = (data.get('label') or '').strip()
|
||||
username = (data.get('username') or '').strip()
|
||||
password = (data.get('password') or '').strip()
|
||||
url = (data.get('url') or '').strip() or None
|
||||
notes = (data.get('notes') or '').strip() or None
|
||||
|
||||
if not label or not username or not password:
|
||||
print('Missing required fields', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
password_enc = encrypt_password(password)
|
||||
now = datetime.utcnow().isoformat(timespec='seconds') + 'Z'
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO entries (label, username, password_enc, url, notes, "
|
||||
"is_favorite, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 0, ?, ?)",
|
||||
(label, username, password_enc, url, notes, now, now)
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2:
|
||||
print("Usage: password_db.py [init|list|add]")
|
||||
return 1
|
||||
|
||||
cmd = argv[1]
|
||||
|
||||
if cmd == "init":
|
||||
cmd_init()
|
||||
elif cmd == "list":
|
||||
cmd_list()
|
||||
elif cmd == "add":
|
||||
cmd_add()
|
||||
else:
|
||||
print(f"Unknown command: {cmd}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv))
|
||||
|
||||
70
password-applet@gilles/stylesheet.css
Normal file
70
password-applet@gilles/stylesheet.css
Normal file
@@ -0,0 +1,70 @@
|
||||
/* stylesheet.css */
|
||||
/* Style du mini gestionnaire de mots de passe GNOME Shell */
|
||||
|
||||
/* Lien cliquable (URL présente) */
|
||||
.password-applet-label-link {
|
||||
color: #5da5ff;
|
||||
}
|
||||
|
||||
/* Nom d’utilisateur */
|
||||
.password-applet-username {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Mot de passe masqué */
|
||||
.password-applet-password {
|
||||
margin-left: 8px;
|
||||
font-family: monospace;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Boutons copier (User / Password) */
|
||||
.password-applet-copy-btn {
|
||||
margin-left: 6px;
|
||||
padding: 0 6px;
|
||||
border-radius: 6px;
|
||||
background-color: #3a3a3a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.password-applet-copy-btn:hover {
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
|
||||
/* Champs de formulaire */
|
||||
.password-applet-entry {
|
||||
min-width: 220px;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
/* Bouton Enregistrer */
|
||||
.password-applet-save-btn {
|
||||
padding: 4px 12px;
|
||||
margin-right: 6px;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.password-applet-save-btn:hover {
|
||||
background-color: #45a047;
|
||||
}
|
||||
|
||||
/* Bouton Annuler */
|
||||
.password-applet-cancel-btn {
|
||||
padding: 4px 12px;
|
||||
background-color: #d9534f;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.password-applet-cancel-btn:hover {
|
||||
background-color: #c9302c;
|
||||
}
|
||||
|
||||
/* Ligne + Formulaire */
|
||||
.password-applet-form-row {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// uiComponents.js
|
||||
// UI du mini gestionnaire de mots de passe.
|
||||
|
||||
import St from 'gi://St';
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
||||
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||||
import * as ExtensionUtils from 'resource:///org/gnome/shell/misc/extensionUtils.js';
|
||||
|
||||
const Clutter = imports.gi.Clutter;
|
||||
const Clipboard = St.Clipboard.get_default();
|
||||
const ClipboardType = St.ClipboardType.CLIPBOARD;
|
||||
|
||||
function copyToClipboard(text) {
|
||||
if (text)
|
||||
Clipboard.set_text(ClipboardType, text);
|
||||
}
|
||||
|
||||
export class PasswordEntryRow extends PopupMenu.PopupBaseMenuItem {
|
||||
constructor(entry) {
|
||||
super({ reactive: true });
|
||||
|
||||
const box = new St.BoxLayout({ vertical: false, x_expand: true });
|
||||
|
||||
let labelWidget;
|
||||
if (entry.url) {
|
||||
labelWidget = new St.Label({
|
||||
text: entry.label,
|
||||
style_class: 'password-applet-label-link',
|
||||
x_expand: true
|
||||
});
|
||||
labelWidget.clutter_text.underline = true;
|
||||
labelWidget.connect('button-press-event', () => {
|
||||
Gio.AppInfo.launch_default_for_uri(entry.url, null);
|
||||
});
|
||||
} else {
|
||||
labelWidget = new St.Label({ text: entry.label, x_expand: true });
|
||||
}
|
||||
|
||||
box.add_child(labelWidget);
|
||||
|
||||
box.add_child(new St.Label({ text: entry.username, x_expand: true }));
|
||||
box.add_child(new St.Label({ text: '••••••••' }));
|
||||
|
||||
const uBtn = new St.Button({ label: 'U' });
|
||||
uBtn.connect('clicked', () => copyToClipboard(entry.username));
|
||||
box.add_child(uBtn);
|
||||
|
||||
const pBtn = new St.Button({ label: 'P' });
|
||||
pBtn.connect('clicked', () => copyToClipboard(entry.password_enc));
|
||||
box.add_child(pBtn);
|
||||
|
||||
this.actor.add_child(box);
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordListSection extends PopupMenu.PopupMenuSection {
|
||||
setEntries(entries) {
|
||||
this.removeAll();
|
||||
|
||||
if (!entries.length) {
|
||||
this.addMenuItem(new PopupMenu.PopupMenuItem(
|
||||
'Aucune entrée', { reactive: false }
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const e of entries)
|
||||
this.addMenuItem(new PasswordEntryRow(e));
|
||||
}
|
||||
}
|
||||
|
||||
export class AddEntrySection extends PopupMenu.PopupMenuSection {
|
||||
constructor(repo, refreshCb) {
|
||||
super();
|
||||
this._repo = repo;
|
||||
this._refreshCb = refreshCb;
|
||||
this._showAddButton();
|
||||
}
|
||||
|
||||
_showAddButton() {
|
||||
this.removeAll();
|
||||
const item = new PopupMenu.PopupMenuItem('➕ Ajouter une entrée');
|
||||
item.connect('activate', () => this._showForm());
|
||||
this.addMenuItem(item);
|
||||
}
|
||||
|
||||
_showForm() {
|
||||
this.removeAll();
|
||||
const wrapper = new PopupMenu.PopupBaseMenuItem({ reactive: false });
|
||||
const box = new St.BoxLayout({ vertical: true });
|
||||
|
||||
const mkEntry = hint =>
|
||||
new St.Entry({ hint_text: hint, style_class: 'password-applet-entry' });
|
||||
|
||||
const label = mkEntry("Label");
|
||||
const user = mkEntry("User");
|
||||
const pwd = mkEntry("Password");
|
||||
const url = mkEntry("URL");
|
||||
const notes = mkEntry("Notes");
|
||||
|
||||
const mkRow = (name, widget) => {
|
||||
const row = new St.BoxLayout();
|
||||
row.add_child(new St.Label({ text: name + " : " }));
|
||||
row.add_child(widget);
|
||||
return row;
|
||||
};
|
||||
|
||||
box.add_child(mkRow("Label", label));
|
||||
box.add_child(mkRow("User", user));
|
||||
box.add_child(mkRow("Password", pwd));
|
||||
box.add_child(mkRow("URL", url));
|
||||
box.add_child(mkRow("Notes", notes));
|
||||
|
||||
const btnRow = new St.BoxLayout();
|
||||
|
||||
const save = new St.Button({ label: "Enregistrer" });
|
||||
save.connect('clicked', () => {
|
||||
const entry = {
|
||||
label: label.get_text().trim(),
|
||||
username: user.get_text().trim(),
|
||||
password: pwd.get_text().trim(),
|
||||
url: url.get_text().trim(),
|
||||
notes: notes.get_text().trim()
|
||||
};
|
||||
|
||||
if (!entry.label || !entry.username || !entry.password) {
|
||||
Main.notify("Password Applet", "Champs obligatoires manquants");
|
||||
return;
|
||||
}
|
||||
|
||||
this._repo.addEntry(entry);
|
||||
this._refreshCb();
|
||||
this._showAddButton();
|
||||
});
|
||||
|
||||
const cancel = new St.Button({ label: "Annuler" });
|
||||
cancel.connect('clicked', () => this._showAddButton());
|
||||
|
||||
btnRow.add_child(save);
|
||||
btnRow.add_child(cancel);
|
||||
|
||||
box.add_child(btnRow);
|
||||
wrapper.actor.add_child(box);
|
||||
|
||||
this.addMenuItem(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordApplet extends PanelMenu.Button {
|
||||
constructor(repo) {
|
||||
super(0.0, 'PasswordApplet', false);
|
||||
|
||||
this._repo = repo;
|
||||
|
||||
this.add_child(new St.Icon({
|
||||
icon_name: 'dialog-password-symbolic',
|
||||
style_class: 'system-status-icon'
|
||||
}));
|
||||
|
||||
this._list = new PasswordListSection();
|
||||
this.menu.addMenuItem(this._list);
|
||||
|
||||
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
||||
|
||||
this._add = new AddEntrySection(repo, () => this._reload());
|
||||
this.menu.addMenuItem(this._add);
|
||||
|
||||
this._reload();
|
||||
}
|
||||
|
||||
_reload() {
|
||||
const entries = this._repo.listEntries();
|
||||
this._list.setEntries(entries);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user