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 +291 -0
- package/package.json +2 -2
- package/scripts/init_users.ts +9 -9
- package/src/lib/services/user_profiles_service.ts +143 -0
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.
|
|
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": "^
|
|
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",
|
package/scripts/init_users.ts
CHANGED
|
@@ -16,7 +16,7 @@ type InitSummary = {
|
|
|
16
16
|
role: {
|
|
17
17
|
inserted: boolean;
|
|
18
18
|
existing: boolean;
|
|
19
|
-
role_id:
|
|
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,
|
|
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
|
|
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:
|
|
147
|
-
: (new_permission as { 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:
|
|
162
|
+
let role_id: string;
|
|
163
163
|
if (Array.isArray(existing_roles) && existing_roles.length > 0) {
|
|
164
|
-
role_id = existing_roles[0].id as
|
|
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:
|
|
177
|
-
: (new_role as { 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
|
+
|