nextjs-hasura-auth 0.1.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 ADDED
@@ -0,0 +1,154 @@
1
+ # Next.js Hasura Authentication Boilerplate
2
+
3
+ This project provides a robust starting point for building applications using Next.js (App Router), Hasura, and strong authentication patterns. It features JWT-based authentication with NextAuth.js, a secure GraphQL proxy to Hasura, direct WebSocket support for subscriptions, and a powerful dynamic query generator.
4
+
5
+ [![Generator Documentation](https://img.shields.io/badge/Generator%20Docs-MD-blue)](GENERATOR.md) [![Apollo Client Documentation](https://img.shields.io/badge/Apollo%20Client%20Docs-MD-orange)](APOLLO.md)
6
+
7
+ See [`GENERATOR.md`](GENERATOR.md) for detailed documentation on the dynamic GraphQL query generator, which simplifies creating queries, mutations, and subscriptions based on your Hasura schema.
8
+
9
+ See [`APOLLO.md`](APOLLO.md) for details on the configured Apollo Client instance and how it handles authenticated requests and subscriptions.
10
+
11
+ ## ✨ Features Checklist
12
+
13
+ **Implemented:**
14
+
15
+ * [x] **Secure Hasura Proxy:** An integrated API route acts as a proxy to Hasura, securely handling requests using the user's session JWT while hiding the Hasura Admin Secret from the client.
16
+ * [x] **Credentials Authentication:** User authentication implemented using NextAuth.js with a login/password (Credentials) provider.
17
+ * [x] **Unified Apollo Client:** A configured Apollo Client instance handles both authenticated HTTP requests (via the proxy) and direct, authenticated WebSocket connections for subscriptions.
18
+ * [x] **Dynamic Query Generator:** A versatile query generator (`lib/generator.ts`) allows dynamic creation of GraphQL operations based on options and schema, suitable for client/server use.
19
+ * [x] **WebSocket Authentication:** Real-time subscriptions connect directly to Hasura via WebSockets, authenticated using the user's session JWT.
20
+
21
+ **Planned / Future Ideas:**
22
+
23
+ * [ ] **Convenience Hooks:** Create easy-to-use React hooks (`useQuery`, `useSubscription`, potentially a `useCRUD` hook/class) that integrate the `Generator` with Apollo Client for streamlined data fetching in components.
24
+ * [ ] **Multi-Platform Builds:** Native builders for Android, iOS, MacOS, Windows, Linux, Oculus (e.g., using Tauri, Capacitor, or Electron).
25
+ * [ ] **Unique Environment Builders:** Specific builds for Chrome Extensions, Firefox Extensions, and VSCode Extensions (including custom UI elements).
26
+ * [ ] Additional Authentication Providers (OAuth: Google, GitHub, etc.).
27
+ * [ ] Role-based access control examples.
28
+ * [ ] Advanced caching strategies.
29
+ * [ ] Comprehensive end-to-end testing setup.
30
+
31
+ ## 🚀 Core Concepts
32
+
33
+ ### 1. Authentication (NextAuth.js)
34
+
35
+ * Uses `NextAuth.js` for handling authentication flow.
36
+ * Configured with the **Credentials provider** for email/password login (see `pages/api/auth/[...nextauth].ts` or `app/api/auth/[...nextauth]/route.ts`).
37
+ * Manages sessions using **JWT**. The JWT contains essential user information and Hasura claims.
38
+ * Provides standard pages/routes for login, logout, and potentially signup.
39
+
40
+ ### 2. Hasura Integration
41
+
42
+ Interaction with the Hasura GraphQL Engine is handled in two primary ways:
43
+
44
+ * **HTTP Requests (Queries/Mutations via Proxy):**
45
+ * Client-side GraphQL queries and mutations are sent to a Next.js API route (`/api/graphql-proxy` or similar).
46
+ * This proxy route retrieves the user's JWT from their session.
47
+ * It then forwards the GraphQL request to the actual Hasura endpoint (`HASURA_GRAPHQL_URL`).
48
+ * Crucially, the proxy uses the **`HASURA_ADMIN_SECRET`** to communicate with Hasura but includes the user's details (like `x-hasura-user-id`, `x-hasura-default-role` derived from the JWT) as session variables in the request headers.
49
+ * This ensures Hasura applies the correct permissions for the logged-in user while keeping the powerful `admin_secret` completely hidden from the browser.
50
+ * **WebSocket Connections (Subscriptions):**
51
+ * For real-time data via GraphQL Subscriptions, the client establishes a direct WebSocket connection to the Hasura endpoint (`wss://...`).
52
+ * Authentication for the WebSocket connection is handled by passing the user's session JWT within the `connectionParams`. Hasura verifies this token to authorize the subscription.
53
+ * The `components/auth/SocketAuthStatus.tsx` component likely demonstrates checking the status of this authenticated connection.
54
+
55
+ ### 3. Apollo Client
56
+
57
+ * A pre-configured Apollo Client instance (`lib/apolloClient.ts` or similar) is set up to manage GraphQL data fetching.
58
+ * It intelligently handles both:
59
+ * **HTTP Link:** Points to the Next.js GraphQL proxy (`/api/graphql-proxy`) for queries and mutations.
60
+ * **WebSocket Link:** Connects directly to Hasura's WebSocket endpoint for subscriptions, including logic to pass the authentication token.
61
+ * The client can be used both client-side (with React hooks) and server-side (for SSR/SSG data fetching).
62
+
63
+ ### 4. Dynamic Query Generation (`GENERATOR.md`)
64
+
65
+ * The core `Generator` function in `lib/generator.ts` allows you to build complex GraphQL operations dynamically based on a simple options object and your `schema.json`.
66
+ * This avoids writing lengthy GraphQL query strings manually.
67
+ * See [`GENERATOR.md`](GENERATOR.md) for full usage details and examples.
68
+ * *Convenience hooks (like `useQuery`, `useSubscription`, `useCRUD`) are planned to further simplify using the generator within React components.*
69
+
70
+ ## 📁 Project Structure (Key Directories)
71
+
72
+ ```
73
+ .
74
+ ├── app/ # Next.js App Router (Pages, Layouts, API Routes)
75
+ │ ├── api/ # API routes (e.g., auth, graphql-proxy)
76
+ │ └── (main)/ # Main application pages/routes
77
+ ├── components/ # Shared React components
78
+ │ ├── auth/ # Authentication-related components
79
+ │ └── ui/ # UI primitives (likely shadcn/ui)
80
+ ├── lib/ # Core logic, utilities, client configurations
81
+ │ ├── apolloClient.ts # Apollo Client setup
82
+ │ ├── auth.ts # Authentication utilities/configs
83
+ │ ├── generator.ts # GraphQL Query Generator
84
+ │ ├── debug.ts # Debug utility
85
+ │ └── ...
86
+ ├── public/ # Static assets
87
+ ├── styles/ # Global styles
88
+ ├── .env.local # Environment variables (Gitignored)
89
+ ├── GENERATOR.md # Query Generator Documentation
90
+ ├── schema.json # Hasura GraphQL schema (for Generator)
91
+ ├── next.config.js # Next.js configuration
92
+ ├── package.json # Project dependencies and scripts
93
+ └── tsconfig.json # TypeScript configuration
94
+ ```
95
+
96
+ ## 🛠️ Getting Started
97
+
98
+ 1. **Clone the repository:**
99
+ ```bash
100
+ git clone <repository-url>
101
+ cd <repository-directory>
102
+ ```
103
+
104
+ 2. **Install dependencies:**
105
+ ```bash
106
+ npm install
107
+ # or
108
+ yarn install
109
+ # or
110
+ pnpm install
111
+ ```
112
+
113
+ 3. **Set up Environment Variables:**
114
+ Create a `.env.local` file in the root directory and add the following variables:
115
+
116
+ ```env
117
+ # Hasura Configuration
118
+ NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT="<your-hasura-graphql-url>" # e.g., https://your-project.hasura.app/v1/graphql
119
+ NEXT_PUBLIC_HASURA_WS_ENDPOINT="<your-hasura-websocket-url>" # e.g., wss://your-project.hasura.app/v1/graphql
120
+ HASURA_ADMIN_SECRET="<your-hasura-admin-secret>"
121
+
122
+ # NextAuth.js Configuration
123
+ NEXTAUTH_URL="<your-deployment-url>" # e.g., http://localhost:3000 for local dev
124
+ NEXTAUTH_SECRET="<generate-a-strong-secret>" # Generate with: openssl rand -base64 32
125
+
126
+ # Other (if needed by your setup)
127
+ # DATABASE_URL="..."
128
+ ```
129
+ * Replace `<...>` with your actual Hasura credentials and secrets.
130
+ * Ensure `NEXTAUTH_URL` points to your application's base URL.
131
+
132
+ 4. **Update Hasura Schema:**
133
+ Make sure the `schema.json` file in the root is up-to-date with your Hasura instance's schema. You might need to fetch this from Hasura if you've made changes.
134
+
135
+ 5. **Run the development server:**
136
+ ```bash
137
+ npm run dev
138
+ # or
139
+ yarn dev
140
+ # or
141
+ pnpm dev
142
+ ```
143
+
144
+ 6. Open [http://localhost:3000](http://localhost:3000) (or your `NEXTAUTH_URL`) in your browser.
145
+
146
+ ## Environment Variables Summary
147
+
148
+ * `NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT`: Public URL for Hasura GraphQL HTTP endpoint.
149
+ * `NEXT_PUBLIC_HASURA_WS_ENDPOINT`: Public URL for Hasura GraphQL WebSocket endpoint.
150
+ * `HASURA_ADMIN_SECRET`: Your Hasura admin secret (kept server-side).
151
+ * `NEXTAUTH_URL`: The canonical URL of your Next.js application.
152
+ * `NEXTAUTH_SECRET`: A secret key used by NextAuth.js to sign JWTs, etc.
153
+
154
+ Let me know what you think!
@@ -0,0 +1,45 @@
1
+ import { ApolloClient } from '@apollo/client';
2
+ /**
3
+ * Get JWT secret from environment variables
4
+ * @returns {Uint8Array} Secret key for JWT signing
5
+ */
6
+ export declare const getJwtSecret: () => Uint8Array;
7
+ interface ApolloOptions {
8
+ url?: string;
9
+ ws?: boolean;
10
+ token?: string;
11
+ secret?: string;
12
+ }
13
+ /**
14
+ * Create Apollo Client
15
+ *
16
+ * @param {Object} options - Options for creating the client
17
+ * @param {boolean} options.ws - Use WebSocket connection
18
+ * @param {string} options.token - JWT token for authorization
19
+ * @param {string} options.secret - Admin secret for Hasura
20
+ * @returns {ApolloClient} Apollo Client
21
+ */
22
+ export declare function createClient(options?: ApolloOptions): ApolloClient<import("@apollo/client").NormalizedCacheObject>;
23
+ /**
24
+ * Get or create Apollo client instance
25
+ * @param options Client options
26
+ * @returns Apollo client instance
27
+ */
28
+ export declare function getClient(options?: {}): ApolloClient<any>;
29
+ /**
30
+ * React hook to get Apollo client instance
31
+ * @returns Apollo client instance
32
+ */
33
+ export declare function useClient(options: ApolloOptions): ApolloClient<import("@apollo/client").NormalizedCacheObject>;
34
+ /**
35
+ * Check connection to Hasura GraphQL endpoint
36
+ * @returns {Promise<boolean>} True if connection is successful
37
+ */
38
+ export declare function checkConnection(client?: ApolloClient<any>): Promise<boolean>;
39
+ declare const _default: {
40
+ createClient: typeof createClient;
41
+ getClient: typeof getClient;
42
+ getJwtSecret: () => Uint8Array;
43
+ checkConnection: typeof checkConnection;
44
+ };
45
+ export default _default;
@@ -0,0 +1,206 @@
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.getJwtSecret = void 0;
16
+ exports.createClient = createClient;
17
+ exports.getClient = getClient;
18
+ exports.useClient = useClient;
19
+ exports.checkConnection = checkConnection;
20
+ const client_1 = require("@apollo/client");
21
+ const context_1 = require("@apollo/client/link/context");
22
+ const utilities_1 = require("@apollo/client/utilities");
23
+ const subscriptions_1 = require("@apollo/client/link/subscriptions");
24
+ const graphql_ws_1 = require("graphql-ws");
25
+ const cross_fetch_1 = __importDefault(require("cross-fetch"));
26
+ const debug_1 = __importDefault(require("./debug"));
27
+ const react_1 = require("react");
28
+ // Create a debug logger for this module
29
+ const debug = (0, debug_1.default)('apollo');
30
+ // Determine if running on client
31
+ const isClient = typeof window !== 'undefined';
32
+ /**
33
+ * Get JWT secret from environment variables
34
+ * @returns {Uint8Array} Secret key for JWT signing
35
+ */
36
+ const getJwtSecret = () => {
37
+ try {
38
+ const jwtSecret = process.env.HASURA_JWT_SECRET || '{"type":"HS256","key":"your-secret-key"}';
39
+ // Parse JWT configuration (may be in JSON format)
40
+ let secretKey;
41
+ try {
42
+ const jwtConfig = typeof jwtSecret === 'string' ? JSON.parse(jwtSecret) : jwtSecret;
43
+ secretKey = jwtConfig.key;
44
+ if (!secretKey) {
45
+ throw new Error('JWT key not found in configuration');
46
+ }
47
+ }
48
+ catch (e) {
49
+ // If failed to parse as JSON, use as string
50
+ secretKey = jwtSecret;
51
+ }
52
+ // Convert key to Uint8Array (required for jose)
53
+ return new TextEncoder().encode(secretKey);
54
+ }
55
+ catch (error) {
56
+ debug('apollo', '❌ Error getting JWT secret:', error);
57
+ throw error;
58
+ }
59
+ };
60
+ exports.getJwtSecret = getJwtSecret;
61
+ /**
62
+ * Create Apollo Client
63
+ *
64
+ * @param {Object} options - Options for creating the client
65
+ * @param {boolean} options.ws - Use WebSocket connection
66
+ * @param {string} options.token - JWT token for authorization
67
+ * @param {string} options.secret - Admin secret for Hasura
68
+ * @returns {ApolloClient} Apollo Client
69
+ */
70
+ function createClient(options = {}) {
71
+ // Default values
72
+ const { url = process.env.NEXT_PUBLIC_HASURA_GRAPHQL_URL, ws = false, token = undefined, secret = process.env.HASURA_ADMIN_SECRET } = options;
73
+ if (!url) {
74
+ throw new Error('❌ options.url or NEXT_PUBLIC_HASURA_GRAPHQL_URL not defined');
75
+ }
76
+ debug('apollo', '🔌 Creating Apollo client with endpoint:', url);
77
+ // HTTP connection without authorization
78
+ const publicHttpLink = new client_1.HttpLink({
79
+ uri: url,
80
+ fetch: cross_fetch_1.default,
81
+ });
82
+ // Create auth link with JWT token
83
+ const authLink = token
84
+ ? (0, context_1.setContext)((_, { headers }) => {
85
+ return {
86
+ headers: Object.assign(Object.assign({}, headers), { Authorization: `Bearer ${token}` })
87
+ };
88
+ })
89
+ : client_1.ApolloLink.from([]);
90
+ // Choose link based on token or secret availability
91
+ let httpLink;
92
+ if (token) {
93
+ // If token provided, use it for authorization
94
+ httpLink = client_1.ApolloLink.from([authLink, publicHttpLink]);
95
+ }
96
+ else if (secret) {
97
+ // If no token but admin secret exists, use it
98
+ httpLink = new client_1.HttpLink({
99
+ uri: url,
100
+ fetch: cross_fetch_1.default,
101
+ headers: {
102
+ 'x-hasura-admin-secret': secret || ''
103
+ }
104
+ });
105
+ ;
106
+ }
107
+ else {
108
+ // If neither token nor secret, use public access
109
+ httpLink = publicHttpLink;
110
+ }
111
+ // Create link splitter for queries
112
+ let splitLink = httpLink;
113
+ // If WebSocket connection needed and we're in browser
114
+ if (ws && isClient) {
115
+ const wsEndpoint = url.replace('http', 'ws').replace('https', 'wss');
116
+ // Configure connection parameters
117
+ const connectionParams = {};
118
+ if (token) {
119
+ // If token provided, use it for WebSocket authorization
120
+ connectionParams.headers = {
121
+ Authorization: `Bearer ${token}`,
122
+ };
123
+ }
124
+ else if (secret) {
125
+ // If no token but admin secret exists, use it
126
+ connectionParams.headers = {
127
+ 'x-hasura-admin-secret': secret,
128
+ };
129
+ }
130
+ // Create WebSocket client
131
+ const wsLink = new subscriptions_1.GraphQLWsLink((0, graphql_ws_1.createClient)({
132
+ url: wsEndpoint,
133
+ connectionParams: () => connectionParams
134
+ }));
135
+ // Split requests: WebSocket for subscriptions, HTTP for others
136
+ splitLink = (0, client_1.split)(({ query }) => {
137
+ const definition = (0, utilities_1.getMainDefinition)(query);
138
+ return (definition.kind === 'OperationDefinition' &&
139
+ definition.operation === 'subscription');
140
+ }, wsLink, httpLink);
141
+ }
142
+ // Create Apollo Client
143
+ return new client_1.ApolloClient({
144
+ link: splitLink,
145
+ cache: new client_1.InMemoryCache(),
146
+ defaultOptions: {
147
+ watchQuery: {
148
+ fetchPolicy: 'network-only',
149
+ errorPolicy: 'all',
150
+ },
151
+ query: {
152
+ fetchPolicy: 'network-only',
153
+ errorPolicy: 'all',
154
+ },
155
+ mutate: {
156
+ errorPolicy: 'all',
157
+ },
158
+ }
159
+ });
160
+ }
161
+ // Default client instance
162
+ let clientInstance = null;
163
+ /**
164
+ * Get or create Apollo client instance
165
+ * @param options Client options
166
+ * @returns Apollo client instance
167
+ */
168
+ function getClient(options = {}) {
169
+ if (!clientInstance) {
170
+ clientInstance = createClient(options);
171
+ }
172
+ return clientInstance;
173
+ }
174
+ /**
175
+ * React hook to get Apollo client instance
176
+ * @returns Apollo client instance
177
+ */
178
+ function useClient(options) {
179
+ return (0, react_1.useMemo)(() => createClient(options), [options]);
180
+ }
181
+ const CHECK_CONNECTION = (0, client_1.gql) `
182
+ query CheckConnection {
183
+ __schema {
184
+ queryType {
185
+ name
186
+ }
187
+ }
188
+ }
189
+ `;
190
+ /**
191
+ * Check connection to Hasura GraphQL endpoint
192
+ * @returns {Promise<boolean>} True if connection is successful
193
+ */
194
+ function checkConnection() {
195
+ return __awaiter(this, arguments, void 0, function* (client = getClient()) {
196
+ var _a, _b, _c;
197
+ const result = yield client.query({ query: CHECK_CONNECTION });
198
+ return !!((_c = (_b = (_a = result.data) === null || _a === void 0 ? void 0 : _a.__schema) === null || _b === void 0 ? void 0 : _b.queryType) === null || _c === void 0 ? void 0 : _c.name);
199
+ });
200
+ }
201
+ exports.default = {
202
+ createClient,
203
+ getClient,
204
+ getJwtSecret: exports.getJwtSecret,
205
+ checkConnection
206
+ };
@@ -0,0 +1,12 @@
1
+ export type DebuggerFunction = (...args: any[]) => void;
2
+ /**
3
+ * Debug utility factory.
4
+ *
5
+ * Always returns a debugger function for the specified namespace.
6
+ * If no namespace is provided, uses 'app' as the default.
7
+ *
8
+ * @param namespace - Namespace for the debugger.
9
+ * @returns A debugger function for the specified namespace.
10
+ */
11
+ declare function Debug(namespace?: string): DebuggerFunction;
12
+ export default Debug;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const debug_1 = __importDefault(require("debug"));
7
+ // @ts-ignore
8
+ const package_json_1 = __importDefault(require("../package.json")); // Using relative path
9
+ // Initialize root debugger using package name
10
+ const rootDebug = (0, debug_1.default)(package_json_1.default.shortName || package_json_1.default.name);
11
+ /**
12
+ * Debug utility factory.
13
+ *
14
+ * Always returns a debugger function for the specified namespace.
15
+ * If no namespace is provided, uses 'app' as the default.
16
+ *
17
+ * @param namespace - Namespace for the debugger.
18
+ * @returns A debugger function for the specified namespace.
19
+ */
20
+ function Debug(namespace) {
21
+ // Return the debugger function for that namespace, defaulting to 'app'
22
+ return rootDebug.extend(namespace || 'app');
23
+ }
24
+ exports.default = Debug;
@@ -0,0 +1,33 @@
1
+ import { DocumentNode } from '@apollo/client/core';
2
+ export type GenerateOperation = 'query' | 'subscription' | 'insert' | 'update' | 'delete';
3
+ export interface GenerateOptions {
4
+ operation: GenerateOperation;
5
+ table: string;
6
+ where?: Record<string, any>;
7
+ returning?: (string | Record<string, any>)[] | Record<string, any> | string;
8
+ aggregate?: Record<string, any>;
9
+ object?: Record<string, any>;
10
+ objects?: Record<string, any>[];
11
+ pk_columns?: Record<string, any>;
12
+ _set?: Record<string, any>;
13
+ limit?: number;
14
+ offset?: number;
15
+ order_by?: Record<string, any>[] | Record<string, any>;
16
+ fragments?: string[];
17
+ variables?: Record<string, any>;
18
+ varCounter?: number;
19
+ }
20
+ export interface GenerateResult {
21
+ queryString: string;
22
+ query: DocumentNode;
23
+ variables: Record<string, any>;
24
+ varCounter: number;
25
+ }
26
+ /**
27
+ * Creates a GraphQL query generator based on the provided schema.
28
+ *
29
+ * @param schema - The GraphQL schema in schema.json format.
30
+ * @returns A function to generate queries.
31
+ */
32
+ export declare function Generator(schema: any): (opts: GenerateOptions) => GenerateResult;
33
+ export default Generator;