hazo_auth 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,6 +29,215 @@ After installing the package, you need to set up configuration files in your pro
29
29
 
30
30
  **Important:** The configuration files must be located in your project root directory (where `process.cwd()` points to), not inside `node_modules`. The package reads configuration from `process.cwd()` at runtime, so storing them elsewhere (including `node_modules/hazo_auth`) will break runtime access.
31
31
 
32
+ ### Database Setup
33
+
34
+ Before using `hazo_auth`, you need to create the required database tables. Run the following SQL scripts in your PostgreSQL database:
35
+
36
+ #### 1. Create the Profile Source Enum Type
37
+
38
+ ```sql
39
+ -- Enum type for profile picture source
40
+ CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
41
+ ```
42
+
43
+ #### 2. Create the Users Table
44
+
45
+ ```sql
46
+ -- Main users table
47
+ CREATE TABLE hazo_users (
48
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
49
+ email_address TEXT NOT NULL UNIQUE,
50
+ password_hash TEXT NOT NULL,
51
+ name TEXT,
52
+ email_verified BOOLEAN NOT NULL DEFAULT FALSE,
53
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
54
+ login_attempts INTEGER NOT NULL DEFAULT 0,
55
+ last_logon TIMESTAMP WITH TIME ZONE,
56
+ profile_picture_url TEXT,
57
+ profile_source hazo_enum_profile_source_enum,
58
+ mfa_secret TEXT,
59
+ url_on_logon TEXT,
60
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
61
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
62
+ );
63
+
64
+ -- Index for email lookups
65
+ CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
66
+ ```
67
+
68
+ #### 3. Create the Refresh Tokens Table
69
+
70
+ ```sql
71
+ -- Refresh tokens table (used for password reset, email verification, etc.)
72
+ CREATE TABLE hazo_refresh_tokens (
73
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
74
+ user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
75
+ token_hash TEXT NOT NULL,
76
+ token_type TEXT NOT NULL,
77
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
78
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
79
+ );
80
+
81
+ -- Index for token lookups
82
+ CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
83
+ CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
84
+ ```
85
+
86
+ #### 4. Create the Permissions Table
87
+
88
+ ```sql
89
+ -- Permissions table for RBAC
90
+ CREATE TABLE hazo_permissions (
91
+ id SERIAL PRIMARY KEY,
92
+ permission_name TEXT NOT NULL UNIQUE,
93
+ description TEXT,
94
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
95
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
96
+ );
97
+ ```
98
+
99
+ #### 5. Create the Roles Table
100
+
101
+ ```sql
102
+ -- Roles table for RBAC
103
+ CREATE TABLE hazo_roles (
104
+ id SERIAL PRIMARY KEY,
105
+ role_name TEXT NOT NULL UNIQUE,
106
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
107
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
108
+ );
109
+ ```
110
+
111
+ #### 6. Create the Role-Permissions Junction Table
112
+
113
+ ```sql
114
+ -- Junction table linking roles to permissions
115
+ CREATE TABLE hazo_role_permissions (
116
+ role_id INTEGER NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
117
+ permission_id INTEGER NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
118
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
119
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
120
+ PRIMARY KEY (role_id, permission_id)
121
+ );
122
+
123
+ -- Indexes for lookups
124
+ CREATE INDEX idx_hazo_role_permissions_role_id ON hazo_role_permissions(role_id);
125
+ CREATE INDEX idx_hazo_role_permissions_permission_id ON hazo_role_permissions(permission_id);
126
+ ```
127
+
128
+ #### 7. Create the User-Roles Junction Table
129
+
130
+ ```sql
131
+ -- Junction table linking users to roles
132
+ CREATE TABLE hazo_user_roles (
133
+ user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
134
+ role_id INTEGER NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
135
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
136
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
137
+ PRIMARY KEY (user_id, role_id)
138
+ );
139
+
140
+ -- Indexes for lookups
141
+ CREATE INDEX idx_hazo_user_roles_user_id ON hazo_user_roles(user_id);
142
+ CREATE INDEX idx_hazo_user_roles_role_id ON hazo_user_roles(role_id);
143
+ ```
144
+
145
+ #### Complete Setup Script
146
+
147
+ For convenience, here's the complete SQL script to create all tables at once:
148
+
149
+ ```sql
150
+ -- ============================================
151
+ -- hazo_auth Database Setup Script
152
+ -- ============================================
153
+
154
+ -- 1. Create enum type
155
+ CREATE TYPE hazo_enum_profile_source_enum AS ENUM ('gravatar', 'custom', 'predefined');
156
+
157
+ -- 2. Create users table
158
+ CREATE TABLE hazo_users (
159
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
160
+ email_address TEXT NOT NULL UNIQUE,
161
+ password_hash TEXT NOT NULL,
162
+ name TEXT,
163
+ email_verified BOOLEAN NOT NULL DEFAULT FALSE,
164
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
165
+ login_attempts INTEGER NOT NULL DEFAULT 0,
166
+ last_logon TIMESTAMP WITH TIME ZONE,
167
+ profile_picture_url TEXT,
168
+ profile_source hazo_enum_profile_source_enum,
169
+ mfa_secret TEXT,
170
+ url_on_logon TEXT,
171
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
172
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
173
+ );
174
+ CREATE INDEX idx_hazo_users_email ON hazo_users(email_address);
175
+
176
+ -- 3. Create refresh tokens table
177
+ CREATE TABLE hazo_refresh_tokens (
178
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
179
+ user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
180
+ token_hash TEXT NOT NULL,
181
+ token_type TEXT NOT NULL,
182
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
183
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
184
+ );
185
+ CREATE INDEX idx_hazo_refresh_tokens_user_id ON hazo_refresh_tokens(user_id);
186
+ CREATE INDEX idx_hazo_refresh_tokens_token_type ON hazo_refresh_tokens(token_type);
187
+
188
+ -- 4. Create permissions table
189
+ CREATE TABLE hazo_permissions (
190
+ id SERIAL PRIMARY KEY,
191
+ permission_name TEXT NOT NULL UNIQUE,
192
+ description TEXT,
193
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
194
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
195
+ );
196
+
197
+ -- 5. Create roles table
198
+ CREATE TABLE hazo_roles (
199
+ id SERIAL PRIMARY KEY,
200
+ role_name TEXT NOT NULL UNIQUE,
201
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
202
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
203
+ );
204
+
205
+ -- 6. Create role-permissions junction table
206
+ CREATE TABLE hazo_role_permissions (
207
+ role_id INTEGER NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
208
+ permission_id INTEGER NOT NULL REFERENCES hazo_permissions(id) ON DELETE CASCADE,
209
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
210
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
211
+ PRIMARY KEY (role_id, permission_id)
212
+ );
213
+ CREATE INDEX idx_hazo_role_permissions_role_id ON hazo_role_permissions(role_id);
214
+ CREATE INDEX idx_hazo_role_permissions_permission_id ON hazo_role_permissions(permission_id);
215
+
216
+ -- 7. Create user-roles junction table
217
+ CREATE TABLE hazo_user_roles (
218
+ user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
219
+ role_id INTEGER NOT NULL REFERENCES hazo_roles(id) ON DELETE CASCADE,
220
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
221
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
222
+ PRIMARY KEY (user_id, role_id)
223
+ );
224
+ CREATE INDEX idx_hazo_user_roles_user_id ON hazo_user_roles(user_id);
225
+ CREATE INDEX idx_hazo_user_roles_role_id ON hazo_user_roles(role_id);
226
+ ```
227
+
228
+ #### Initialize Default Permissions and Super User
229
+
230
+ After creating the tables, you can use the `init-users` script to set up default permissions and a super user:
231
+
232
+ ```bash
233
+ npm run init-users
234
+ ```
235
+
236
+ This script reads from `hazo_auth_config.ini` and:
237
+ 1. Creates default permissions from `application_permission_list_defaults`
238
+ 2. Creates a `default_super_user_role` role with all permissions
239
+ 3. Assigns the role to the user specified in `default_super_user_email`
240
+
32
241
  ### Expose hazo_auth Routes in the Consumer App
33
242
 
34
243
  Because `src/app/hazo_auth` (pages) and `src/app/api/hazo_auth` (API routes) need to be part of the consuming Next.js app’s routing tree, make sure they exist in your project’s `src/app` directory. Two recommended approaches:
@@ -1009,6 +1218,88 @@ Example custom styling:
1009
1218
  }
1010
1219
  ```
1011
1220
 
1221
+ ## User Profile Service
1222
+
1223
+ The `hazo_auth` package provides a batch user profile retrieval service for applications that need basic user information, such as chat applications or user lists.
1224
+
1225
+ ### `hazo_get_user_profiles`
1226
+
1227
+ Retrieves basic profile information for multiple users in a single batch call.
1228
+
1229
+ **Location:** `src/lib/services/user_profiles_service.ts`
1230
+
1231
+ **Function Signature:**
1232
+ ```typescript
1233
+ import { hazo_get_user_profiles } from "hazo_auth/lib/services/user_profiles_service";
1234
+ import type { GetProfilesResult, UserProfileInfo } from "hazo_auth/lib/services/user_profiles_service";
1235
+
1236
+ async function hazo_get_user_profiles(
1237
+ adapter: HazoConnectAdapter,
1238
+ user_ids: string[],
1239
+ ): Promise<GetProfilesResult>
1240
+ ```
1241
+
1242
+ **Return Type:**
1243
+ ```typescript
1244
+ type UserProfileInfo = {
1245
+ user_id: string;
1246
+ profile_picture_url: string | null;
1247
+ email: string;
1248
+ name: string | null;
1249
+ days_since_created: number;
1250
+ };
1251
+
1252
+ type GetProfilesResult = {
1253
+ success: boolean;
1254
+ profiles: UserProfileInfo[];
1255
+ not_found_ids: string[];
1256
+ error?: string;
1257
+ };
1258
+ ```
1259
+
1260
+ **Features:**
1261
+ - **Batch Retrieval:** Fetches multiple user profiles in a single database query
1262
+ - **Deduplication:** Automatically removes duplicate user IDs from input
1263
+ - **Not Found Tracking:** Returns list of user IDs that were not found in the database
1264
+ - **Profile Picture:** Returns the resolved profile picture URL (Gravatar, library, or uploaded)
1265
+ - **Account Age:** Calculates days since account creation
1266
+
1267
+ **Example Usage:**
1268
+
1269
+ ```typescript
1270
+ // In an API route or server component
1271
+ import { hazo_get_user_profiles } from "hazo_auth/lib/services/user_profiles_service";
1272
+ import { get_hazo_connect_instance } from "hazo_auth/lib/hazo_connect_instance.server";
1273
+
1274
+ export async function GET(request: NextRequest) {
1275
+ const adapter = get_hazo_connect_instance();
1276
+
1277
+ // Get profiles for multiple users (e.g., chat participants)
1278
+ const result = await hazo_get_user_profiles(adapter, [
1279
+ "user-id-1",
1280
+ "user-id-2",
1281
+ "user-id-3",
1282
+ ]);
1283
+
1284
+ if (!result.success) {
1285
+ return NextResponse.json({ error: result.error }, { status: 500 });
1286
+ }
1287
+
1288
+ // result.profiles contains found user profiles
1289
+ // result.not_found_ids contains IDs that weren't found
1290
+ return NextResponse.json({
1291
+ profiles: result.profiles,
1292
+ not_found: result.not_found_ids,
1293
+ });
1294
+ }
1295
+ ```
1296
+
1297
+ **Use Cases:**
1298
+ - Chat applications displaying participant information
1299
+ - User lists with profile pictures and names
1300
+ - Activity feeds showing user details
1301
+ - Any feature requiring batch user profile lookups
1302
+
1012
1303
  ### Local Development (for package contributors)
1013
1304
 
1014
1305
  - `npm install` to install dependencies.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_auth",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "files": [
5
5
  "src/**/*",
6
6
  "public/file.svg",
@@ -103,7 +103,7 @@
103
103
  "better-sqlite3": "^12.4.1",
104
104
  "cross-env": "^10.1.0",
105
105
  "eslint": "^8.57.0",
106
- "eslint-config-next": "^14.2.7",
106
+ "eslint-config-next": "^16.0.4",
107
107
  "eslint-plugin-storybook": "^10.0.6",
108
108
  "jest": "^30.2.0",
109
109
  "jest-environment-jsdom": "^29.7.0",
@@ -16,7 +16,7 @@ type InitSummary = {
16
16
  role: {
17
17
  inserted: boolean;
18
18
  existing: boolean;
19
- role_id: number | null;
19
+ role_id: string | null;
20
20
  };
21
21
  role_permissions: {
22
22
  inserted: number;
@@ -115,7 +115,7 @@ async function init_users(): Promise<void> {
115
115
  console.log();
116
116
 
117
117
  // 2. Add permissions to hazo_permissions table
118
- const permission_id_map: Record<string, number> = {};
118
+ const permission_id_map: Record<string, string> = {};
119
119
  const now = new Date().toISOString();
120
120
 
121
121
  for (const permission_name of permission_names) {
@@ -129,7 +129,7 @@ async function init_users(): Promise<void> {
129
129
 
130
130
  if (Array.isArray(existing_permissions) && existing_permissions.length > 0) {
131
131
  const existing_permission = existing_permissions[0];
132
- const perm_id = existing_permission.id as number;
132
+ const perm_id = existing_permission.id as string;
133
133
  permission_id_map[trimmed_name] = perm_id;
134
134
  summary.permissions.existing.push(trimmed_name);
135
135
  console.log(`✓ Permission already exists: ${trimmed_name} (ID: ${perm_id})`);
@@ -143,8 +143,8 @@ async function init_users(): Promise<void> {
143
143
  });
144
144
 
145
145
  const perm_id = Array.isArray(new_permission)
146
- ? (new_permission[0] as { id: number }).id
147
- : (new_permission as { id: number }).id;
146
+ ? (new_permission[0] as { id: string }).id
147
+ : (new_permission as { id: string }).id;
148
148
  permission_id_map[trimmed_name] = perm_id;
149
149
  summary.permissions.inserted.push(trimmed_name);
150
150
  console.log(`✓ Inserted permission: ${trimmed_name} (ID: ${perm_id})`);
@@ -159,9 +159,9 @@ async function init_users(): Promise<void> {
159
159
  role_name,
160
160
  });
161
161
 
162
- let role_id: number;
162
+ let role_id: string;
163
163
  if (Array.isArray(existing_roles) && existing_roles.length > 0) {
164
- role_id = existing_roles[0].id as number;
164
+ role_id = existing_roles[0].id as string;
165
165
  summary.role.existing = true;
166
166
  summary.role.role_id = role_id;
167
167
  console.log(`✓ Role already exists: ${role_name} (ID: ${role_id})`);
@@ -173,8 +173,8 @@ async function init_users(): Promise<void> {
173
173
  });
174
174
 
175
175
  role_id = Array.isArray(new_role)
176
- ? (new_role[0] as { id: number }).id
177
- : (new_role as { id: number }).id;
176
+ ? (new_role[0] as { id: string }).id
177
+ : (new_role as { id: string }).id;
178
178
  summary.role.inserted = true;
179
179
  summary.role.role_id = role_id;
180
180
  console.log(`✓ Created role: ${role_name} (ID: ${role_id})`);
@@ -0,0 +1,143 @@
1
+ // file_description: service for batch retrieval of basic user profile information for chat applications and similar use cases
2
+ // section: imports
3
+ import type { HazoConnectAdapter } from "hazo_connect";
4
+ import { createCrudService } from "hazo_connect/server";
5
+ import { differenceInDays } from "date-fns";
6
+ import { create_app_logger } from "../app_logger";
7
+ import { sanitize_error_for_user } from "../utils/error_sanitizer";
8
+
9
+ // section: types
10
+ /**
11
+ * Basic user profile information returned by get_profiles
12
+ * Contains resolved profile picture URL, email, name, and account age
13
+ */
14
+ export type UserProfileInfo = {
15
+ user_id: string;
16
+ profile_picture_url: string | null;
17
+ email: string;
18
+ name: string | null;
19
+ days_since_created: number;
20
+ };
21
+
22
+ /**
23
+ * Result type for get_profiles function
24
+ * Includes found profiles and list of IDs that were not found
25
+ */
26
+ export type GetProfilesResult = {
27
+ success: boolean;
28
+ profiles: UserProfileInfo[];
29
+ not_found_ids: string[];
30
+ error?: string;
31
+ };
32
+
33
+ // section: helpers
34
+ /**
35
+ * Retrieves basic profile information for multiple users in a single batch call
36
+ * Useful for chat applications and similar use cases where basic user info is needed
37
+ * @param adapter - The hazo_connect adapter instance
38
+ * @param user_ids - Array of user IDs to retrieve profiles for
39
+ * @returns GetProfilesResult with found profiles and list of not found IDs
40
+ */
41
+ export async function hazo_get_user_profiles(
42
+ adapter: HazoConnectAdapter,
43
+ user_ids: string[],
44
+ ): Promise<GetProfilesResult> {
45
+ const logger = create_app_logger();
46
+
47
+ try {
48
+ // Handle empty input
49
+ if (!user_ids || user_ids.length === 0) {
50
+ return {
51
+ success: true,
52
+ profiles: [],
53
+ not_found_ids: [],
54
+ };
55
+ }
56
+
57
+ // Remove duplicates from input
58
+ const unique_user_ids = [...new Set(user_ids)];
59
+
60
+ // Create CRUD service for hazo_users table
61
+ const users_service = createCrudService(adapter, "hazo_users");
62
+
63
+ // Query users by IDs using the 'in' filter
64
+ // PostgREST supports 'in' filter syntax: id=in.(id1,id2,id3)
65
+ const users = await users_service.findBy({
66
+ id: `in.(${unique_user_ids.join(",")})`,
67
+ });
68
+
69
+ // Handle case where no users are found
70
+ if (!Array.isArray(users)) {
71
+ logger.warn("hazo_get_user_profiles_unexpected_response", {
72
+ filename: "user_profiles_service.ts",
73
+ line_number: 70,
74
+ message: "Unexpected response format from database query",
75
+ user_ids: unique_user_ids,
76
+ });
77
+
78
+ return {
79
+ success: true,
80
+ profiles: [],
81
+ not_found_ids: unique_user_ids,
82
+ };
83
+ }
84
+
85
+ // Build set of found user IDs for quick lookup
86
+ const found_user_ids = new Set(users.map((user) => user.id as string));
87
+
88
+ // Determine which user IDs were not found
89
+ const not_found_ids = unique_user_ids.filter((id) => !found_user_ids.has(id));
90
+
91
+ // Transform database records to UserProfileInfo
92
+ const now = new Date();
93
+ const profiles: UserProfileInfo[] = users.map((user) => {
94
+ const created_at = user.created_at as string;
95
+ const created_date = new Date(created_at);
96
+ const days_since_created = differenceInDays(now, created_date);
97
+
98
+ return {
99
+ user_id: user.id as string,
100
+ profile_picture_url: (user.profile_picture_url as string) || null,
101
+ email: user.email_address as string,
102
+ name: (user.name as string) || null,
103
+ days_since_created,
104
+ };
105
+ });
106
+
107
+ // Log successful retrieval
108
+ logger.info("hazo_get_user_profiles_success", {
109
+ filename: "user_profiles_service.ts",
110
+ line_number: 105,
111
+ message: "Successfully retrieved user profiles",
112
+ requested_count: unique_user_ids.length,
113
+ found_count: profiles.length,
114
+ not_found_count: not_found_ids.length,
115
+ });
116
+
117
+ return {
118
+ success: true,
119
+ profiles,
120
+ not_found_ids,
121
+ };
122
+ } catch (error) {
123
+ const user_friendly_error = sanitize_error_for_user(error, {
124
+ logToConsole: true,
125
+ logToLogger: true,
126
+ logger,
127
+ context: {
128
+ filename: "user_profiles_service.ts",
129
+ line_number: 122,
130
+ operation: "hazo_get_user_profiles",
131
+ user_ids_count: user_ids?.length || 0,
132
+ },
133
+ });
134
+
135
+ return {
136
+ success: false,
137
+ profiles: [],
138
+ not_found_ids: [],
139
+ error: user_friendly_error,
140
+ };
141
+ }
142
+ }
143
+