UserService.vala 15 KB

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