@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 +131 -0
- package/dist/index.d.mts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +79 -0
- package/dist/index.mjs +79 -0
- package/package.json +1 -1
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"] });
|