UserService.vala 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. using Inversion;
  2. using Invercargill.DataStructures;
  3. namespace Spry.Authentication {
  4. /**
  5. * Error domain for user-related operations.
  6. */
  7. public errordomain UserError {
  8. USER_NOT_FOUND,
  9. DUPLICATE_USERNAME,
  10. DUPLICATE_EMAIL,
  11. INVALID_PASSWORD,
  12. INVALID_CREDENTIALS,
  13. USER_INACTIVE,
  14. PERMISSION_DENIED,
  15. STORAGE_ERROR
  16. }
  17. /**
  18. * UserService provides user management operations including CRUD,
  19. * password hashing, and authentication.
  20. *
  21. * This service uses the inject<> pattern for dependency injection.
  22. * All methods are async to work with the repository async API.
  23. */
  24. public class UserService : GLib.Object {
  25. private UserRepository _repository = inject<UserRepository>();
  26. // =========================================================================
  27. // User Creation
  28. // =========================================================================
  29. /**
  30. * Creates a new user with the specified credentials.
  31. *
  32. * This method:
  33. * - Validates username uniqueness
  34. * - Validates email uniqueness
  35. * - Hashes password with Argon2id via libsodium
  36. * - Creates User with UUID and timestamps
  37. *
  38. * @param username The unique username
  39. * @param email The unique email address
  40. * @param password The plaintext password to hash
  41. * @return The created User
  42. * @throws UserError on validation or storage failure
  43. */
  44. public async User create_user_async(string username, string email, string password) throws Error {
  45. // Validate username uniqueness
  46. if (yield username_exists_async(username)) {
  47. throw new UserError.DUPLICATE_USERNAME("Username already exists");
  48. }
  49. // Validate email uniqueness
  50. if (yield email_exists_async(email)) {
  51. throw new UserError.DUPLICATE_EMAIL("Email already exists");
  52. }
  53. // Hash password with Argon2id
  54. var password_hash = hash_password(password);
  55. if (password_hash == null) {
  56. throw new UserError.STORAGE_ERROR("Failed to hash password");
  57. }
  58. // Create user via repository
  59. var user = yield _repository.create(username, email, (!)password_hash);
  60. return user;
  61. }
  62. // =========================================================================
  63. // User Retrieval
  64. // =========================================================================
  65. /**
  66. * Gets a user by their unique ID.
  67. *
  68. * @param user_id The user's unique identifier
  69. * @return The User, or null if not found
  70. * @throws Error on storage failure
  71. */
  72. public async User? get_user_async(string user_id) throws Error {
  73. return yield _repository.get_by_id(user_id);
  74. }
  75. /**
  76. * Gets a user by their username.
  77. *
  78. * @param username The username to look up
  79. * @return The User, or null if not found
  80. * @throws Error on storage failure
  81. */
  82. public async User? get_user_by_username_async(string username) throws Error {
  83. return yield _repository.get_by_username(username);
  84. }
  85. /**
  86. * Gets a user by their email address.
  87. *
  88. * @param email The email address to look up
  89. * @return The User, or null if not found
  90. * @throws Error on storage failure
  91. */
  92. public async User? get_user_by_email_async(string email) throws Error {
  93. return yield _repository.get_by_email(email);
  94. }
  95. // =========================================================================
  96. // User Update
  97. // =========================================================================
  98. /**
  99. * Updates an existing user.
  100. *
  101. * This method:
  102. * - Updates the updated_at timestamp
  103. * - Handles username/email changes with uniqueness validation
  104. *
  105. * @param user The user to update
  106. * @throws Error on validation or storage failure
  107. */
  108. public async void update_user_async(User user) throws Error {
  109. // Get existing user to check for changes
  110. var existing = yield get_user_async(user.id);
  111. if (existing == null) {
  112. throw new UserError.USER_NOT_FOUND("User not found");
  113. }
  114. // Check if username changed
  115. if (existing.username != user.username) {
  116. // Check new username uniqueness
  117. var existing_with_username = yield get_user_by_username_async(user.username);
  118. if (existing_with_username != null && existing_with_username.id != user.id) {
  119. throw new UserError.DUPLICATE_USERNAME("Username already exists");
  120. }
  121. }
  122. // Check if email changed
  123. if (existing.email != user.email) {
  124. // Check new email uniqueness
  125. var existing_with_email = yield get_user_by_email_async(user.email);
  126. if (existing_with_email != null && existing_with_email.id != user.id) {
  127. throw new UserError.DUPLICATE_EMAIL("Email already exists");
  128. }
  129. }
  130. // Update timestamp
  131. user.updated_at = new DateTime.now_utc();
  132. // Store updated user via repository
  133. yield _repository.update(user);
  134. }
  135. // =========================================================================
  136. // User Deletion
  137. // =========================================================================
  138. /**
  139. * Deletes a user by their unique ID.
  140. *
  141. * @param user_id The user's unique identifier
  142. * @throws Error on storage failure
  143. */
  144. public async void delete_user_async(string user_id) throws Error {
  145. // Get user first (optional, for logging/cleanup)
  146. var user = yield get_user_async(user_id);
  147. if (user == null) {
  148. throw new UserError.USER_NOT_FOUND("User not found");
  149. }
  150. // Delete user via repository
  151. yield _repository.delete(user_id);
  152. }
  153. // =========================================================================
  154. // User Listing
  155. // =========================================================================
  156. /**
  157. * Lists users with pagination support.
  158. *
  159. * Note: This method is not supported by the basic UserRepository interface.
  160. * Subclasses or extensions should implement this as needed.
  161. *
  162. * @param offset The number of users to skip
  163. * @param limit The maximum number of users to return
  164. * @return A Vector of users
  165. * @throws Error on storage failure
  166. */
  167. public async Vector<User> list_users_async(int offset = 0, int limit = 100) throws Error {
  168. // The basic UserRepository interface doesn't include list operations
  169. // This would need to be added to the interface or handled differently
  170. // For now, return an empty list as a placeholder
  171. return new Vector<User>();
  172. }
  173. // =========================================================================
  174. // Password Management
  175. // =========================================================================
  176. /**
  177. * Hashes a password using Argon2id via libsodium.
  178. *
  179. * @param password The plaintext password to hash
  180. * @return The hashed password string, or null on failure
  181. */
  182. public string? hash_password(string password) {
  183. return Sodium.PasswordHashing.hash(password);
  184. }
  185. /**
  186. * Verifies a password against a stored hash.
  187. *
  188. * @param user The user to verify against
  189. * @param password The plaintext password to verify
  190. * @return true if the password matches, false otherwise
  191. */
  192. public bool verify_password(User user, string password) {
  193. return Sodium.PasswordHashing.check(user.password_hash, password);
  194. }
  195. /**
  196. * Sets a new password for a user.
  197. *
  198. * @param user The user to update
  199. * @param new_password The new plaintext password
  200. * @throws Error on failure
  201. */
  202. public async void set_password_async(User user, string new_password) throws Error {
  203. var password_hash = hash_password(new_password);
  204. if (password_hash == null) {
  205. throw new UserError.STORAGE_ERROR("Failed to hash password");
  206. }
  207. user.password_hash = (!)password_hash;
  208. user.updated_at = new DateTime.now_utc();
  209. yield update_user_async(user);
  210. }
  211. // =========================================================================
  212. // Authentication
  213. // =========================================================================
  214. /**
  215. * Authenticates a user by username/email and password.
  216. *
  217. * This method:
  218. * - Looks up user by username or email
  219. * - Verifies the password
  220. * - Returns the user if valid
  221. *
  222. * @param username_or_email The username or email address
  223. * @param password The plaintext password
  224. * @return The authenticated User, or null if authentication failed
  225. * @throws Error on storage failure
  226. */
  227. public async User? authenticate_async(string username_or_email, string password) throws Error {
  228. // Try to find user by username first, then by email
  229. User? user = yield get_user_by_username_async(username_or_email);
  230. if (user == null) {
  231. user = yield get_user_by_email_async(username_or_email);
  232. }
  233. if (user == null) {
  234. return null;
  235. }
  236. // Verify password
  237. bool password_valid = verify_password(user, password);
  238. if (!password_valid) {
  239. return null;
  240. }
  241. return user;
  242. }
  243. // =========================================================================
  244. // Utility Methods
  245. // =========================================================================
  246. /**
  247. * Checks if a username already exists.
  248. *
  249. * @param username The username to check
  250. * @return true if the username exists
  251. * @throws Error on storage failure
  252. */
  253. public async bool username_exists_async(string username) throws Error {
  254. return yield _repository.exists_by_username(username);
  255. }
  256. /**
  257. * Checks if an email already exists.
  258. *
  259. * @param email The email to check
  260. * @return true if the email exists
  261. * @throws Error on storage failure
  262. */
  263. public async bool email_exists_async(string email) throws Error {
  264. return yield _repository.exists_by_email(email);
  265. }
  266. /**
  267. * Gets the total count of users.
  268. *
  269. * Note: This method is not supported by the basic UserRepository interface.
  270. * Subclasses or extensions should implement this as needed.
  271. *
  272. * @return The number of users
  273. * @throws Error on storage failure
  274. */
  275. public async int user_count_async() throws Error {
  276. // The basic UserRepository interface doesn't include count operations
  277. // This would need to be added to the interface or handled differently
  278. return 0;
  279. }
  280. // =========================================================================
  281. // Permission Operations
  282. // =========================================================================
  283. /**
  284. * Adds a permission to a user.
  285. *
  286. * @param user The user to add the permission to
  287. * @param permission The permission to add
  288. * @throws Error on storage failure
  289. */
  290. public async void add_permission_async(User user, string permission) throws Error {
  291. yield _repository.add_permission(user.id, permission);
  292. }
  293. /**
  294. * Removes a permission from a user.
  295. *
  296. * @param user The user to remove the permission from
  297. * @param permission The permission to remove
  298. * @throws Error on storage failure
  299. */
  300. public async void remove_permission_async(User user, string permission) throws Error {
  301. yield _repository.remove_permission(user.id, permission);
  302. }
  303. /**
  304. * Checks if a user has a specific permission.
  305. *
  306. * @param user The user to check
  307. * @param permission The permission to check
  308. * @return true if the user has the permission
  309. * @throws Error on storage failure
  310. */
  311. public async bool has_permission_async(User user, string permission) throws Error {
  312. return yield _repository.has_permission(user.id, permission);
  313. }
  314. /**
  315. * Gets all permissions for a user.
  316. *
  317. * @param user The user to get permissions for
  318. * @return A Vector of permission strings
  319. * @throws Error on storage failure
  320. */
  321. public async Vector<string> get_permissions_async(User user) throws Error {
  322. return yield _repository.get_permissions(user.id);
  323. }
  324. // =========================================================================
  325. // App Data Operations
  326. // =========================================================================
  327. /**
  328. * Sets an app data value for a user.
  329. *
  330. * @param user The user to set the app data for
  331. * @param key The app data key
  332. * @param value The app data value
  333. * @throws Error on storage failure
  334. */
  335. public async void set_app_data_async(User user, string key, string value) throws Error {
  336. yield _repository.set_app_data(user.id, key, value);
  337. }
  338. /**
  339. * Gets an app data value for a user.
  340. *
  341. * @param user The user to get the app data for
  342. * @param key The app data key
  343. * @return The app data value, or null if not found
  344. * @throws Error on storage failure
  345. */
  346. public async string? get_app_data_async(User user, string key) throws Error {
  347. return yield _repository.get_app_data(user.id, key);
  348. }
  349. }
  350. }