@forjio/engine-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/dist/__tests__/api-keys.test.d.ts +2 -0
- package/dist/__tests__/api-keys.test.d.ts.map +1 -0
- package/dist/__tests__/api-keys.test.js +78 -0
- package/dist/__tests__/api-keys.test.js.map +1 -0
- package/dist/__tests__/helpers/express-mock.d.ts +13 -0
- package/dist/__tests__/helpers/express-mock.d.ts.map +1 -0
- package/dist/__tests__/helpers/express-mock.js +46 -0
- package/dist/__tests__/helpers/express-mock.js.map +1 -0
- package/dist/__tests__/helpers/redis-mock.d.ts +19 -0
- package/dist/__tests__/helpers/redis-mock.d.ts.map +1 -0
- package/dist/__tests__/helpers/redis-mock.js +69 -0
- package/dist/__tests__/helpers/redis-mock.js.map +1 -0
- package/dist/__tests__/jwt.test.d.ts +2 -0
- package/dist/__tests__/jwt.test.d.ts.map +1 -0
- package/dist/__tests__/jwt.test.js +98 -0
- package/dist/__tests__/jwt.test.js.map +1 -0
- package/dist/__tests__/middleware.test.d.ts +2 -0
- package/dist/__tests__/middleware.test.d.ts.map +1 -0
- package/dist/__tests__/middleware.test.js +173 -0
- package/dist/__tests__/middleware.test.js.map +1 -0
- package/dist/__tests__/session.test.d.ts +2 -0
- package/dist/__tests__/session.test.d.ts.map +1 -0
- package/dist/__tests__/session.test.js +96 -0
- package/dist/__tests__/session.test.js.map +1 -0
- package/dist/api-keys.d.ts +29 -0
- package/dist/api-keys.d.ts.map +1 -0
- package/dist/api-keys.js +50 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +7 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +66 -0
- package/dist/jwt.js.map +1 -0
- package/dist/middleware.d.ts +29 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +94 -0
- package/dist/middleware.js.map +1 -0
- package/dist/oauth.d.ts +16 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +78 -0
- package/dist/oauth.js.map +1 -0
- package/dist/session.d.ts +9 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +61 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +34 -0
- package/src/__tests__/api-keys.test.ts +94 -0
- package/src/__tests__/helpers/express-mock.ts +47 -0
- package/src/__tests__/helpers/redis-mock.ts +65 -0
- package/src/__tests__/jwt.test.ts +122 -0
- package/src/__tests__/middleware.test.ts +220 -0
- package/src/__tests__/session.test.ts +120 -0
- package/src/api-keys.ts +56 -0
- package/src/index.ts +46 -0
- package/src/jwt.ts +61 -0
- package/src/middleware.ts +113 -0
- package/src/oauth.ts +111 -0
- package/src/session.ts +78 -0
- package/src/types.ts +45 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +21 -0
package/src/oauth.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import passport from 'passport';
|
|
2
|
+
import { Strategy as GoogleStrategy, Profile as GoogleProfile } from 'passport-google-oauth20';
|
|
3
|
+
import { Strategy as GithubStrategy } from 'passport-github2';
|
|
4
|
+
import { AuthConfig } from './types';
|
|
5
|
+
|
|
6
|
+
export interface OAuthProfile {
|
|
7
|
+
provider: 'google' | 'github';
|
|
8
|
+
providerId: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name: string | null;
|
|
11
|
+
avatarUrl: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type FindOrCreateUser = (
|
|
15
|
+
profile: OAuthProfile
|
|
16
|
+
) => Promise<{ id: string; email: string }>;
|
|
17
|
+
|
|
18
|
+
export function configureGoogleStrategy(
|
|
19
|
+
config: AuthConfig,
|
|
20
|
+
findOrCreateUser: FindOrCreateUser
|
|
21
|
+
): void {
|
|
22
|
+
if (!config.google) return;
|
|
23
|
+
|
|
24
|
+
passport.use(
|
|
25
|
+
new GoogleStrategy(
|
|
26
|
+
{
|
|
27
|
+
clientID: config.google.clientId,
|
|
28
|
+
clientSecret: config.google.clientSecret,
|
|
29
|
+
callbackURL: config.google.callbackUrl,
|
|
30
|
+
scope: ['profile', 'email'],
|
|
31
|
+
},
|
|
32
|
+
async (_accessToken, _refreshToken, profile: GoogleProfile, done) => {
|
|
33
|
+
try {
|
|
34
|
+
const email = profile.emails?.[0]?.value;
|
|
35
|
+
if (!email) {
|
|
36
|
+
return done(new Error('No email found in Google profile'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const oauthProfile: OAuthProfile = {
|
|
40
|
+
provider: 'google',
|
|
41
|
+
providerId: profile.id,
|
|
42
|
+
email,
|
|
43
|
+
name: profile.displayName || null,
|
|
44
|
+
avatarUrl: profile.photos?.[0]?.value || null,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const user = await findOrCreateUser(oauthProfile);
|
|
48
|
+
return done(null, user);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return done(err as Error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function configureGithubStrategy(
|
|
58
|
+
config: AuthConfig,
|
|
59
|
+
findOrCreateUser: FindOrCreateUser
|
|
60
|
+
): void {
|
|
61
|
+
if (!config.github) return;
|
|
62
|
+
|
|
63
|
+
passport.use(
|
|
64
|
+
new GithubStrategy(
|
|
65
|
+
{
|
|
66
|
+
clientID: config.github.clientId,
|
|
67
|
+
clientSecret: config.github.clientSecret,
|
|
68
|
+
callbackURL: config.github.callbackUrl,
|
|
69
|
+
scope: ['user:email'],
|
|
70
|
+
},
|
|
71
|
+
async (
|
|
72
|
+
_accessToken: string,
|
|
73
|
+
_refreshToken: string,
|
|
74
|
+
profile: { id: string; displayName?: string; emails?: Array<{ value: string }>; photos?: Array<{ value: string }> },
|
|
75
|
+
done: (err: Error | null, user?: { id: string; email: string }) => void
|
|
76
|
+
) => {
|
|
77
|
+
try {
|
|
78
|
+
const email = profile.emails?.[0]?.value;
|
|
79
|
+
if (!email) {
|
|
80
|
+
return done(new Error('No email found in GitHub profile'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const oauthProfile: OAuthProfile = {
|
|
84
|
+
provider: 'github',
|
|
85
|
+
providerId: profile.id,
|
|
86
|
+
email,
|
|
87
|
+
name: profile.displayName || null,
|
|
88
|
+
avatarUrl: profile.photos?.[0]?.value || null,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const user = await findOrCreateUser(oauthProfile);
|
|
92
|
+
return done(null, user);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return done(err as Error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function setupOAuth(
|
|
102
|
+
config: AuthConfig,
|
|
103
|
+
findOrCreateUser: FindOrCreateUser
|
|
104
|
+
): void {
|
|
105
|
+
if (config.google) {
|
|
106
|
+
configureGoogleStrategy(config, findOrCreateUser);
|
|
107
|
+
}
|
|
108
|
+
if (config.github) {
|
|
109
|
+
configureGithubStrategy(config, findOrCreateUser);
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { SessionData } from './types';
|
|
4
|
+
|
|
5
|
+
const KEY_PREFIX = 'engine:session:';
|
|
6
|
+
const USER_SESSIONS_PREFIX = 'engine:user-sessions:';
|
|
7
|
+
const DEFAULT_TTL_SECONDS = 7 * 24 * 60 * 60; // 7 days
|
|
8
|
+
|
|
9
|
+
export interface SessionStore {
|
|
10
|
+
create(sessionData: SessionData): Promise<string>;
|
|
11
|
+
get(sessionId: string): Promise<SessionData | null>;
|
|
12
|
+
destroy(sessionId: string): Promise<void>;
|
|
13
|
+
destroyAllForUser(userId: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createSessionStore(redisUrl: string): SessionStore {
|
|
17
|
+
const redis = new Redis(redisUrl);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
async create(sessionData: SessionData): Promise<string> {
|
|
21
|
+
const sessionId = uuidv4();
|
|
22
|
+
const key = `${KEY_PREFIX}${sessionId}`;
|
|
23
|
+
const serialized = JSON.stringify(sessionData);
|
|
24
|
+
|
|
25
|
+
await redis.setex(key, DEFAULT_TTL_SECONDS, serialized);
|
|
26
|
+
|
|
27
|
+
// Track session IDs per user for bulk destruction
|
|
28
|
+
const userKey = `${USER_SESSIONS_PREFIX}${sessionData.userId}`;
|
|
29
|
+
await redis.sadd(userKey, sessionId);
|
|
30
|
+
await redis.expire(userKey, DEFAULT_TTL_SECONDS);
|
|
31
|
+
|
|
32
|
+
return sessionId;
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async get(sessionId: string): Promise<SessionData | null> {
|
|
36
|
+
const key = `${KEY_PREFIX}${sessionId}`;
|
|
37
|
+
const data = await redis.get(key);
|
|
38
|
+
|
|
39
|
+
if (!data) return null;
|
|
40
|
+
|
|
41
|
+
const session = JSON.parse(data) as SessionData;
|
|
42
|
+
|
|
43
|
+
// Check logical expiry
|
|
44
|
+
if (session.expiresAt < Date.now()) {
|
|
45
|
+
await redis.del(key);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return session;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async destroy(sessionId: string): Promise<void> {
|
|
53
|
+
const key = `${KEY_PREFIX}${sessionId}`;
|
|
54
|
+
|
|
55
|
+
// Get session to find userId before deleting
|
|
56
|
+
const data = await redis.get(key);
|
|
57
|
+
if (data) {
|
|
58
|
+
const session = JSON.parse(data) as SessionData;
|
|
59
|
+
const userKey = `${USER_SESSIONS_PREFIX}${session.userId}`;
|
|
60
|
+
await redis.srem(userKey, sessionId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await redis.del(key);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async destroyAllForUser(userId: string): Promise<void> {
|
|
67
|
+
const userKey = `${USER_SESSIONS_PREFIX}${userId}`;
|
|
68
|
+
const sessionIds = await redis.smembers(userKey);
|
|
69
|
+
|
|
70
|
+
if (sessionIds.length > 0) {
|
|
71
|
+
const keys = sessionIds.map((id) => `${KEY_PREFIX}${id}`);
|
|
72
|
+
await redis.del(...keys);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await redis.del(userKey);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface EngineUser {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
name: string | null;
|
|
5
|
+
role: 'user' | 'admin';
|
|
6
|
+
subscriptionTier: string | null;
|
|
7
|
+
midtransCustomerId: string | null;
|
|
8
|
+
emailVerified: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ApiKey {
|
|
12
|
+
id: string;
|
|
13
|
+
key: string;
|
|
14
|
+
userId: string;
|
|
15
|
+
productSlug: string;
|
|
16
|
+
scopes: string[];
|
|
17
|
+
expiresAt: Date | null;
|
|
18
|
+
lastUsedAt: Date | null;
|
|
19
|
+
createdAt: Date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SessionData {
|
|
23
|
+
userId: string;
|
|
24
|
+
email: string;
|
|
25
|
+
role: string;
|
|
26
|
+
productSlug: string;
|
|
27
|
+
expiresAt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface JwtPayload {
|
|
31
|
+
userId: string;
|
|
32
|
+
email: string;
|
|
33
|
+
role: string;
|
|
34
|
+
subscriptionTier: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AuthConfig {
|
|
38
|
+
jwtSecret: string;
|
|
39
|
+
jwtRefreshSecret: string;
|
|
40
|
+
jwtExpiresIn?: string;
|
|
41
|
+
jwtRefreshExpiresIn?: string;
|
|
42
|
+
redisUrl?: string;
|
|
43
|
+
google?: { clientId: string; clientSecret: string; callbackUrl: string };
|
|
44
|
+
github?: { clientId: string; clientSecret: string; callbackUrl: string };
|
|
45
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['src/**/__tests__/**/*.test.ts'],
|
|
8
|
+
exclude: ['**/node_modules/**', '**/dist/**', '**/*.d.ts'],
|
|
9
|
+
testTimeout: 10000,
|
|
10
|
+
coverage: {
|
|
11
|
+
provider: 'v8',
|
|
12
|
+
reporter: ['text', 'json', 'html'],
|
|
13
|
+
exclude: [
|
|
14
|
+
'node_modules/',
|
|
15
|
+
'**/*.d.ts',
|
|
16
|
+
'**/*.config.*',
|
|
17
|
+
'src/__tests__/**',
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
})
|