@meridianjs/auth 0.1.5 → 0.1.8

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,131 @@
1
+ # @meridianjs/auth
2
+
3
+ Authentication module for MeridianJS. Provides JWT-based register/login flows, Google OAuth support, Express middleware for token verification, and RBAC guards.
4
+
5
+ Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.
6
+
7
+ ## Service: `authModuleService`
8
+
9
+ ```typescript
10
+ const svc = req.scope.resolve("authModuleService") as any
11
+ ```
12
+
13
+ ### Methods
14
+
15
+ ```typescript
16
+ // Register a new user — returns { user, token }
17
+ const { user, token } = await svc.register({
18
+ email: "alice@example.com",
19
+ password: "password123",
20
+ first_name: "Alice",
21
+ last_name: "Smith",
22
+ role: "member", // optional: "super-admin" | "admin" | "moderator" | "member"
23
+ })
24
+
25
+ // Login — returns { user, token }
26
+ const { user, token } = await svc.login({
27
+ email: "alice@example.com",
28
+ password: "password123",
29
+ })
30
+
31
+ // Verify and decode a JWT
32
+ const payload = await svc.verifyToken(token)
33
+ // payload: { sub, workspaceId, roles, permissions, jti }
34
+ ```
35
+
36
+ ## Middleware
37
+
38
+ ### `authenticateJWT`
39
+
40
+ Verifies the `Authorization: Bearer <token>` header and attaches `req.user`. Returns `401` if the token is missing or invalid.
41
+
42
+ ```typescript
43
+ import { authenticateJWT } from "@meridianjs/auth"
44
+
45
+ // Applied globally in middlewares.ts:
46
+ export default {
47
+ routes: [
48
+ { matcher: "/admin", middlewares: [authenticateJWT] },
49
+ ],
50
+ }
51
+
52
+ // Or inline in a route:
53
+ export const GET = [authenticateJWT, async (req: any, res: Response) => {
54
+ res.json({ userId: req.user.id })
55
+ }]
56
+ ```
57
+
58
+ ### `requireRoles`
59
+
60
+ Guard a route or handler to specific roles. Returns `403` if the user's roles don't match.
61
+
62
+ ```typescript
63
+ import { requireRoles } from "@meridianjs/auth"
64
+
65
+ export const DELETE = [
66
+ authenticateJWT,
67
+ requireRoles("admin", "super-admin"),
68
+ async (req: any, res: Response) => {
69
+ // Only admins reach here
70
+ },
71
+ ]
72
+ ```
73
+
74
+ ### `requirePermission`
75
+
76
+ Guard based on a specific permission string from a custom AppRole:
77
+
78
+ ```typescript
79
+ import { requirePermission } from "@meridianjs/auth"
80
+
81
+ export const POST = [
82
+ authenticateJWT,
83
+ requirePermission("issues:create"),
84
+ async (req: any, res: Response) => { /* ... */ },
85
+ ]
86
+ ```
87
+
88
+ ## JWT Payload
89
+
90
+ ```typescript
91
+ interface JwtPayload {
92
+ sub: string // User ID
93
+ workspaceId: string // Active workspace ID (null until workspace selected)
94
+ roles: string[] // e.g. ["admin"] or ["member"]
95
+ permissions: string[] // Custom AppRole permissions (e.g. ["issues:create", "issues:delete"])
96
+ jti: string // Unique token ID (for revocation)
97
+ iat: number
98
+ exp: number
99
+ }
100
+ ```
101
+
102
+ Tokens expire after **7 days** by default. Configure in `projectConfig`:
103
+
104
+ ```typescript
105
+ projectConfig: {
106
+ jwtSecret: process.env.JWT_SECRET!,
107
+ jwtExpiresIn: "30d", // optional
108
+ }
109
+ ```
110
+
111
+ ## Google OAuth
112
+
113
+ When `@meridianjs/google-oauth` is configured, the auth module exposes:
114
+
115
+ ```
116
+ GET /auth/google → Redirects to Google consent screen
117
+ GET /auth/google/callback → Handles OAuth code, returns JWT
118
+ ```
119
+
120
+ ## API Routes
121
+
122
+ | Method | Path | Description |
123
+ |---|---|---|
124
+ | `POST` | `/auth/register` | Register, return `{ user, token }` |
125
+ | `POST` | `/auth/login` | Login, return `{ user, token }` |
126
+ | `GET` | `/auth/invite/:token` | Validate an invitation token |
127
+ | `POST` | `/auth/invite/:token` | Accept invitation (register or login) |
128
+
129
+ ## License
130
+
131
+ MIT
package/dist/index.d.mts CHANGED
@@ -32,6 +32,20 @@ interface JwtPayload {
32
32
  iat?: number;
33
33
  exp?: number;
34
34
  }
35
+ interface GoogleAuthInput {
36
+ googleId: string;
37
+ email: string;
38
+ firstName: string | null;
39
+ lastName: string | null;
40
+ picture: string | null;
41
+ inviteRecord?: {
42
+ id: string;
43
+ email: string | null;
44
+ role: string;
45
+ workspace_id: string;
46
+ app_role_id: string | null;
47
+ } | null;
48
+ }
35
49
  declare const AuthModuleService_base: new (container: MeridianContainer) => _meridianjs_types.IModuleService;
36
50
  declare class AuthModuleService extends AuthModuleService_base {
37
51
  private readonly container;
@@ -40,6 +54,13 @@ declare class AuthModuleService extends AuthModuleService_base {
40
54
  register(input: RegisterInput): Promise<AuthResult>;
41
55
  /** Authenticate with email + password and return a signed JWT. */
42
56
  login(input: LoginInput): Promise<AuthResult>;
57
+ /**
58
+ * Sign in or register a user via Google OAuth.
59
+ * 1. Look up by google_id — existing SSO user
60
+ * 2. Look up by email — link google_id to existing account
61
+ * 3. Create new user
62
+ */
63
+ loginOrRegisterWithGoogle(input: GoogleAuthInput): Promise<AuthResult>;
43
64
  /** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
44
65
  verifyToken(token: string, secret: string): JwtPayload;
45
66
  /** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
@@ -93,4 +114,4 @@ declare function requireWorkspace(req: any, res: Response, next: NextFunction):
93
114
  declare const AUTH_MODULE = "authModuleService";
94
115
  declare const _default: _meridianjs_types.ModuleDefinition;
95
116
 
96
- export { AUTH_MODULE, AuthModuleService, type AuthResult, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requirePermission, requireRoles, requireWorkspace };
117
+ export { AUTH_MODULE, AuthModuleService, type AuthResult, type GoogleAuthInput, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requirePermission, requireRoles, requireWorkspace };
package/dist/index.d.ts CHANGED
@@ -32,6 +32,20 @@ interface JwtPayload {
32
32
  iat?: number;
33
33
  exp?: number;
34
34
  }
35
+ interface GoogleAuthInput {
36
+ googleId: string;
37
+ email: string;
38
+ firstName: string | null;
39
+ lastName: string | null;
40
+ picture: string | null;
41
+ inviteRecord?: {
42
+ id: string;
43
+ email: string | null;
44
+ role: string;
45
+ workspace_id: string;
46
+ app_role_id: string | null;
47
+ } | null;
48
+ }
35
49
  declare const AuthModuleService_base: new (container: MeridianContainer) => _meridianjs_types.IModuleService;
36
50
  declare class AuthModuleService extends AuthModuleService_base {
37
51
  private readonly container;
@@ -40,6 +54,13 @@ declare class AuthModuleService extends AuthModuleService_base {
40
54
  register(input: RegisterInput): Promise<AuthResult>;
41
55
  /** Authenticate with email + password and return a signed JWT. */
42
56
  login(input: LoginInput): Promise<AuthResult>;
57
+ /**
58
+ * Sign in or register a user via Google OAuth.
59
+ * 1. Look up by google_id — existing SSO user
60
+ * 2. Look up by email — link google_id to existing account
61
+ * 3. Create new user
62
+ */
63
+ loginOrRegisterWithGoogle(input: GoogleAuthInput): Promise<AuthResult>;
43
64
  /** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
44
65
  verifyToken(token: string, secret: string): JwtPayload;
45
66
  /** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
@@ -93,4 +114,4 @@ declare function requireWorkspace(req: any, res: Response, next: NextFunction):
93
114
  declare const AUTH_MODULE = "authModuleService";
94
115
  declare const _default: _meridianjs_types.ModuleDefinition;
95
116
 
96
- export { AUTH_MODULE, AuthModuleService, type AuthResult, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requirePermission, requireRoles, requireWorkspace };
117
+ export { AUTH_MODULE, AuthModuleService, type AuthResult, type GoogleAuthInput, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requirePermission, requireRoles, requireWorkspace };
package/dist/index.js CHANGED
@@ -122,6 +122,85 @@ var AuthModuleService = class extends (0, import_framework_utils.MeridianService
122
122
  token
123
123
  };
124
124
  }
125
+ /**
126
+ * Sign in or register a user via Google OAuth.
127
+ * 1. Look up by google_id — existing SSO user
128
+ * 2. Look up by email — link google_id to existing account
129
+ * 3. Create new user
130
+ */
131
+ async loginOrRegisterWithGoogle(input) {
132
+ const userService = this.container.resolve("userModuleService");
133
+ const config = this.container.resolve("config");
134
+ let user = await userService.retrieveUserByGoogleId(input.googleId);
135
+ if (!user) {
136
+ const existingByEmail = await userService.retrieveUserByEmail(input.email.toLowerCase().trim());
137
+ if (existingByEmail) {
138
+ throw Object.assign(
139
+ new Error(
140
+ "An account with this email already exists. Please sign in with your password. You can link Google sign-in from your account settings afterwards."
141
+ ),
142
+ { status: 409 }
143
+ );
144
+ }
145
+ }
146
+ if (user) {
147
+ if (!user.is_active) {
148
+ throw Object.assign(new Error("Account deactivated"), { status: 403 });
149
+ }
150
+ await userService.recordLogin(user.id).catch(() => {
151
+ });
152
+ const permissions2 = await this.resolvePermissions(user.app_role_id);
153
+ const { token: token2, jti: jti2, expiresAt: expiresAt2 } = this.signToken(user.id, null, [user.role ?? "member"], permissions2, config.projectConfig.jwtSecret);
154
+ await userService.createSession(jti2, user.id, expiresAt2).catch(() => {
155
+ });
156
+ return {
157
+ user: { id: user.id, email: user.email, first_name: user.first_name ?? null, last_name: user.last_name ?? null },
158
+ token: token2
159
+ };
160
+ }
161
+ const invite = input.inviteRecord;
162
+ let role = "member";
163
+ if (invite) {
164
+ role = invite.role ?? "member";
165
+ } else {
166
+ const [, userCount] = await userService.listAndCountUsers({}, { limit: 1 });
167
+ if (userCount === 0) {
168
+ role = "super-admin";
169
+ } else {
170
+ throw Object.assign(
171
+ new Error("You are not authorized to access this application. Contact an admin for an invitation."),
172
+ { status: 403 }
173
+ );
174
+ }
175
+ }
176
+ const password_hash = await import_bcrypt.default.hash((0, import_crypto.randomUUID)(), 1);
177
+ const newUser = await userService.createUser({
178
+ email: input.email.toLowerCase().trim(),
179
+ password_hash,
180
+ first_name: input.firstName ?? null,
181
+ last_name: input.lastName ?? null,
182
+ role,
183
+ is_active: true,
184
+ google_id: input.googleId,
185
+ ...invite?.app_role_id ? { app_role_id: invite.app_role_id } : {}
186
+ });
187
+ if (invite) {
188
+ try {
189
+ const workspaceMemberService = this.container.resolve("workspaceMemberModuleService");
190
+ const wsRole = invite.role === "member" ? "member" : "admin";
191
+ await workspaceMemberService.ensureMember(invite.workspace_id, newUser.id, wsRole);
192
+ } catch {
193
+ }
194
+ }
195
+ const permissions = await this.resolvePermissions(newUser.app_role_id);
196
+ const { token, jti, expiresAt } = this.signToken(newUser.id, null, [newUser.role], permissions, config.projectConfig.jwtSecret);
197
+ await userService.createSession(jti, newUser.id, expiresAt).catch(() => {
198
+ });
199
+ return {
200
+ user: { id: newUser.id, email: newUser.email, first_name: newUser.first_name ?? null, last_name: newUser.last_name ?? null },
201
+ token
202
+ };
203
+ }
125
204
  /** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
126
205
  verifyToken(token, secret) {
127
206
  return import_jsonwebtoken.default.verify(token, secret, { algorithms: ["HS256"] });
package/dist/index.mjs CHANGED
@@ -82,6 +82,85 @@ var AuthModuleService = class extends MeridianService({}) {
82
82
  token
83
83
  };
84
84
  }
85
+ /**
86
+ * Sign in or register a user via Google OAuth.
87
+ * 1. Look up by google_id — existing SSO user
88
+ * 2. Look up by email — link google_id to existing account
89
+ * 3. Create new user
90
+ */
91
+ async loginOrRegisterWithGoogle(input) {
92
+ const userService = this.container.resolve("userModuleService");
93
+ const config = this.container.resolve("config");
94
+ let user = await userService.retrieveUserByGoogleId(input.googleId);
95
+ if (!user) {
96
+ const existingByEmail = await userService.retrieveUserByEmail(input.email.toLowerCase().trim());
97
+ if (existingByEmail) {
98
+ throw Object.assign(
99
+ new Error(
100
+ "An account with this email already exists. Please sign in with your password. You can link Google sign-in from your account settings afterwards."
101
+ ),
102
+ { status: 409 }
103
+ );
104
+ }
105
+ }
106
+ if (user) {
107
+ if (!user.is_active) {
108
+ throw Object.assign(new Error("Account deactivated"), { status: 403 });
109
+ }
110
+ await userService.recordLogin(user.id).catch(() => {
111
+ });
112
+ const permissions2 = await this.resolvePermissions(user.app_role_id);
113
+ const { token: token2, jti: jti2, expiresAt: expiresAt2 } = this.signToken(user.id, null, [user.role ?? "member"], permissions2, config.projectConfig.jwtSecret);
114
+ await userService.createSession(jti2, user.id, expiresAt2).catch(() => {
115
+ });
116
+ return {
117
+ user: { id: user.id, email: user.email, first_name: user.first_name ?? null, last_name: user.last_name ?? null },
118
+ token: token2
119
+ };
120
+ }
121
+ const invite = input.inviteRecord;
122
+ let role = "member";
123
+ if (invite) {
124
+ role = invite.role ?? "member";
125
+ } else {
126
+ const [, userCount] = await userService.listAndCountUsers({}, { limit: 1 });
127
+ if (userCount === 0) {
128
+ role = "super-admin";
129
+ } else {
130
+ throw Object.assign(
131
+ new Error("You are not authorized to access this application. Contact an admin for an invitation."),
132
+ { status: 403 }
133
+ );
134
+ }
135
+ }
136
+ const password_hash = await bcrypt.hash(randomUUID(), 1);
137
+ const newUser = await userService.createUser({
138
+ email: input.email.toLowerCase().trim(),
139
+ password_hash,
140
+ first_name: input.firstName ?? null,
141
+ last_name: input.lastName ?? null,
142
+ role,
143
+ is_active: true,
144
+ google_id: input.googleId,
145
+ ...invite?.app_role_id ? { app_role_id: invite.app_role_id } : {}
146
+ });
147
+ if (invite) {
148
+ try {
149
+ const workspaceMemberService = this.container.resolve("workspaceMemberModuleService");
150
+ const wsRole = invite.role === "member" ? "member" : "admin";
151
+ await workspaceMemberService.ensureMember(invite.workspace_id, newUser.id, wsRole);
152
+ } catch {
153
+ }
154
+ }
155
+ const permissions = await this.resolvePermissions(newUser.app_role_id);
156
+ const { token, jti, expiresAt } = this.signToken(newUser.id, null, [newUser.role], permissions, config.projectConfig.jwtSecret);
157
+ await userService.createSession(jti, newUser.id, expiresAt).catch(() => {
158
+ });
159
+ return {
160
+ user: { id: newUser.id, email: newUser.email, first_name: newUser.first_name ?? null, last_name: newUser.last_name ?? null },
161
+ token
162
+ };
163
+ }
85
164
  /** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
86
165
  verifyToken(token, secret) {
87
166
  return jwt.verify(token, secret, { algorithms: ["HS256"] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meridianjs/auth",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "description": "Meridian auth module — JWT authentication and middleware",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",