397 lines
11 KiB
TypeScript
397 lines
11 KiB
TypeScript
import { v4 as uuidv4 } from 'uuid';
|
|
import {
|
|
User,
|
|
Medication,
|
|
UserSettings,
|
|
TakenDoses,
|
|
CustomReminder,
|
|
CouchDBDocument,
|
|
UserRole,
|
|
} from '../types';
|
|
import { AccountStatus } from './auth/auth.constants';
|
|
import { CouchDBError } from './couchdb';
|
|
|
|
// Production CouchDB Service that connects to a real CouchDB instance
|
|
export class CouchDBService {
|
|
private baseUrl: string;
|
|
private auth: string;
|
|
|
|
constructor() {
|
|
// Get CouchDB configuration from environment
|
|
const couchdbUrl = process.env.VITE_COUCHDB_URL || 'http://localhost:5984';
|
|
const couchdbUser = process.env.VITE_COUCHDB_USER || 'admin';
|
|
const couchdbPassword = process.env.VITE_COUCHDB_PASSWORD || 'password';
|
|
|
|
this.baseUrl = couchdbUrl;
|
|
this.auth = btoa(`${couchdbUser}:${couchdbPassword}`);
|
|
|
|
// Initialize databases
|
|
this.initializeDatabases();
|
|
}
|
|
|
|
private async initializeDatabases(): Promise<void> {
|
|
const databases = [
|
|
'users',
|
|
'medications',
|
|
'settings',
|
|
'taken_doses',
|
|
'reminders',
|
|
];
|
|
|
|
for (const dbName of databases) {
|
|
try {
|
|
await this.createDatabaseIfNotExists(dbName);
|
|
} catch (error) {
|
|
console.error(`Failed to initialize database ${dbName}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async createDatabaseIfNotExists(dbName: string): Promise<void> {
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/${dbName}`, {
|
|
method: 'HEAD',
|
|
headers: {
|
|
Authorization: `Basic ${this.auth}`,
|
|
},
|
|
});
|
|
|
|
if (response.status === 404) {
|
|
// Database doesn't exist, create it
|
|
const createResponse = await fetch(`${this.baseUrl}/${dbName}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
Authorization: `Basic ${this.auth}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!createResponse.ok) {
|
|
throw new Error(`Failed to create database ${dbName}`);
|
|
}
|
|
|
|
console.warn(`✅ Created CouchDB database: ${dbName}`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error checking/creating database ${dbName}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async makeRequest(
|
|
method: string,
|
|
path: string,
|
|
body?: Record<string, unknown>
|
|
): Promise<Record<string, unknown>> {
|
|
const url = `${this.baseUrl}${path}`;
|
|
const options: RequestInit = {
|
|
method,
|
|
headers: {
|
|
Authorization: `Basic ${this.auth}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
};
|
|
|
|
if (body) {
|
|
options.body = JSON.stringify(body);
|
|
}
|
|
|
|
const response = await fetch(url, options);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new CouchDBError(`CouchDB error: ${errorText}`, response.status);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
private async getDoc<T extends CouchDBDocument>(
|
|
dbName: string,
|
|
id: string
|
|
): Promise<T | null> {
|
|
try {
|
|
const doc = await this.makeRequest('GET', `/${dbName}/${id}`);
|
|
return doc as T;
|
|
} catch (error) {
|
|
if (error instanceof CouchDBError && error.status === 404) {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async putDoc<T extends CouchDBDocument>(
|
|
dbName: string,
|
|
doc: Omit<T, '_rev'> & { _rev?: string }
|
|
): Promise<T> {
|
|
const response = await this.makeRequest(
|
|
'PUT',
|
|
`/${dbName}/${doc._id}`,
|
|
doc
|
|
);
|
|
return { ...doc, _rev: response.rev } as T;
|
|
}
|
|
|
|
private async query<T>(
|
|
dbName: string,
|
|
selector: Record<string, unknown>
|
|
): Promise<T[]> {
|
|
const response = await this.makeRequest('POST', `/${dbName}/_find`, {
|
|
selector,
|
|
limit: 1000,
|
|
});
|
|
return response.docs as T[];
|
|
}
|
|
|
|
// User Management Methods
|
|
async findUserByUsername(username: string): Promise<User | null> {
|
|
const users = await this.query<User>('users', { username });
|
|
return users[0] || null;
|
|
}
|
|
|
|
async findUserByEmail(email: string): Promise<User | null> {
|
|
const users = await this.query<User>('users', { email });
|
|
return users[0] || null;
|
|
}
|
|
|
|
async createUser(username: string): Promise<User> {
|
|
const existingUser = await this.findUserByUsername(username);
|
|
if (existingUser) {
|
|
throw new CouchDBError('User already exists', 409);
|
|
}
|
|
|
|
const newUser: Omit<User, '_rev'> = { _id: uuidv4(), username };
|
|
return this.putDoc<User>('users', newUser);
|
|
}
|
|
|
|
async createUserWithPassword(
|
|
email: string,
|
|
password: string,
|
|
username?: string
|
|
): Promise<User> {
|
|
const existingUser = await this.findUserByEmail(email);
|
|
if (existingUser) {
|
|
throw new CouchDBError('User already exists', 409);
|
|
}
|
|
|
|
const newUser: Omit<User, '_rev'> = {
|
|
_id: uuidv4(),
|
|
username: username || email.split('@')[0],
|
|
email,
|
|
password,
|
|
emailVerified: false,
|
|
status: AccountStatus.PENDING,
|
|
role: UserRole.USER,
|
|
createdAt: new Date(),
|
|
lastLoginAt: new Date(),
|
|
};
|
|
|
|
return this.putDoc<User>('users', newUser);
|
|
}
|
|
|
|
async createUserFromOAuth(userData: {
|
|
email: string;
|
|
username: string;
|
|
avatar?: string;
|
|
}): Promise<User> {
|
|
const existingUser = await this.findUserByEmail(userData.email);
|
|
if (existingUser) {
|
|
throw new CouchDBError('User already exists', 409);
|
|
}
|
|
|
|
const newUser: Omit<User, '_rev'> = {
|
|
_id: uuidv4(),
|
|
username: userData.username,
|
|
email: userData.email,
|
|
avatar: userData.avatar,
|
|
emailVerified: true,
|
|
status: AccountStatus.ACTIVE,
|
|
role: UserRole.USER,
|
|
createdAt: new Date(),
|
|
lastLoginAt: new Date(),
|
|
};
|
|
|
|
return this.putDoc<User>('users', newUser);
|
|
}
|
|
|
|
async getUserById(id: string): Promise<User | null> {
|
|
return this.getDoc<User>('users', id);
|
|
}
|
|
|
|
async updateUser(user: User): Promise<User> {
|
|
return this.putDoc<User>('users', user);
|
|
}
|
|
|
|
async deleteUser(id: string): Promise<void> {
|
|
const user = await this.getDoc<User>('users', id);
|
|
if (!user) {
|
|
throw new CouchDBError('User not found', 404);
|
|
}
|
|
|
|
await this.makeRequest('DELETE', `/users/${id}?rev=${user._rev}`);
|
|
}
|
|
|
|
// Medication Methods
|
|
async getMedications(userId: string): Promise<Medication[]> {
|
|
return this.query<Medication>('medications', { userId });
|
|
}
|
|
|
|
async createMedication(
|
|
medication: Omit<Medication, '_id' | '_rev'>
|
|
): Promise<Medication> {
|
|
const newMedication = { ...medication, _id: uuidv4() };
|
|
return this.putDoc<Medication>('medications', newMedication);
|
|
}
|
|
|
|
async updateMedication(medication: Medication): Promise<Medication> {
|
|
return this.putDoc<Medication>('medications', medication);
|
|
}
|
|
|
|
async deleteMedication(id: string): Promise<void> {
|
|
const medication = await this.getDoc<Medication>('medications', id);
|
|
if (!medication) {
|
|
throw new CouchDBError('Medication not found', 404);
|
|
}
|
|
|
|
await this.makeRequest(
|
|
'DELETE',
|
|
`/medications/${id}?rev=${medication._rev}`
|
|
);
|
|
}
|
|
|
|
// Settings Methods
|
|
async getSettings(userId: string): Promise<UserSettings> {
|
|
const settings = await this.getDoc<UserSettings>('settings', userId);
|
|
if (!settings) {
|
|
const defaultSettings: Omit<UserSettings, '_rev'> = {
|
|
_id: userId,
|
|
notificationsEnabled: true,
|
|
hasCompletedOnboarding: false,
|
|
};
|
|
return this.putDoc<UserSettings>('settings', defaultSettings);
|
|
}
|
|
return settings;
|
|
}
|
|
|
|
async updateSettings(settings: UserSettings): Promise<UserSettings> {
|
|
return this.putDoc<UserSettings>('settings', settings);
|
|
}
|
|
|
|
// Taken Doses Methods
|
|
async getTakenDoses(userId: string): Promise<TakenDoses> {
|
|
const doses = await this.getDoc<TakenDoses>('taken_doses', userId);
|
|
if (!doses) {
|
|
const defaultDoses: Omit<TakenDoses, '_rev'> = {
|
|
_id: userId,
|
|
doses: {},
|
|
};
|
|
return this.putDoc<TakenDoses>('taken_doses', defaultDoses);
|
|
}
|
|
return doses;
|
|
}
|
|
|
|
async updateTakenDoses(takenDoses: TakenDoses): Promise<TakenDoses> {
|
|
return this.putDoc<TakenDoses>('taken_doses', takenDoses);
|
|
}
|
|
|
|
// Reminder Methods
|
|
async getReminders(userId: string): Promise<CustomReminder[]> {
|
|
return this.query<CustomReminder>('reminders', { userId });
|
|
}
|
|
|
|
async createReminder(
|
|
reminder: Omit<CustomReminder, '_id' | '_rev'>
|
|
): Promise<CustomReminder> {
|
|
const newReminder = { ...reminder, _id: uuidv4() };
|
|
return this.putDoc<CustomReminder>('reminders', newReminder);
|
|
}
|
|
|
|
async updateReminder(reminder: CustomReminder): Promise<CustomReminder> {
|
|
return this.putDoc<CustomReminder>('reminders', reminder);
|
|
}
|
|
|
|
async deleteReminder(id: string): Promise<void> {
|
|
const reminder = await this.getDoc<CustomReminder>('reminders', id);
|
|
if (!reminder) {
|
|
throw new CouchDBError('Reminder not found', 404);
|
|
}
|
|
|
|
await this.makeRequest('DELETE', `/reminders/${id}?rev=${reminder._rev}`);
|
|
}
|
|
|
|
// Admin Methods
|
|
async getAllUsers(): Promise<User[]> {
|
|
return this.query<User>('users', {});
|
|
}
|
|
|
|
async updateUserStatus(userId: string, status: AccountStatus): Promise<User> {
|
|
const user = await this.getUserById(userId);
|
|
if (!user) {
|
|
throw new CouchDBError('User not found', 404);
|
|
}
|
|
|
|
const updatedUser = { ...user, status };
|
|
return this.updateUser(updatedUser);
|
|
}
|
|
|
|
async changeUserPassword(userId: string, newPassword: string): Promise<User> {
|
|
const user = await this.getUserById(userId);
|
|
if (!user) {
|
|
throw new CouchDBError('User not found', 404);
|
|
}
|
|
|
|
const updatedUser = { ...user, password: newPassword };
|
|
return this.updateUser(updatedUser);
|
|
}
|
|
|
|
// Cleanup Methods
|
|
async deleteAllUserData(userId: string): Promise<void> {
|
|
// Delete user medications, settings, doses, and reminders
|
|
const [medications, reminders] = await Promise.all([
|
|
this.getMedications(userId),
|
|
this.getReminders(userId),
|
|
]);
|
|
|
|
// Delete all user data
|
|
const deletePromises = [
|
|
...medications.map(med => this.deleteMedication(med._id)),
|
|
...reminders.map(rem => this.deleteReminder(rem._id)),
|
|
];
|
|
|
|
// Delete settings and taken doses
|
|
try {
|
|
const settings = await this.getDoc('settings', userId);
|
|
if (settings) {
|
|
deletePromises.push(
|
|
this.makeRequest(
|
|
'DELETE',
|
|
`/settings/${userId}?rev=${settings._rev}`
|
|
).then(() => undefined)
|
|
);
|
|
}
|
|
} catch (_error) {
|
|
// Settings might not exist
|
|
}
|
|
|
|
try {
|
|
const takenDoses = await this.getDoc('taken_doses', userId);
|
|
if (takenDoses) {
|
|
deletePromises.push(
|
|
this.makeRequest(
|
|
'DELETE',
|
|
`/taken_doses/${userId}?rev=${takenDoses._rev}`
|
|
).then(() => undefined)
|
|
);
|
|
}
|
|
} catch (_error) {
|
|
// Taken doses might not exist
|
|
}
|
|
|
|
await Promise.all(deletePromises);
|
|
|
|
// Finally delete the user
|
|
await this.deleteUser(userId);
|
|
}
|
|
}
|