nextjs-hasura-auth 0.1.2 → 0.1.3

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.
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.hashPassword = hashPassword;
16
+ exports.comparePassword = comparePassword;
17
+ exports.getOrCreateUserAndAccount = getOrCreateUserAndAccount;
18
+ const bcrypt_1 = __importDefault(require("bcrypt"));
19
+ const client_1 = require("@apollo/client"); // Import ApolloError
20
+ const debug_1 = __importDefault(require("./debug"));
21
+ const debug = (0, debug_1.default)('auth:db-utils');
22
+ const SALT_ROUNDS = 10;
23
+ /**
24
+ * Hashes a password using bcrypt.
25
+ * @param password The plain text password.
26
+ * @returns The hashed password.
27
+ */
28
+ function hashPassword(password) {
29
+ return __awaiter(this, void 0, void 0, function* () {
30
+ const hashedPassword = yield bcrypt_1.default.hash(password, SALT_ROUNDS);
31
+ debug('Password hashed successfully.');
32
+ return hashedPassword;
33
+ });
34
+ }
35
+ /**
36
+ * Compares a plain text password with a hash.
37
+ * @param password The plain text password.
38
+ * @param hash The hash to compare against.
39
+ * @returns True if the password matches the hash, false otherwise.
40
+ */
41
+ function comparePassword(password, hash) {
42
+ return __awaiter(this, void 0, void 0, function* () {
43
+ const isMatch = yield bcrypt_1.default.compare(password, hash);
44
+ debug(`Password comparison result: ${isMatch}`);
45
+ return isMatch;
46
+ });
47
+ }
48
+ /**
49
+ * Finds or creates a user and their associated account based on provider information.
50
+ * This function handles the core logic of linking OAuth/Credentials logins to Hasura users.
51
+ *
52
+ * @param client The initialized NHA Client instance.
53
+ * @param provider The OAuth provider name (e.g., 'google', 'credentials').
54
+ * @param providerAccountId The user's unique ID from the provider.
55
+ * @param profile Optional profile information from the provider (name, email, image).
56
+ * @returns The Hasura user object associated with the account.
57
+ * @throws Error if user/account processing fails.
58
+ */
59
+ function getOrCreateUserAndAccount(client, provider, providerAccountId, profile) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ var _a, _b, _c;
62
+ debug(`getOrCreateUserAndAccount called for provider: ${provider}, providerAccountId: ${providerAccountId}`);
63
+ // --- 1. Try to find the account ---
64
+ let existingUser = null;
65
+ try {
66
+ const accountResult = yield client.select({
67
+ table: 'accounts',
68
+ where: {
69
+ provider: { _eq: provider },
70
+ provider_account_id: { _eq: providerAccountId },
71
+ },
72
+ returning: [
73
+ { user: ['id', 'name', 'email', 'email_verified', 'image', 'password', 'created_at', 'updated_at', 'is_admin', 'hasura_role'] } // Removed extra backslashes
74
+ ],
75
+ limit: 1, // Optimization: we only need one
76
+ });
77
+ if (((_a = accountResult === null || accountResult === void 0 ? void 0 : accountResult.accounts) === null || _a === void 0 ? void 0 : _a.length) > 0 && accountResult.accounts[0].user) {
78
+ existingUser = accountResult.accounts[0].user;
79
+ debug(`Found existing account for ${provider}:${providerAccountId}. User ID: ${existingUser.id}`);
80
+ // Optionally update user profile info (name, image) from provider here if desired
81
+ // await client.update({ table: 'users', pk_columns: { id: existingUser.id }, _set: { name: profile.name, image: profile.image } });
82
+ return existingUser;
83
+ }
84
+ }
85
+ catch (error) {
86
+ debug(`Error searching for account ${provider}:${providerAccountId}:`, error);
87
+ throw new Error(`Failed to search for existing account: ${error.message}`);
88
+ }
89
+ debug(`No existing account found for ${provider}:${providerAccountId}. Proceeding to find/create user.`);
90
+ // --- 2. Try to find the user by email (if provided) ---
91
+ // Important: Only link if email is provided and preferably verified by the OAuth provider.
92
+ // For credentials, we find the user first in the `authorize` function.
93
+ if ((profile === null || profile === void 0 ? void 0 : profile.email) && provider !== 'credentials') { // Avoid linking for credentials here
94
+ try {
95
+ const userByEmailResult = yield client.select({
96
+ table: 'users',
97
+ where: { email: { _eq: profile.email } },
98
+ returning: ['id', 'name', 'email', 'email_verified', 'image', 'password', 'created_at', 'updated_at', 'is_admin', 'hasura_role'], // Removed extra backslashes
99
+ limit: 1,
100
+ });
101
+ if (((_b = userByEmailResult === null || userByEmailResult === void 0 ? void 0 : userByEmailResult.users) === null || _b === void 0 ? void 0 : _b.length) > 0) {
102
+ existingUser = userByEmailResult.users[0];
103
+ debug(`Found existing user by email ${profile.email}. User ID: ${existingUser.id}. Linking account.`);
104
+ // Link account to this existing user
105
+ yield client.insert({
106
+ table: 'accounts',
107
+ object: {
108
+ user_id: existingUser.id,
109
+ provider: provider,
110
+ provider_account_id: providerAccountId,
111
+ type: provider === 'credentials' ? 'credentials' : 'oauth', // Set type based on provider
112
+ },
113
+ returning: ['id'], // Removed extra backslashes
114
+ });
115
+ debug(`Account ${provider}:${providerAccountId} linked to user ${existingUser.id}.`);
116
+ return existingUser;
117
+ }
118
+ }
119
+ catch (error) {
120
+ // Handle potential duplicate account insertion error gracefully if needed
121
+ if (error instanceof client_1.ApolloError && error.message.includes('Uniqueness violation')) {
122
+ debug(`Account ${provider}:${providerAccountId} likely already linked during concurrent request. Attempting to refetch.`);
123
+ // Retry finding the account, as it might have been created concurrently
124
+ return getOrCreateUserAndAccount(client, provider, providerAccountId, profile);
125
+ }
126
+ else {
127
+ debug(`Error searching for user by email ${profile.email} or linking account:`, error);
128
+ throw new Error(`Failed to process user by email or link account: ${error.message}`);
129
+ }
130
+ }
131
+ }
132
+ // --- 3. Create new user and account ---
133
+ debug(`No existing user found by email or account link. Creating new user and account for ${provider}:${providerAccountId}.`);
134
+ try {
135
+ // We need to insert the user first, then the account linking to it.
136
+ // Hasura doesn't directly support nested inserts with linking back in the same mutation easily via the generator.
137
+ const newUserInput = {
138
+ name: profile === null || profile === void 0 ? void 0 : profile.name,
139
+ email: profile === null || profile === void 0 ? void 0 : profile.email, // Will be null for credentials initially, set later?
140
+ image: profile === null || profile === void 0 ? void 0 : profile.image,
141
+ hasura_role: 'user', // Default role
142
+ // email_verified: profile?.email_verified ? new Date().toISOString() : null, // Need verification logic for OAuth
143
+ };
144
+ // For credentials, the user is created *after* email verification, but we handle it here for OAuth
145
+ const newUserResult = yield client.insert({
146
+ table: 'users',
147
+ object: newUserInput,
148
+ // Return all fields needed for the session/JWT
149
+ returning: ['id', 'name', 'email', 'email_verified', 'image', 'password', 'created_at', 'updated_at', 'is_admin', 'hasura_role'], // Removed extra backslashes
150
+ });
151
+ if (!((_c = newUserResult === null || newUserResult === void 0 ? void 0 : newUserResult.insert_users_one) === null || _c === void 0 ? void 0 : _c.id)) {
152
+ throw new Error('Failed to create new user or retrieve its ID.');
153
+ }
154
+ const newUser = newUserResult.insert_users_one;
155
+ debug(`New user created with ID: ${newUser.id}`);
156
+ // Now create the account linked to the new user
157
+ yield client.insert({
158
+ table: 'accounts',
159
+ object: {
160
+ user_id: newUser.id,
161
+ provider: provider,
162
+ provider_account_id: providerAccountId,
163
+ type: provider === 'credentials' ? 'credentials' : 'oauth', // Set type based on provider
164
+ },
165
+ returning: ['id'], // Removed extra backslashes
166
+ });
167
+ debug(`Account ${provider}:${providerAccountId} created and linked to new user ${newUser.id}.`);
168
+ return newUser;
169
+ }
170
+ catch (error) {
171
+ debug('Error creating new user or account:', error);
172
+ // Handle potential duplicate user email error more gracefully if necessary
173
+ if (error instanceof client_1.ApolloError && error.message.includes('Uniqueness violation') && error.message.includes('users_email_key')) {
174
+ debug(`User with email ${profile === null || profile === void 0 ? void 0 : profile.email} likely already exists. Attempting to find and link.`);
175
+ // If user creation failed due to duplicate email, retry finding by email and linking account
176
+ if ((profile === null || profile === void 0 ? void 0 : profile.email) && provider !== 'credentials') {
177
+ return getOrCreateUserAndAccount(client, provider, providerAccountId, profile);
178
+ }
179
+ }
180
+ throw new Error(`Failed to create new user/account: ${error.message}`);
181
+ }
182
+ });
183
+ }
@@ -1,54 +1,54 @@
1
- import { ApolloClient, ApolloError, FetchResult, Observable, OperationVariables, useQuery as useApolloQuery, useSubscription as useApolloSubscription } from '@apollo/client';
2
- import { GenerateOptions, GenerateResult } from './generator';
3
- /**
4
- * Represents the result structure from Apollo hooks, including potential errors.
5
- */
6
- export interface HookResult<TData = any> {
7
- loading: boolean;
8
- data?: TData;
9
- error?: ApolloError;
10
- [key: string]: any;
11
- generatedQuery?: GenerateResult['query'];
12
- generatedVariables?: GenerateResult['variables'];
1
+ import { QueryHookOptions as ApolloQueryHookOptions, SubscriptionHookOptions as ApolloSubscriptionHookOptions, MutationHookOptions as ApolloMutationHookOptions, MutationTuple, QueryResult, SubscriptionResult, OperationVariables, ApolloClient, NormalizedCacheObject, FetchResult, Observable } from '@apollo/client';
2
+ import { GenerateOptions } from './generator';
3
+ interface ClientMethodOptions extends Omit<GenerateOptions, 'operation'> {
4
+ role?: string;
13
5
  }
14
6
  export declare class Client {
15
7
  private apolloClient;
16
8
  private generate;
17
- constructor(apolloClient: ApolloClient<any>);
9
+ constructor(apolloClient: ApolloClient<NormalizedCacheObject>);
18
10
  /**
19
11
  * Executes a GraphQL query (select operation).
20
- * @param options - Options for generating the query.
12
+ * @param options - Options for generating the query, including an optional `role`.
21
13
  * @returns Promise resolving with the query result data.
22
14
  * @throws ApolloError if the query fails or returns GraphQL errors.
23
15
  */
24
- select<TData = any>(options: Omit<GenerateOptions, 'operation'>): Promise<TData>;
16
+ select<TData = any>(options: ClientMethodOptions): Promise<TData>;
25
17
  /**
26
18
  * Executes a GraphQL insert mutation.
27
- * @param options - Options for generating the mutation.
19
+ * @param options - Options for generating the mutation, including an optional `role`.
28
20
  * @returns Promise resolving with the mutation result data.
29
21
  * @throws ApolloError if the mutation fails or returns GraphQL errors.
30
22
  */
31
- insert<TData = any>(options: Omit<GenerateOptions, 'operation'>): Promise<TData>;
23
+ insert<TData = any>(options: ClientMethodOptions): Promise<TData>;
32
24
  /**
33
25
  * Executes a GraphQL update mutation.
34
- * @param options - Options for generating the mutation.
26
+ * @param options - Options for generating the mutation, including an optional `role`.
35
27
  * @returns Promise resolving with the mutation result data.
36
28
  * @throws ApolloError if the mutation fails or returns GraphQL errors.
37
29
  */
38
- update<TData = any>(options: Omit<GenerateOptions, 'operation'>): Promise<TData>;
30
+ update<TData = any>(options: ClientMethodOptions): Promise<TData>;
39
31
  /**
40
32
  * Executes a GraphQL delete mutation.
41
- * @param options - Options for generating the mutation.
33
+ * @param options - Options for generating the mutation, including an optional `role`.
42
34
  * @returns Promise resolving with the mutation result data.
43
35
  * @throws ApolloError if the mutation fails or returns GraphQL errors.
44
36
  */
45
- delete<TData = any>(options: Omit<GenerateOptions, 'operation'>): Promise<TData>;
37
+ delete<TData = any>(options: ClientMethodOptions): Promise<TData>;
46
38
  /**
47
39
  * Initiates a GraphQL subscription.
48
- * @param options - Options for generating the subscription.
40
+ * Note: Role support via context might not work for WebSockets depending on Apollo Link setup.
41
+ * Role is typically set during WebSocket connection establishment.
42
+ * @param options - Options for generating the subscription, including an optional `role`.
49
43
  * @returns An Observable for the subscription results.
50
44
  */
51
- subscribe<TData = any, TVariables extends OperationVariables = OperationVariables>(options: Omit<GenerateOptions, 'operation'>): Observable<FetchResult<TData>>;
45
+ subscribe<TData = any, TVariables extends OperationVariables = OperationVariables>(options: ClientMethodOptions): Observable<FetchResult<TData>>;
46
+ useSubscription<TData = any, TVariables extends OperationVariables = OperationVariables>(generateOptions: ClientGeneratorOptions, hookOptions?: SubscriptionHookOptions<TData, TVariables> & {
47
+ variables?: TVariables;
48
+ }): SubscriptionResult<TData, TVariables>;
49
+ useQuery<TData = any, TVariables extends OperationVariables = OperationVariables>(generateOptions: ClientGeneratorOptions, hookOptions?: QueryHookOptions<TData, TVariables> & {
50
+ variables?: TVariables;
51
+ }): QueryResult<TData, TVariables>;
52
52
  }
53
53
  /**
54
54
  * Hook to get the Apollo Client instance.
@@ -57,26 +57,25 @@ export declare class Client {
57
57
  * @returns The ApolloClient instance.
58
58
  * @throws Error if no client is found.
59
59
  */
60
- export declare function useClient(providedClient?: ApolloClient<any> | null): ApolloClient<any>;
61
- /**
62
- * Hook to perform a GraphQL query using the generator.
63
- * @template TData - The expected data type.
64
- * @param options - Options for the generator (operation is automatically set to 'query').
65
- * @param providedClient - Optional ApolloClient instance to use instead of context.
66
- * @param hookOptions - Optional additional options passed directly to Apollo's useQuery hook.
67
- * @returns An object containing loading state, data, error, and the generated query/variables.
68
- */
69
- export declare function useQuery<TData = any>(options: Omit<GenerateOptions, 'operation'>, providedClient?: ApolloClient<any> | null, hookOptions?: Omit<Parameters<typeof useApolloQuery>[1], 'variables' | 'client' | 'query'>): HookResult<TData>;
70
- /** Alias for useQuery */
60
+ export declare function useClient(providedClient?: ApolloClient<any> | null): Client;
61
+ type BaseHookOptions = {
62
+ role?: string;
63
+ };
64
+ type QueryHookOptions<TData, TVariables extends OperationVariables> = BaseHookOptions & Omit<ApolloQueryHookOptions<TData, TVariables>, 'query' | 'variables' | 'context'>;
65
+ type SubscriptionHookOptions<TData, TVariables extends OperationVariables> = BaseHookOptions & Omit<ApolloSubscriptionHookOptions<TData, TVariables>, 'query' | 'variables' | 'context'>;
66
+ type MutationHookOptions<TData, TVariables extends OperationVariables> = BaseHookOptions & Omit<ApolloMutationHookOptions<TData, TVariables>, 'mutation' | 'variables' | 'context'>;
67
+ type ClientGeneratorOptions = Omit<GenerateOptions, 'operation'>;
68
+ export declare function useQuery<TData = any, TVariables extends OperationVariables = OperationVariables>(generateOptions: ClientGeneratorOptions, hookOptions?: QueryHookOptions<TData, TVariables> & {
69
+ variables?: TVariables;
70
+ }): QueryResult<TData, TVariables>;
71
+ export declare function useSubscription<TData = any, TVariables extends OperationVariables = OperationVariables>(generateOptions: ClientGeneratorOptions, hookOptions?: SubscriptionHookOptions<TData, TVariables> & {
72
+ variables?: TVariables;
73
+ }): SubscriptionResult<TData, TVariables>;
74
+ export declare function useMutation<TData = any, TVariables extends OperationVariables = OperationVariables>(generateOptions: GenerateOptions, // Use full GenerateOptions for mutations
75
+ hookOptions?: MutationHookOptions<TData, TVariables>): MutationTuple<TData, TVariables>;
71
76
  export declare const useSelect: typeof useQuery;
72
- /**
73
- * Hook to perform a GraphQL subscription using the generator.
74
- * @template TData - The expected data type.
75
- * @param options - Options for the generator (operation is automatically set to 'subscription').
76
- * @param providedClient - Optional ApolloClient instance to use instead of context.
77
- * @param hookOptions - Optional additional options passed directly to Apollo's useSubscription hook.
78
- * @returns An object containing loading state, data, error, and the generated query/variables.
79
- */
80
- export declare function useSubscription<TData = any>(options: Omit<GenerateOptions, 'operation'>, providedClient?: ApolloClient<any> | null, hookOptions?: Omit<Parameters<typeof useApolloSubscription>[1], 'variables' | 'client' | 'query'>): HookResult<TData>;
81
- /** Alias for useSubscription */
77
+ export declare const useInsert: (genOpts: Omit<GenerateOptions, "operation">, hookOpts?: MutationHookOptions<any, any>) => MutationTuple<any, any>;
78
+ export declare const useUpdate: (genOpts: Omit<GenerateOptions, "operation">, hookOpts?: MutationHookOptions<any, any>) => MutationTuple<any, any>;
79
+ export declare const useDelete: (genOpts: Omit<GenerateOptions, "operation">, hookOpts?: MutationHookOptions<any, any>) => MutationTuple<any, any>;
82
80
  export declare const useSubscribe: typeof useSubscription;
81
+ export {};