Implement loading meilisearch config from disk
Encrypted such as it is
This commit is contained in:
@@ -15,6 +15,10 @@ Journaler helps you build a consistent journaling habit by periodically remindin
|
||||
|
||||

|
||||
|
||||
## Note
|
||||
|
||||
**Versions up to 3 use a local sqlite database while versions 4 and above use meillisearch!**
|
||||
|
||||
## Features
|
||||
|
||||
- **Automated Reminders**: Customizable popup intervals to remind you to journal
|
||||
|
@@ -3,22 +3,25 @@ import 'dart:core';
|
||||
import 'dart:math';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:journaler/notes.dart';
|
||||
import 'package:journaler/meilisearch_config.dart';
|
||||
|
||||
const endpoint = 'https://meili.site.quack-lab.dev/';
|
||||
const noteIndex = 'notes';
|
||||
const scratchIndex = 'scratch';
|
||||
const settingsIndex = 'settings';
|
||||
const apiKey = String.fromEnvironment(
|
||||
'MEILISEARCH_API_KEY',
|
||||
defaultValue:
|
||||
'',
|
||||
);
|
||||
const header = {
|
||||
'Authorization': 'Bearer $apiKey',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final alphanum = RegExp(r'[a-zA-Z0-9]', caseSensitive: false);
|
||||
|
||||
Future<Map<String, String>> _getHeaders() async {
|
||||
final apiKey = await getMeilisearchApiKey();
|
||||
return {
|
||||
'Authorization': 'Bearer $apiKey',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
Future<String> _getEndpoint() async {
|
||||
return await getMeilisearchEndpoint();
|
||||
}
|
||||
|
||||
class MeilisearchQuery {
|
||||
String q;
|
||||
String? filter;
|
||||
@@ -93,65 +96,72 @@ class MeilisearchResponse {
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
|
||||
if (!await indexExists(noteIndex)) {
|
||||
await http.post(
|
||||
Uri.parse('$endpoint/indexes'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode({'uid': noteIndex, 'primaryKey': 'id'}),
|
||||
);
|
||||
}
|
||||
await http.put(
|
||||
Uri.parse('$endpoint/indexes/$noteIndex/settings/sortable-attributes'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(['date']),
|
||||
);
|
||||
await http.put(
|
||||
Uri.parse('$endpoint/indexes/$noteIndex/settings/filterable-attributes'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(['date', 'topLetter', 'topLetterFrequency']),
|
||||
);
|
||||
|
||||
if (!await indexExists(scratchIndex)) {
|
||||
await http.post(
|
||||
Uri.parse('$endpoint/indexes'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode({'uid': scratchIndex, 'primaryKey': 'id'}),
|
||||
);
|
||||
}
|
||||
await http.put(
|
||||
Uri.parse('$endpoint/indexes/$scratchIndex/settings/sortable-attributes'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(['date']),
|
||||
);
|
||||
await http.put(
|
||||
Uri.parse('$endpoint/indexes/$scratchIndex/settings/filterable-attributes'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(['date']),
|
||||
);
|
||||
|
||||
if (!await indexExists(settingsIndex)) {
|
||||
await http.post(
|
||||
Uri.parse('$endpoint/indexes'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode({'uid': settingsIndex, 'primaryKey': 'key'}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> indexExists(String index) async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final response = await http.get(
|
||||
Uri.parse('$endpoint/indexes/$index'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
);
|
||||
return response.statusCode == 200;
|
||||
}
|
||||
|
||||
// Settings Management
|
||||
Future<String?> getSetting(String key) async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final searchCondition = MeilisearchQuery(q: '', filter: 'key = $key');
|
||||
final response = await http.post(
|
||||
Uri.parse('$endpoint/indexes/$settingsIndex/search'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(searchCondition.toJson()),
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
@@ -165,10 +175,12 @@ Future<String?> getSetting(String key) async {
|
||||
}
|
||||
|
||||
Future<void> setSetting(String key, String value) async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final document = {'key': key, 'value': value};
|
||||
final response = await http.post(
|
||||
Uri.parse('$endpoint/indexes/$settingsIndex/documents'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(document),
|
||||
);
|
||||
if (response.statusCode != 202) {
|
||||
@@ -179,6 +191,8 @@ Future<void> setSetting(String key, String value) async {
|
||||
// Maybe we could factor a lot of this out into a separate function
|
||||
// But we don't care for now...
|
||||
Future<List<Note>> searchNotes(String query) async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final searchCondition = MeilisearchQuery(
|
||||
q: query,
|
||||
limit: 10,
|
||||
@@ -189,7 +203,7 @@ Future<List<Note>> searchNotes(String query) async {
|
||||
);
|
||||
final response = await http.post(
|
||||
Uri.parse('$endpoint/indexes/$noteIndex/search'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(searchCondition.toJson()),
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
@@ -209,6 +223,8 @@ Future<List<Note>> searchNotes(String query) async {
|
||||
}
|
||||
|
||||
Future<Note?> getPreviousTo(int epochTime) async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final searchCondition = MeilisearchQuery(
|
||||
q: '',
|
||||
filter: 'date < $epochTime',
|
||||
@@ -217,7 +233,7 @@ Future<Note?> getPreviousTo(int epochTime) async {
|
||||
);
|
||||
final response = await http.post(
|
||||
Uri.parse('$endpoint/indexes/$noteIndex/search'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(searchCondition.toJson()),
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
@@ -237,6 +253,8 @@ Future<Note?> getPreviousTo(int epochTime) async {
|
||||
}
|
||||
|
||||
Future<Note?> getNextTo(int epochTime) async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final searchCondition = MeilisearchQuery(
|
||||
q: '',
|
||||
filter: 'date > $epochTime',
|
||||
@@ -245,7 +263,7 @@ Future<Note?> getNextTo(int epochTime) async {
|
||||
);
|
||||
final response = await http.post(
|
||||
Uri.parse('$endpoint/indexes/$noteIndex/search'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(searchCondition.toJson()),
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
@@ -265,6 +283,8 @@ Future<Note?> getNextTo(int epochTime) async {
|
||||
}
|
||||
|
||||
Future<Note?> getLatest() async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final searchCondition = MeilisearchQuery(
|
||||
q: '',
|
||||
sort: ['date:desc'],
|
||||
@@ -272,7 +292,7 @@ Future<Note?> getLatest() async {
|
||||
);
|
||||
final response = await http.post(
|
||||
Uri.parse('$endpoint/indexes/$noteIndex/search'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(searchCondition.toJson()),
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
@@ -303,6 +323,8 @@ String generateRandomString(int length) {
|
||||
}
|
||||
|
||||
Future<Note> createNote(String content) async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final lines = content.split('\n');
|
||||
final trimmedLines = <String>[];
|
||||
for (final line in lines) {
|
||||
@@ -334,7 +356,7 @@ Future<Note> createNote(String content) async {
|
||||
};
|
||||
final response = await http.post(
|
||||
Uri.parse('$endpoint/indexes/$noteIndex/documents'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(document),
|
||||
);
|
||||
if (response.statusCode != 202) {
|
||||
@@ -348,13 +370,15 @@ Future<Note> createNote(String content) async {
|
||||
}
|
||||
|
||||
Future<List<Note>> getProblematic({double threshold = 0.7}) async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final searchCondition = MeilisearchQuery(
|
||||
q: '',
|
||||
filter: 'topLetterFrequency > $threshold',
|
||||
);
|
||||
final response = await http.post(
|
||||
Uri.parse('$endpoint/indexes/$noteIndex/search'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(searchCondition.toJson()),
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
@@ -380,6 +404,8 @@ Future<List<Note>> getProblematic({double threshold = 0.7}) async {
|
||||
// TODO: only update if changed
|
||||
// How? idk
|
||||
Future<void> updateNote(Note note) async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final lines = note.content.split('\n');
|
||||
final trimmedLines = <String>[];
|
||||
for (final line in lines) {
|
||||
@@ -413,7 +439,7 @@ Future<void> updateNote(Note note) async {
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('$endpoint/indexes/$noteIndex/documents'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(document),
|
||||
);
|
||||
if (response.statusCode != 202) {
|
||||
@@ -424,9 +450,11 @@ Future<void> updateNote(Note note) async {
|
||||
}
|
||||
|
||||
Future<void> deleteNote(String id) async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final response = await http.delete(
|
||||
Uri.parse('$endpoint/indexes/$noteIndex/documents/$id'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
);
|
||||
if (response.statusCode != 202) {
|
||||
throw Exception(
|
||||
@@ -436,6 +464,8 @@ Future<void> deleteNote(String id) async {
|
||||
}
|
||||
|
||||
Future<Scratch?> getLatestScratch() async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final searchCondition = MeilisearchQuery(
|
||||
q: '',
|
||||
sort: ['date:desc'],
|
||||
@@ -443,7 +473,7 @@ Future<Scratch?> getLatestScratch() async {
|
||||
);
|
||||
final response = await http.post(
|
||||
Uri.parse('$endpoint/indexes/$scratchIndex/search'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(searchCondition.toJson()),
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
@@ -463,6 +493,8 @@ Future<Scratch?> getLatestScratch() async {
|
||||
}
|
||||
|
||||
Future<Scratch> createScratch(String content) async {
|
||||
final endpoint = await _getEndpoint();
|
||||
final headers = await _getHeaders();
|
||||
final document = {
|
||||
'id': generateRandomString(32),
|
||||
'date': DateTime.now().toUtc().millisecondsSinceEpoch,
|
||||
@@ -470,7 +502,7 @@ Future<Scratch> createScratch(String content) async {
|
||||
};
|
||||
final response = await http.post(
|
||||
Uri.parse('$endpoint/indexes/$scratchIndex/documents'),
|
||||
headers: header,
|
||||
headers: headers,
|
||||
body: jsonEncode(document),
|
||||
);
|
||||
if (response.statusCode != 202) {
|
||||
|
133
lib/meilisearch_config.dart
Normal file
133
lib/meilisearch_config.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
const defaultEndpoint = 'http://localhost:7700';
|
||||
const defaultApiKey = 'masterKey';
|
||||
|
||||
// Cache for configuration
|
||||
String? _cachedEndpoint;
|
||||
String? _cachedApiKey;
|
||||
bool _isInitialized = false;
|
||||
|
||||
// Get the config file path in the user's home directory
|
||||
Future<String> _getConfigPath() async {
|
||||
final home =
|
||||
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
|
||||
if (home == null) {
|
||||
throw Exception('Could not find home directory');
|
||||
}
|
||||
final configDir = Directory(path.join(home, '.journaler'));
|
||||
if (!await configDir.exists()) {
|
||||
await configDir.create(recursive: true);
|
||||
}
|
||||
return path.join(configDir.path, 'meilisearch_config.enc');
|
||||
}
|
||||
|
||||
// Simple encryption key derived from machine-specific data
|
||||
String _getEncryptionKey() {
|
||||
final machineId =
|
||||
Platform.operatingSystem +
|
||||
Platform.operatingSystemVersion +
|
||||
Platform.localHostname;
|
||||
return sha256.convert(utf8.encode(machineId)).toString();
|
||||
}
|
||||
|
||||
// Encrypt data
|
||||
String _encrypt(String data) {
|
||||
final key = _getEncryptionKey();
|
||||
final bytes = utf8.encode(data);
|
||||
final encrypted = <int>[];
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
encrypted.add(bytes[i] ^ key.codeUnitAt(i % key.length));
|
||||
}
|
||||
return base64.encode(encrypted);
|
||||
}
|
||||
|
||||
// Decrypt data
|
||||
String _decrypt(String encrypted) {
|
||||
final key = _getEncryptionKey();
|
||||
final bytes = base64.decode(encrypted);
|
||||
final decrypted = <int>[];
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
decrypted.add(bytes[i] ^ key.codeUnitAt(i % key.length));
|
||||
}
|
||||
return utf8.decode(decrypted);
|
||||
}
|
||||
|
||||
// Initialize cache from file
|
||||
Future<void> _initializeCache() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
try {
|
||||
final configPath = await _getConfigPath();
|
||||
final file = File(configPath);
|
||||
if (!await file.exists()) {
|
||||
_cachedEndpoint = defaultEndpoint;
|
||||
_cachedApiKey = defaultApiKey;
|
||||
_isInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
final encrypted = await file.readAsString();
|
||||
final decrypted = _decrypt(encrypted);
|
||||
final config = jsonDecode(decrypted);
|
||||
|
||||
_cachedEndpoint = config['endpoint'] ?? defaultEndpoint;
|
||||
_cachedApiKey = config['apiKey'] ?? defaultApiKey;
|
||||
} catch (e) {
|
||||
_cachedEndpoint = defaultEndpoint;
|
||||
_cachedApiKey = defaultApiKey;
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
Future<String> getMeilisearchEndpoint() async {
|
||||
await _initializeCache();
|
||||
return _cachedEndpoint!;
|
||||
}
|
||||
|
||||
Future<String> getMeilisearchApiKey() async {
|
||||
await _initializeCache();
|
||||
return _cachedApiKey!;
|
||||
}
|
||||
|
||||
Future<void> setMeilisearchEndpoint(String endpoint) async {
|
||||
final configPath = await _getConfigPath();
|
||||
final file = File(configPath);
|
||||
|
||||
Map<String, dynamic> config = {};
|
||||
if (await file.exists()) {
|
||||
final encrypted = await file.readAsString();
|
||||
final decrypted = _decrypt(encrypted);
|
||||
config = jsonDecode(decrypted);
|
||||
}
|
||||
|
||||
config['endpoint'] = endpoint;
|
||||
final encrypted = _encrypt(jsonEncode(config));
|
||||
await file.writeAsString(encrypted);
|
||||
|
||||
// Update cache
|
||||
_cachedEndpoint = endpoint;
|
||||
}
|
||||
|
||||
Future<void> setMeilisearchApiKey(String apiKey) async {
|
||||
final configPath = await _getConfigPath();
|
||||
final file = File(configPath);
|
||||
|
||||
Map<String, dynamic> config = {};
|
||||
if (await file.exists()) {
|
||||
final encrypted = await file.readAsString();
|
||||
final decrypted = _decrypt(encrypted);
|
||||
config = jsonDecode(decrypted);
|
||||
}
|
||||
|
||||
config['apiKey'] = apiKey;
|
||||
final encrypted = _encrypt(jsonEncode(config));
|
||||
await file.writeAsString(encrypted);
|
||||
|
||||
// Update cache
|
||||
_cachedApiKey = apiKey;
|
||||
}
|
@@ -130,7 +130,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
|
@@ -42,6 +42,7 @@ dependencies:
|
||||
ps_list: ^0.0.5
|
||||
intl: ^0.20.2
|
||||
http: ^1.4.0
|
||||
crypto: ^3.0.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user