SqlUserRepository.vala 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using InvercargillSql;
  4. namespace Spry.Authentication {
  5. /**
  6. * SQL implementation of UserRepository using InvercargillSql.
  7. */
  8. public class SqlUserRepository : Object, UserRepository {
  9. private Connection _connection;
  10. // =========================================================================
  11. // Constructor
  12. // =========================================================================
  13. public SqlUserRepository(Connection connection) {
  14. _connection = connection;
  15. }
  16. // =========================================================================
  17. // Retrieval Operations
  18. // =========================================================================
  19. public async User? get_by_id(string id) throws Error {
  20. var sql = "SELECT * FROM users WHERE id = :id";
  21. var results = yield _connection.create_command(sql)
  22. .with_parameter("id", id)
  23. .execute_query_async();
  24. var row = results.first_or_default();
  25. if (row == null) {
  26. return null;
  27. }
  28. var user = user_from_properties(row);
  29. // Load permissions
  30. var permissions = yield get_permissions(id);
  31. foreach (var perm in permissions) {
  32. user.add_permission(perm);
  33. }
  34. // Load app data
  35. var app_data = yield get_all_app_data(id);
  36. foreach (var key in app_data.keys) {
  37. user.app_data.set(key, app_data.get(key));
  38. }
  39. return user;
  40. }
  41. public async User? get_by_username(string username) throws Error {
  42. var sql = "SELECT * FROM users WHERE username = :username";
  43. var results = yield _connection.create_command(sql)
  44. .with_parameter("username", username)
  45. .execute_query_async();
  46. var row = results.first_or_default();
  47. if (row == null) {
  48. return null;
  49. }
  50. var user = user_from_properties(row);
  51. // Load permissions
  52. var permissions = yield get_permissions(user.id);
  53. foreach (var perm in permissions) {
  54. user.add_permission(perm);
  55. }
  56. // Load app data
  57. var app_data = yield get_all_app_data(user.id);
  58. foreach (var key in app_data.keys) {
  59. user.app_data.set(key, app_data.get(key));
  60. }
  61. return user;
  62. }
  63. public async User? get_by_email(string email) throws Error {
  64. var sql = "SELECT * FROM users WHERE email = :email";
  65. var results = yield _connection.create_command(sql)
  66. .with_parameter("email", email)
  67. .execute_query_async();
  68. var row = results.first_or_default();
  69. if (row == null) {
  70. return null;
  71. }
  72. var user = user_from_properties(row);
  73. // Load permissions
  74. var permissions = yield get_permissions(user.id);
  75. foreach (var perm in permissions) {
  76. user.add_permission(perm);
  77. }
  78. // Load app data
  79. var app_data = yield get_all_app_data(user.id);
  80. foreach (var key in app_data.keys) {
  81. user.app_data.set(key, app_data.get(key));
  82. }
  83. return user;
  84. }
  85. // =========================================================================
  86. // Mutation Operations
  87. // =========================================================================
  88. public async User create(string username, string email, string password_hash) throws Error {
  89. var id = generate_uuid();
  90. var now = new DateTime.now_utc();
  91. var sql = """
  92. INSERT INTO users (id, username, email, password_hash, created_at, updated_at)
  93. VALUES (:id, :username, :email, :password_hash, :created_at, :updated_at)
  94. """;
  95. yield _connection.create_command(sql)
  96. .with_parameter("id", id)
  97. .with_parameter("username", username)
  98. .with_parameter("email", email)
  99. .with_parameter("password_hash", password_hash)
  100. .with_parameter("created_at", now.format_iso8601())
  101. .with_parameter("updated_at", now.format_iso8601())
  102. .execute_non_query_async();
  103. var user = new User();
  104. user.set_id(id);
  105. user.set_username(username);
  106. user.email = email;
  107. user.password_hash = password_hash;
  108. user.created_at = now;
  109. user.updated_at = now;
  110. return user;
  111. }
  112. public async void update(User user) throws Error {
  113. var now = new DateTime.now_utc();
  114. var sql = """
  115. UPDATE users SET
  116. username = :username,
  117. email = :email,
  118. password_hash = :password_hash,
  119. updated_at = :updated_at
  120. WHERE id = :id
  121. """;
  122. yield _connection.create_command(sql)
  123. .with_parameter("id", user.id)
  124. .with_parameter("username", user.username)
  125. .with_parameter("email", user.email)
  126. .with_parameter("password_hash", user.password_hash)
  127. .with_parameter("updated_at", now.format_iso8601())
  128. .execute_non_query_async();
  129. user.updated_at = now;
  130. }
  131. public async void delete(string id) throws Error {
  132. var sql = "DELETE FROM users WHERE id = :id";
  133. yield _connection.create_command(sql)
  134. .with_parameter("id", id)
  135. .execute_non_query_async();
  136. }
  137. // =========================================================================
  138. // Query Operations
  139. // =========================================================================
  140. public async bool exists_by_username(string username) throws Error {
  141. var sql = "SELECT COUNT(*) FROM users WHERE username = :username";
  142. var scalar = yield _connection.create_command(sql)
  143. .with_parameter("username", username)
  144. .execute_scalar_async();
  145. if (scalar == null) {
  146. return false;
  147. }
  148. return scalar.as<int64?>() > 0;
  149. }
  150. public async bool exists_by_email(string email) throws Error {
  151. var sql = "SELECT COUNT(*) FROM users WHERE email = :email";
  152. var scalar = yield _connection.create_command(sql)
  153. .with_parameter("email", email)
  154. .execute_scalar_async();
  155. if (scalar == null) {
  156. return false;
  157. }
  158. return scalar.as<int64?>() > 0;
  159. }
  160. // =========================================================================
  161. // Permission Operations
  162. // =========================================================================
  163. public async void add_permission(string user_id, string permission) throws Error {
  164. var sql = """
  165. INSERT OR IGNORE INTO user_permissions (user_id, permission)
  166. VALUES (:user_id, :permission)
  167. """;
  168. yield _connection.create_command(sql)
  169. .with_parameter("user_id", user_id)
  170. .with_parameter("permission", permission)
  171. .execute_non_query_async();
  172. }
  173. public async void remove_permission(string user_id, string permission) throws Error {
  174. var sql = """
  175. DELETE FROM user_permissions
  176. WHERE user_id = :user_id AND permission = :permission
  177. """;
  178. yield _connection.create_command(sql)
  179. .with_parameter("user_id", user_id)
  180. .with_parameter("permission", permission)
  181. .execute_non_query_async();
  182. }
  183. public async bool has_permission(string user_id, string permission) throws Error {
  184. var sql = """
  185. SELECT COUNT(*) FROM user_permissions
  186. WHERE user_id = :user_id AND permission = :permission
  187. """;
  188. var scalar = yield _connection.create_command(sql)
  189. .with_parameter("user_id", user_id)
  190. .with_parameter("permission", permission)
  191. .execute_scalar_async();
  192. if (scalar == null) {
  193. return false;
  194. }
  195. return scalar.as<int64?>() > 0;
  196. }
  197. public async Vector<string> get_permissions(string user_id) throws Error {
  198. var sql = "SELECT permission FROM user_permissions WHERE user_id = :user_id";
  199. var results = yield _connection.create_command(sql)
  200. .with_parameter("user_id", user_id)
  201. .execute_query_async();
  202. var permissions = new Vector<string>();
  203. foreach (var row in results) {
  204. var perm_elem = row.get("permission");
  205. if (perm_elem != null) {
  206. var perm = perm_elem.as<string>();
  207. if (perm != null && perm.length > 0) {
  208. permissions.add(perm);
  209. }
  210. }
  211. }
  212. return permissions;
  213. }
  214. // =========================================================================
  215. // App Data Operations
  216. // =========================================================================
  217. public async void set_app_data(string user_id, string key, string value) throws Error {
  218. var sql = """
  219. INSERT OR REPLACE INTO user_app_data (user_id, key, value)
  220. VALUES (:user_id, :key, :value)
  221. """;
  222. yield _connection.create_command(sql)
  223. .with_parameter("user_id", user_id)
  224. .with_parameter("key", key)
  225. .with_parameter("value", value)
  226. .execute_non_query_async();
  227. }
  228. public async string? get_app_data(string user_id, string key) throws Error {
  229. var sql = """
  230. SELECT value FROM user_app_data
  231. WHERE user_id = :user_id AND key = :key
  232. """;
  233. var scalar = yield _connection.create_command(sql)
  234. .with_parameter("user_id", user_id)
  235. .with_parameter("key", key)
  236. .execute_scalar_async();
  237. if (scalar == null) {
  238. return null;
  239. }
  240. return scalar.as<string>();
  241. }
  242. // =========================================================================
  243. // Private Helpers
  244. // =========================================================================
  245. private User user_from_properties(Properties props) {
  246. var user = new User();
  247. // Required fields
  248. user.set_id(get_string_or_empty(props, "id"));
  249. user.set_username(get_string_or_empty(props, "username"));
  250. user.email = get_string_or_empty(props, "email");
  251. user.password_hash = get_string_or_empty(props, "password_hash");
  252. // created_at
  253. var created_str = get_string_or_empty(props, "created_at");
  254. if (created_str.length > 0) {
  255. user.created_at = new DateTime.from_iso8601(created_str, new TimeZone.utc());
  256. }
  257. // updated_at (nullable)
  258. var updated_str = get_string_or_null(props, "updated_at");
  259. if (updated_str != null && updated_str.length > 0) {
  260. user.updated_at = new DateTime.from_iso8601(updated_str, new TimeZone.utc());
  261. }
  262. return user;
  263. }
  264. private string get_string_or_empty(Properties props, string key) {
  265. if (!props.has(key)) {
  266. return "";
  267. }
  268. var elem = props.get(key);
  269. if (elem == null) {
  270. return "";
  271. }
  272. var str = elem.as<string>();
  273. return str ?? "";
  274. }
  275. private string? get_string_or_null(Properties props, string key) {
  276. if (!props.has(key)) {
  277. return null;
  278. }
  279. var elem = props.get(key);
  280. if (elem == null) {
  281. return null;
  282. }
  283. return elem.as<string>();
  284. }
  285. private async Dictionary<string, string> get_all_app_data(string user_id) throws Error {
  286. var sql = "SELECT key, value FROM user_app_data WHERE user_id = :user_id";
  287. var results = yield _connection.create_command(sql)
  288. .with_parameter("user_id", user_id)
  289. .execute_query_async();
  290. var app_data = new Dictionary<string, string>();
  291. foreach (var row in results) {
  292. var key_elem = row.get("key");
  293. var value_elem = row.get("value");
  294. if (key_elem != null) {
  295. var key = key_elem.as<string>();
  296. var value = value_elem != null ? value_elem.as<string>() ?? "" : "";
  297. if (key != null && key.length > 0) {
  298. app_data.set(key, value);
  299. }
  300. }
  301. }
  302. return app_data;
  303. }
  304. private string generate_uuid() {
  305. uint8[] bytes = new uint8[16];
  306. Sodium.Random.random_bytes(bytes);
  307. // Set version 4 (random UUID)
  308. bytes[6] = (bytes[6] & 0x0f) | 0x40;
  309. // Set variant RFC 4122
  310. bytes[8] = (bytes[8] & 0x3f) | 0x80;
  311. return "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x".printf(
  312. bytes[0], bytes[1], bytes[2], bytes[3],
  313. bytes[4], bytes[5], bytes[6], bytes[7],
  314. bytes[8], bytes[9], bytes[10], bytes[11],
  315. bytes[12], bytes[13], bytes[14], bytes[15]
  316. );
  317. }
  318. }
  319. }