@quatrain/auth-github 1.2.10
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 +18 -0
- package/dist/GithubAuthAdapter.d.ts +34 -0
- package/dist/GithubAuthAdapter.js +136 -0
- package/dist/GithubAuthAdapter.js.map +1 -0
- package/dist/GithubAuthAdapter.test.d.ts +1 -0
- package/dist/GithubAuthAdapter.test.js +177 -0
- package/dist/GithubAuthAdapter.test.js.map +1 -0
- package/dist/GithubAuthApi.d.ts +6 -0
- package/dist/GithubAuthApi.js +73 -0
- package/dist/GithubAuthApi.js.map +1 -0
- package/dist/GithubAuthApi.test.d.ts +1 -0
- package/dist/GithubAuthApi.test.js +117 -0
- package/dist/GithubAuthApi.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/GithubAuthAdapter.test.ts +172 -0
- package/src/GithubAuthAdapter.ts +89 -0
- package/src/GithubAuthApi.test.ts +142 -0
- package/src/GithubAuthApi.ts +70 -0
- package/src/index.ts +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @quatrain/auth-github
|
|
2
|
+
|
|
3
|
+
Authentication adapter and pluggable endpoints for GitHub OAuth 2.0 Web Application Flow.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
This package is a workspace package within the Quatrain Core monorepo. It depends on `@quatrain/auth` and `@quatrain/api`.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
yarn add @quatrain/auth-github
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **OAuth 2.0 Flow**: Handles authorization URL generation and exchanging code for token.
|
|
16
|
+
- **Pluggable API Router**: Framework-agnostic `GithubAuthApi` endpoint handler matching `ServerAdapter` specification.
|
|
17
|
+
- **Deep Linking**: Supports generic redirect schemes for mobile contexts.
|
|
18
|
+
- **GitHub API Utilities**: Methods to check repository existence and create new repositories.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AbstractOAuthAdapter } from '@quatrain/auth';
|
|
2
|
+
/**
|
|
3
|
+
* Authentication adapter for GitHub OAuth 2.0 Web Application Flow.
|
|
4
|
+
* Handles profiles, repository existence checks, and repository creation.
|
|
5
|
+
*/
|
|
6
|
+
export declare class GithubAuthAdapter extends AbstractOAuthAdapter {
|
|
7
|
+
protected _authorizationEndpoint: string;
|
|
8
|
+
protected _tokenEndpoint: string;
|
|
9
|
+
protected _userProfileEndpoint: string;
|
|
10
|
+
/**
|
|
11
|
+
* Factory method to create a new instance from config credentials.
|
|
12
|
+
*/
|
|
13
|
+
static factory(config: any): GithubAuthAdapter | null;
|
|
14
|
+
/**
|
|
15
|
+
* Returns the pluggable API routes builder.
|
|
16
|
+
*/
|
|
17
|
+
getEndpointHandler(): any;
|
|
18
|
+
/**
|
|
19
|
+
* Returns user details from GitHub by validating the access token.
|
|
20
|
+
*/
|
|
21
|
+
getAuthToken(accessToken: string): Promise<any>;
|
|
22
|
+
/**
|
|
23
|
+
* Custom GitHub API call to check repository existence.
|
|
24
|
+
*/
|
|
25
|
+
checkRepositoryExists(accessToken: string, owner: string, name: string): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Custom GitHub API call to create a repository.
|
|
28
|
+
*/
|
|
29
|
+
createRepository(accessToken: string, name: string, options?: {
|
|
30
|
+
private?: boolean;
|
|
31
|
+
description?: string;
|
|
32
|
+
autoInit?: boolean;
|
|
33
|
+
}): Promise<any>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.GithubAuthAdapter = void 0;
|
|
46
|
+
const auth_1 = require("@quatrain/auth");
|
|
47
|
+
const GithubAuthApi_1 = require("./GithubAuthApi");
|
|
48
|
+
const nativeFetch = __importStar(require("node-fetch-native"));
|
|
49
|
+
/**
|
|
50
|
+
* Authentication adapter for GitHub OAuth 2.0 Web Application Flow.
|
|
51
|
+
* Handles profiles, repository existence checks, and repository creation.
|
|
52
|
+
*/
|
|
53
|
+
class GithubAuthAdapter extends auth_1.AbstractOAuthAdapter {
|
|
54
|
+
constructor() {
|
|
55
|
+
super(...arguments);
|
|
56
|
+
this._authorizationEndpoint = 'https://github.com/login/oauth/authorize';
|
|
57
|
+
this._tokenEndpoint = 'https://github.com/login/oauth/access_token';
|
|
58
|
+
this._userProfileEndpoint = 'https://api.github.com/user';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Factory method to create a new instance from config credentials.
|
|
62
|
+
*/
|
|
63
|
+
static factory(config) {
|
|
64
|
+
if (!config.clientId || !config.clientSecret)
|
|
65
|
+
return null;
|
|
66
|
+
return new GithubAuthAdapter({ config });
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns the pluggable API routes builder.
|
|
70
|
+
*/
|
|
71
|
+
getEndpointHandler() {
|
|
72
|
+
return GithubAuthApi_1.GithubAuthApi;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Returns user details from GitHub by validating the access token.
|
|
76
|
+
*/
|
|
77
|
+
getAuthToken(accessToken) {
|
|
78
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
const response = yield nativeFetch.fetch(this._userProfileEndpoint, {
|
|
80
|
+
headers: {
|
|
81
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
82
|
+
'Accept': 'application/json',
|
|
83
|
+
'User-Agent': 'Quatrain-Auth-Github'
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new Error(`Failed to fetch Github user profile: ${response.statusText}`);
|
|
88
|
+
}
|
|
89
|
+
return yield response.json();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Custom GitHub API call to check repository existence.
|
|
94
|
+
*/
|
|
95
|
+
checkRepositoryExists(accessToken, owner, name) {
|
|
96
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
const response = yield nativeFetch.fetch(`https://api.github.com/repos/${owner}/${name}`, {
|
|
98
|
+
headers: {
|
|
99
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
100
|
+
'Accept': 'application/json',
|
|
101
|
+
'User-Agent': 'Quatrain-Auth-Github'
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return response.status === 200;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Custom GitHub API call to create a repository.
|
|
109
|
+
*/
|
|
110
|
+
createRepository(accessToken_1, name_1) {
|
|
111
|
+
return __awaiter(this, arguments, void 0, function* (accessToken, name, options = {}) {
|
|
112
|
+
const response = yield nativeFetch.fetch('https://api.github.com/user/repos', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: {
|
|
115
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
'Accept': 'application/json',
|
|
118
|
+
'User-Agent': 'Quatrain-Auth-Github'
|
|
119
|
+
},
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
name,
|
|
122
|
+
private: options.private !== false,
|
|
123
|
+
description: options.description || 'Dépôt créé automatiquement par Modaka',
|
|
124
|
+
auto_init: options.autoInit !== false
|
|
125
|
+
})
|
|
126
|
+
});
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const errData = yield response.json().catch(() => ({}));
|
|
129
|
+
throw new Error(`Failed to create repository: ${errData.message || response.statusText}`);
|
|
130
|
+
}
|
|
131
|
+
return yield response.json();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
exports.GithubAuthAdapter = GithubAuthAdapter;
|
|
136
|
+
//# sourceMappingURL=GithubAuthAdapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GithubAuthAdapter.js","sourceRoot":"","sources":["../src/GithubAuthAdapter.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,yCAAqD;AACrD,mDAA+C;AAC/C,+DAAgD;AAEhD;;;GAGG;AACH,MAAa,iBAAkB,SAAQ,2BAAoB;IAA3D;;QACa,2BAAsB,GAAG,0CAA0C,CAAA;QACnE,mBAAc,GAAG,6CAA6C,CAAA;QAC9D,yBAAoB,GAAG,6BAA6B,CAAA;IA6EjE,CAAC;IA3EE;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,MAAW;QACvB,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,YAAY;YAAE,OAAO,IAAI,CAAA;QACzD,OAAO,IAAI,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED;;OAEG;IACa,kBAAkB;QAC/B,OAAO,6BAAa,CAAA;IACvB,CAAC;IAED;;OAEG;IACG,YAAY,CAAC,WAAmB;;YACnC,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,oBAAoB,EAAE;gBACjE,OAAO,EAAE;oBACN,eAAe,EAAE,UAAU,WAAW,EAAE;oBACxC,QAAQ,EAAE,kBAAkB;oBAC5B,YAAY,EAAE,sBAAsB;iBACtC;aACH,CAAC,CAAA;YAEF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAChB,MAAM,IAAI,KAAK,CAAC,wCAAwC,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;YACjF,CAAC;YAED,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QAC/B,CAAC;KAAA;IAED;;OAEG;IACG,qBAAqB,CAAC,WAAmB,EAAE,KAAa,EAAE,IAAY;;YACzE,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,KAAK,CAAC,gCAAgC,KAAK,IAAI,IAAI,EAAE,EAAE;gBACvF,OAAO,EAAE;oBACN,eAAe,EAAE,UAAU,WAAW,EAAE;oBACxC,QAAQ,EAAE,kBAAkB;oBAC5B,YAAY,EAAE,sBAAsB;iBACtC;aACH,CAAC,CAAA;YACF,OAAO,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAA;QACjC,CAAC;KAAA;IAED;;OAEG;IACG,gBAAgB;6DAAC,WAAmB,EAAE,IAAY,EAAE,UAA2E,EAAE;YACpI,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,KAAK,CAAC,mCAAmC,EAAE;gBAC3E,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACN,eAAe,EAAE,UAAU,WAAW,EAAE;oBACxC,cAAc,EAAE,kBAAkB;oBAClC,QAAQ,EAAE,kBAAkB;oBAC5B,YAAY,EAAE,sBAAsB;iBACtC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBAClB,IAAI;oBACJ,OAAO,EAAE,OAAO,CAAC,OAAO,KAAK,KAAK;oBAClC,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,uCAAuC;oBAC3E,SAAS,EAAE,OAAO,CAAC,QAAQ,KAAK,KAAK;iBACvC,CAAC;aACJ,CAAC,CAAA;YAEF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAChB,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;gBACvD,MAAM,IAAI,KAAK,CAAC,gCAAgC,OAAO,CAAC,OAAO,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;YAC5F,CAAC;YAED,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QAC/B,CAAC;KAAA;CACH;AAhFD,8CAgFC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
const GithubAuthAdapter_1 = require("./GithubAuthAdapter");
|
|
46
|
+
const nativeFetch = __importStar(require("node-fetch-native"));
|
|
47
|
+
jest.mock('node-fetch-native');
|
|
48
|
+
describe('GithubAuthAdapter', () => {
|
|
49
|
+
let adapter;
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
jest.clearAllMocks();
|
|
52
|
+
adapter = new GithubAuthAdapter_1.GithubAuthAdapter({
|
|
53
|
+
config: {
|
|
54
|
+
clientId: 'test-client-id',
|
|
55
|
+
clientSecret: 'test-client-secret',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('factory()', () => {
|
|
60
|
+
it('should return null if clientId or clientSecret is missing', () => {
|
|
61
|
+
expect(GithubAuthAdapter_1.GithubAuthAdapter.factory({})).toBeNull();
|
|
62
|
+
expect(GithubAuthAdapter_1.GithubAuthAdapter.factory({ clientId: 'id' })).toBeNull();
|
|
63
|
+
expect(GithubAuthAdapter_1.GithubAuthAdapter.factory({ clientSecret: 'secret' })).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
it('should return an instance if config is valid', () => {
|
|
66
|
+
const instance = GithubAuthAdapter_1.GithubAuthAdapter.factory({
|
|
67
|
+
clientId: 'id',
|
|
68
|
+
clientSecret: 'secret',
|
|
69
|
+
});
|
|
70
|
+
expect(instance).toBeInstanceOf(GithubAuthAdapter_1.GithubAuthAdapter);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('getAuthorizationUrl()', () => {
|
|
74
|
+
it('should return correct authorization URL', () => {
|
|
75
|
+
const url = adapter.getAuthorizationUrl('http://callback', ['repo', 'user'], 'state-123');
|
|
76
|
+
expect(url).toBe('https://github.com/login/oauth/authorize?client_id=test-client-id&scope=repo%20user&redirect_uri=http%3A%2F%2Fcallback&state=state-123');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('exchangeCodeForToken()', () => {
|
|
80
|
+
it('should exchange code for token successfully', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
81
|
+
const mockResponse = {
|
|
82
|
+
ok: true,
|
|
83
|
+
json: jest.fn().mockResolvedValue({
|
|
84
|
+
access_token: 'github-access-token-123',
|
|
85
|
+
token_type: 'bearer',
|
|
86
|
+
scope: 'repo',
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
nativeFetch.fetch.mockResolvedValue(mockResponse);
|
|
90
|
+
const result = yield adapter.exchangeCodeForToken('code-123', 'http://callback');
|
|
91
|
+
expect(result.access_token).toBe('github-access-token-123');
|
|
92
|
+
expect(nativeFetch.fetch).toHaveBeenCalledWith('https://github.com/login/oauth/access_token', expect.objectContaining({
|
|
93
|
+
method: 'POST',
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
client_id: 'test-client-id',
|
|
96
|
+
client_secret: 'test-client-secret',
|
|
97
|
+
code: 'code-123',
|
|
98
|
+
grant_type: 'authorization_code',
|
|
99
|
+
redirect_uri: 'http://callback',
|
|
100
|
+
}),
|
|
101
|
+
}));
|
|
102
|
+
}));
|
|
103
|
+
it('should throw error on failed exchange request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
const mockResponse = {
|
|
105
|
+
ok: false,
|
|
106
|
+
statusText: 'Bad Request',
|
|
107
|
+
};
|
|
108
|
+
nativeFetch.fetch.mockResolvedValue(mockResponse);
|
|
109
|
+
yield expect(adapter.exchangeCodeForToken('code-123')).rejects.toThrow('OAuth token exchange failed: Bad Request');
|
|
110
|
+
}));
|
|
111
|
+
});
|
|
112
|
+
describe('getAuthToken()', () => {
|
|
113
|
+
it('should fetch and return user profile information', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
114
|
+
const mockResponse = {
|
|
115
|
+
ok: true,
|
|
116
|
+
json: jest.fn().mockResolvedValue({
|
|
117
|
+
id: 9999,
|
|
118
|
+
login: 'octocat',
|
|
119
|
+
name: 'The Octocat',
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
nativeFetch.fetch.mockResolvedValue(mockResponse);
|
|
123
|
+
const result = yield adapter.getAuthToken('token-123');
|
|
124
|
+
expect(result.login).toBe('octocat');
|
|
125
|
+
expect(nativeFetch.fetch).toHaveBeenCalledWith('https://api.github.com/user', expect.objectContaining({
|
|
126
|
+
headers: {
|
|
127
|
+
'Authorization': 'Bearer token-123',
|
|
128
|
+
'Accept': 'application/json',
|
|
129
|
+
'User-Agent': 'Quatrain-Auth-Github',
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
}));
|
|
133
|
+
});
|
|
134
|
+
describe('checkRepositoryExists()', () => {
|
|
135
|
+
it('should return true if repo exists', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
136
|
+
const mockResponse = { status: 200 };
|
|
137
|
+
nativeFetch.fetch.mockResolvedValue(mockResponse);
|
|
138
|
+
const exists = yield adapter.checkRepositoryExists('token-123', 'owner', 'repo');
|
|
139
|
+
expect(exists).toBe(true);
|
|
140
|
+
expect(nativeFetch.fetch).toHaveBeenCalledWith('https://api.github.com/repos/owner/repo', expect.any(Object));
|
|
141
|
+
}));
|
|
142
|
+
it('should return false if repo does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
143
|
+
const mockResponse = { status: 404 };
|
|
144
|
+
nativeFetch.fetch.mockResolvedValue(mockResponse);
|
|
145
|
+
const exists = yield adapter.checkRepositoryExists('token-123', 'owner', 'repo');
|
|
146
|
+
expect(exists).toBe(false);
|
|
147
|
+
}));
|
|
148
|
+
});
|
|
149
|
+
describe('createRepository()', () => {
|
|
150
|
+
it('should call GitHub user repos endpoint to create a repo', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
+
const mockResponse = {
|
|
152
|
+
ok: true,
|
|
153
|
+
json: jest.fn().mockResolvedValue({
|
|
154
|
+
id: 123456,
|
|
155
|
+
name: 'new-repo',
|
|
156
|
+
full_name: 'octocat/new-repo',
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
nativeFetch.fetch.mockResolvedValue(mockResponse);
|
|
160
|
+
const result = yield adapter.createRepository('token-123', 'new-repo', {
|
|
161
|
+
private: true,
|
|
162
|
+
description: 'My new repo',
|
|
163
|
+
});
|
|
164
|
+
expect(result.name).toBe('new-repo');
|
|
165
|
+
expect(nativeFetch.fetch).toHaveBeenCalledWith('https://api.github.com/user/repos', expect.objectContaining({
|
|
166
|
+
method: 'POST',
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
name: 'new-repo',
|
|
169
|
+
private: true,
|
|
170
|
+
description: 'My new repo',
|
|
171
|
+
auto_init: true,
|
|
172
|
+
}),
|
|
173
|
+
}));
|
|
174
|
+
}));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
//# sourceMappingURL=GithubAuthAdapter.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GithubAuthAdapter.test.js","sourceRoot":"","sources":["../src/GithubAuthAdapter.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ServerAdapter } from '@quatrain/api';
|
|
2
|
+
/**
|
|
3
|
+
* Pluggable router endpoints for Github OAuth authentication.
|
|
4
|
+
* Compatible with any Quatrain ServerAdapter (Astro, Express, etc.).
|
|
5
|
+
*/
|
|
6
|
+
export declare function GithubAuthApi(router: ServerAdapter, path: string, options?: any): void;
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.GithubAuthApi = GithubAuthApi;
|
|
13
|
+
const GithubAuthAdapter_1 = require("./GithubAuthAdapter");
|
|
14
|
+
/**
|
|
15
|
+
* Pluggable router endpoints for Github OAuth authentication.
|
|
16
|
+
* Compatible with any Quatrain ServerAdapter (Astro, Express, etc.).
|
|
17
|
+
*/
|
|
18
|
+
function GithubAuthApi(router, path, options = {}) {
|
|
19
|
+
const adapter = options.adapter || GithubAuthAdapter_1.GithubAuthAdapter.factory(options.config || {});
|
|
20
|
+
if (!adapter) {
|
|
21
|
+
throw new Error('GithubAuthApi requires a valid GithubAuthAdapter or configuration.');
|
|
22
|
+
}
|
|
23
|
+
// Route: GET [root]/login
|
|
24
|
+
router.get('/login', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
var _a, _b;
|
|
26
|
+
const redirectUri = req.query.redirect_uri;
|
|
27
|
+
const scopes = (req.query.scopes || 'repo').split(',');
|
|
28
|
+
const state = req.query.state;
|
|
29
|
+
const authUrl = adapter.getAuthorizationUrl(redirectUri, scopes, state);
|
|
30
|
+
const wantsJson = req.query.json === 'true' || ((_b = (_a = req.headers) === null || _a === void 0 ? void 0 : _a.accept) === null || _b === void 0 ? void 0 : _b.includes('application/json'));
|
|
31
|
+
if (wantsJson) {
|
|
32
|
+
res.status(200).json({ url: authUrl });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
res.status(302).setHeader('Location', authUrl);
|
|
36
|
+
res.send(`Redirecting to GitHub...`);
|
|
37
|
+
}));
|
|
38
|
+
// Route: GET [root]/callback
|
|
39
|
+
router.get('/callback', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
40
|
+
const code = req.query.code;
|
|
41
|
+
const redirectUri = req.query.redirect_uri;
|
|
42
|
+
const platform = req.query.platform;
|
|
43
|
+
if (!code) {
|
|
44
|
+
res.status(400).send('Missing authorization code.');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const tokenData = yield adapter.exchangeCodeForToken(code, redirectUri);
|
|
49
|
+
// Support standard deep link redirect for mobile context if an app scheme is configured
|
|
50
|
+
const appScheme = req.query.app_scheme || options.appScheme;
|
|
51
|
+
if (appScheme) {
|
|
52
|
+
const redirectUrl = `${appScheme}://auth/github/callback?token=${tokenData.access_token}`;
|
|
53
|
+
res.status(302).setHeader('Location', redirectUrl);
|
|
54
|
+
res.send(`Redirecting back to app...`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Support standard redirect for web context if a target URI is configured
|
|
58
|
+
const webRedirectUri = req.query.web_redirect_uri || options.webRedirectUri;
|
|
59
|
+
if (webRedirectUri) {
|
|
60
|
+
const separator = webRedirectUri.includes('?') ? '&' : '?';
|
|
61
|
+
const redirectUrl = `${webRedirectUri}${separator}token=${tokenData.access_token}`;
|
|
62
|
+
res.status(302).setHeader('Location', redirectUrl);
|
|
63
|
+
res.send(`Redirecting...`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
res.status(200).json(tokenData);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
res.status(500).send(`OAuth error: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=GithubAuthApi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GithubAuthApi.js","sourceRoot":"","sources":["../src/GithubAuthApi.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const GithubAuthApi_1 = require("./GithubAuthApi");
|
|
13
|
+
describe('GithubAuthApi', () => {
|
|
14
|
+
let mockRouter;
|
|
15
|
+
let mockAdapter;
|
|
16
|
+
let routes = {};
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
routes = {};
|
|
19
|
+
mockRouter = {
|
|
20
|
+
get: jest.fn().mockImplementation((path, handler) => {
|
|
21
|
+
routes[path] = handler;
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
mockAdapter = {
|
|
25
|
+
getAuthorizationUrl: jest.fn().mockReturnValue('https://github.com/login/authorize'),
|
|
26
|
+
exchangeCodeForToken: jest.fn().mockResolvedValue({
|
|
27
|
+
access_token: 'token-abc',
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
it('should register /login and /callback routes', () => {
|
|
32
|
+
(0, GithubAuthApi_1.GithubAuthApi)(mockRouter, '/api/auth/github', {
|
|
33
|
+
adapter: mockAdapter,
|
|
34
|
+
});
|
|
35
|
+
expect(mockRouter.get).toHaveBeenCalledWith('/login', expect.any(Function));
|
|
36
|
+
expect(mockRouter.get).toHaveBeenCalledWith('/callback', expect.any(Function));
|
|
37
|
+
});
|
|
38
|
+
it('should handle /login route redirect', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
39
|
+
(0, GithubAuthApi_1.GithubAuthApi)(mockRouter, '/api/auth/github', {
|
|
40
|
+
adapter: mockAdapter,
|
|
41
|
+
});
|
|
42
|
+
const req = {
|
|
43
|
+
query: {
|
|
44
|
+
redirect_uri: 'http://my-callback',
|
|
45
|
+
scopes: 'repo,user',
|
|
46
|
+
state: 'mystate',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const res = {
|
|
50
|
+
status: jest.fn().mockReturnThis(),
|
|
51
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
52
|
+
send: jest.fn(),
|
|
53
|
+
};
|
|
54
|
+
yield routes['/login'](req, res);
|
|
55
|
+
expect(mockAdapter.getAuthorizationUrl).toHaveBeenCalledWith('http://my-callback', ['repo', 'user'], 'mystate');
|
|
56
|
+
expect(res.status).toHaveBeenCalledWith(302);
|
|
57
|
+
expect(res.setHeader).toHaveBeenCalledWith('Location', 'https://github.com/login/authorize');
|
|
58
|
+
}));
|
|
59
|
+
it('should handle /callback route token exchange', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
60
|
+
(0, GithubAuthApi_1.GithubAuthApi)(mockRouter, '/api/auth/github', {
|
|
61
|
+
adapter: mockAdapter,
|
|
62
|
+
});
|
|
63
|
+
const req = {
|
|
64
|
+
query: {
|
|
65
|
+
code: 'code-123',
|
|
66
|
+
redirect_uri: 'http://my-callback',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const res = {
|
|
70
|
+
status: jest.fn().mockReturnThis(),
|
|
71
|
+
json: jest.fn(),
|
|
72
|
+
};
|
|
73
|
+
yield routes['/callback'](req, res);
|
|
74
|
+
expect(mockAdapter.exchangeCodeForToken).toHaveBeenCalledWith('code-123', 'http://my-callback');
|
|
75
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
76
|
+
expect(res.json).toHaveBeenCalledWith({ access_token: 'token-abc' });
|
|
77
|
+
}));
|
|
78
|
+
it('should redirect to deep link for mobile platform', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
+
(0, GithubAuthApi_1.GithubAuthApi)(mockRouter, '/api/auth/github', {
|
|
80
|
+
adapter: mockAdapter,
|
|
81
|
+
appScheme: 'myapp',
|
|
82
|
+
});
|
|
83
|
+
const req = {
|
|
84
|
+
query: {
|
|
85
|
+
code: 'code-123',
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const res = {
|
|
89
|
+
status: jest.fn().mockReturnThis(),
|
|
90
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
91
|
+
send: jest.fn(),
|
|
92
|
+
};
|
|
93
|
+
yield routes['/callback'](req, res);
|
|
94
|
+
expect(res.status).toHaveBeenCalledWith(302);
|
|
95
|
+
expect(res.setHeader).toHaveBeenCalledWith('Location', 'myapp://auth/github/callback?token=token-abc');
|
|
96
|
+
}));
|
|
97
|
+
it('should redirect to web URL if webRedirectUri is configured', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
98
|
+
(0, GithubAuthApi_1.GithubAuthApi)(mockRouter, '/api/auth/github', {
|
|
99
|
+
adapter: mockAdapter,
|
|
100
|
+
webRedirectUri: 'http://localhost:3000/settings?tab=sync',
|
|
101
|
+
});
|
|
102
|
+
const req = {
|
|
103
|
+
query: {
|
|
104
|
+
code: 'code-123',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const res = {
|
|
108
|
+
status: jest.fn().mockReturnThis(),
|
|
109
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
110
|
+
send: jest.fn(),
|
|
111
|
+
};
|
|
112
|
+
yield routes['/callback'](req, res);
|
|
113
|
+
expect(res.status).toHaveBeenCalledWith(302);
|
|
114
|
+
expect(res.setHeader).toHaveBeenCalledWith('Location', 'http://localhost:3000/settings?tab=sync&token=token-abc');
|
|
115
|
+
}));
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=GithubAuthApi.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GithubAuthApi.test.js","sourceRoot":"","sources":["../src/GithubAuthApi.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./GithubAuthAdapter"), exports);
|
|
18
|
+
__exportStar(require("./GithubAuthApi"), exports);
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAmC;AACnC,kDAA+B"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quatrain/auth-github",
|
|
3
|
+
"version": "1.2.10",
|
|
4
|
+
"license": "AGPL-3.0-only",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bun": "src/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"LICENSE.md",
|
|
10
|
+
"src/",
|
|
11
|
+
"dist/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Quatrain/Core.git",
|
|
17
|
+
"directory": "packages/auth-github"
|
|
18
|
+
},
|
|
19
|
+
"author": "Quatrain Développement SAS <developers@quatrain.com>",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@quatrain/api": "^1.1.7",
|
|
22
|
+
"@quatrain/auth": "^1.2.10",
|
|
23
|
+
"@quatrain/backend": "^1.2.17",
|
|
24
|
+
"node-fetch-native": "^1.6.4"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@tsconfig/recommended": "^1.0.1",
|
|
28
|
+
"@types/node": "^22.10.1",
|
|
29
|
+
"typescript": "^5.2.2"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"wbuild": "tsc --watch",
|
|
34
|
+
"bump-to": "yarn version"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { GithubAuthAdapter } from './GithubAuthAdapter'
|
|
2
|
+
import * as nativeFetch from 'node-fetch-native'
|
|
3
|
+
|
|
4
|
+
jest.mock('node-fetch-native')
|
|
5
|
+
|
|
6
|
+
describe('GithubAuthAdapter', () => {
|
|
7
|
+
let adapter: GithubAuthAdapter
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks()
|
|
11
|
+
|
|
12
|
+
adapter = new GithubAuthAdapter({
|
|
13
|
+
config: {
|
|
14
|
+
clientId: 'test-client-id',
|
|
15
|
+
clientSecret: 'test-client-secret',
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('factory()', () => {
|
|
21
|
+
it('should return null if clientId or clientSecret is missing', () => {
|
|
22
|
+
expect(GithubAuthAdapter.factory({})).toBeNull()
|
|
23
|
+
expect(GithubAuthAdapter.factory({ clientId: 'id' })).toBeNull()
|
|
24
|
+
expect(GithubAuthAdapter.factory({ clientSecret: 'secret' })).toBeNull()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should return an instance if config is valid', () => {
|
|
28
|
+
const instance = GithubAuthAdapter.factory({
|
|
29
|
+
clientId: 'id',
|
|
30
|
+
clientSecret: 'secret',
|
|
31
|
+
})
|
|
32
|
+
expect(instance).toBeInstanceOf(GithubAuthAdapter)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('getAuthorizationUrl()', () => {
|
|
37
|
+
it('should return correct authorization URL', () => {
|
|
38
|
+
const url = adapter.getAuthorizationUrl(
|
|
39
|
+
'http://callback',
|
|
40
|
+
['repo', 'user'],
|
|
41
|
+
'state-123'
|
|
42
|
+
)
|
|
43
|
+
expect(url).toBe(
|
|
44
|
+
'https://github.com/login/oauth/authorize?client_id=test-client-id&scope=repo%20user&redirect_uri=http%3A%2F%2Fcallback&state=state-123'
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('exchangeCodeForToken()', () => {
|
|
50
|
+
it('should exchange code for token successfully', async () => {
|
|
51
|
+
const mockResponse = {
|
|
52
|
+
ok: true,
|
|
53
|
+
json: jest.fn().mockResolvedValue({
|
|
54
|
+
access_token: 'github-access-token-123',
|
|
55
|
+
token_type: 'bearer',
|
|
56
|
+
scope: 'repo',
|
|
57
|
+
}),
|
|
58
|
+
}
|
|
59
|
+
;(nativeFetch.fetch as jest.Mock).mockResolvedValue(mockResponse)
|
|
60
|
+
|
|
61
|
+
const result = await adapter.exchangeCodeForToken('code-123', 'http://callback')
|
|
62
|
+
expect(result.access_token).toBe('github-access-token-123')
|
|
63
|
+
expect(nativeFetch.fetch).toHaveBeenCalledWith(
|
|
64
|
+
'https://github.com/login/oauth/access_token',
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
method: 'POST',
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
client_id: 'test-client-id',
|
|
69
|
+
client_secret: 'test-client-secret',
|
|
70
|
+
code: 'code-123',
|
|
71
|
+
grant_type: 'authorization_code',
|
|
72
|
+
redirect_uri: 'http://callback',
|
|
73
|
+
}),
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should throw error on failed exchange request', async () => {
|
|
79
|
+
const mockResponse = {
|
|
80
|
+
ok: false,
|
|
81
|
+
statusText: 'Bad Request',
|
|
82
|
+
}
|
|
83
|
+
;(nativeFetch.fetch as jest.Mock).mockResolvedValue(mockResponse)
|
|
84
|
+
|
|
85
|
+
await expect(
|
|
86
|
+
adapter.exchangeCodeForToken('code-123')
|
|
87
|
+
).rejects.toThrow('OAuth token exchange failed: Bad Request')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('getAuthToken()', () => {
|
|
92
|
+
it('should fetch and return user profile information', async () => {
|
|
93
|
+
const mockResponse = {
|
|
94
|
+
ok: true,
|
|
95
|
+
json: jest.fn().mockResolvedValue({
|
|
96
|
+
id: 9999,
|
|
97
|
+
login: 'octocat',
|
|
98
|
+
name: 'The Octocat',
|
|
99
|
+
}),
|
|
100
|
+
}
|
|
101
|
+
;(nativeFetch.fetch as jest.Mock).mockResolvedValue(mockResponse)
|
|
102
|
+
|
|
103
|
+
const result = await adapter.getAuthToken('token-123')
|
|
104
|
+
expect(result.login).toBe('octocat')
|
|
105
|
+
expect(nativeFetch.fetch).toHaveBeenCalledWith(
|
|
106
|
+
'https://api.github.com/user',
|
|
107
|
+
expect.objectContaining({
|
|
108
|
+
headers: {
|
|
109
|
+
'Authorization': 'Bearer token-123',
|
|
110
|
+
'Accept': 'application/json',
|
|
111
|
+
'User-Agent': 'Quatrain-Auth-Github',
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('checkRepositoryExists()', () => {
|
|
119
|
+
it('should return true if repo exists', async () => {
|
|
120
|
+
const mockResponse = { status: 200 }
|
|
121
|
+
;(nativeFetch.fetch as jest.Mock).mockResolvedValue(mockResponse)
|
|
122
|
+
|
|
123
|
+
const exists = await adapter.checkRepositoryExists('token-123', 'owner', 'repo')
|
|
124
|
+
expect(exists).toBe(true)
|
|
125
|
+
expect(nativeFetch.fetch).toHaveBeenCalledWith(
|
|
126
|
+
'https://api.github.com/repos/owner/repo',
|
|
127
|
+
expect.any(Object)
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should return false if repo does not exist', async () => {
|
|
132
|
+
const mockResponse = { status: 404 }
|
|
133
|
+
;(nativeFetch.fetch as jest.Mock).mockResolvedValue(mockResponse)
|
|
134
|
+
|
|
135
|
+
const exists = await adapter.checkRepositoryExists('token-123', 'owner', 'repo')
|
|
136
|
+
expect(exists).toBe(false)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('createRepository()', () => {
|
|
141
|
+
it('should call GitHub user repos endpoint to create a repo', async () => {
|
|
142
|
+
const mockResponse = {
|
|
143
|
+
ok: true,
|
|
144
|
+
json: jest.fn().mockResolvedValue({
|
|
145
|
+
id: 123456,
|
|
146
|
+
name: 'new-repo',
|
|
147
|
+
full_name: 'octocat/new-repo',
|
|
148
|
+
}),
|
|
149
|
+
}
|
|
150
|
+
;(nativeFetch.fetch as jest.Mock).mockResolvedValue(mockResponse)
|
|
151
|
+
|
|
152
|
+
const result = await adapter.createRepository('token-123', 'new-repo', {
|
|
153
|
+
private: true,
|
|
154
|
+
description: 'My new repo',
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
expect(result.name).toBe('new-repo')
|
|
158
|
+
expect(nativeFetch.fetch).toHaveBeenCalledWith(
|
|
159
|
+
'https://api.github.com/user/repos',
|
|
160
|
+
expect.objectContaining({
|
|
161
|
+
method: 'POST',
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
name: 'new-repo',
|
|
164
|
+
private: true,
|
|
165
|
+
description: 'My new repo',
|
|
166
|
+
auto_init: true,
|
|
167
|
+
}),
|
|
168
|
+
})
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { AbstractOAuthAdapter } from '@quatrain/auth'
|
|
2
|
+
import { GithubAuthApi } from './GithubAuthApi'
|
|
3
|
+
import * as nativeFetch from 'node-fetch-native'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Authentication adapter for GitHub OAuth 2.0 Web Application Flow.
|
|
7
|
+
* Handles profiles, repository existence checks, and repository creation.
|
|
8
|
+
*/
|
|
9
|
+
export class GithubAuthAdapter extends AbstractOAuthAdapter {
|
|
10
|
+
protected _authorizationEndpoint = 'https://github.com/login/oauth/authorize'
|
|
11
|
+
protected _tokenEndpoint = 'https://github.com/login/oauth/access_token'
|
|
12
|
+
protected _userProfileEndpoint = 'https://api.github.com/user'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Factory method to create a new instance from config credentials.
|
|
16
|
+
*/
|
|
17
|
+
static factory(config: any): GithubAuthAdapter | null {
|
|
18
|
+
if (!config.clientId || !config.clientSecret) return null
|
|
19
|
+
return new GithubAuthAdapter({ config })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns the pluggable API routes builder.
|
|
24
|
+
*/
|
|
25
|
+
public override getEndpointHandler(): any {
|
|
26
|
+
return GithubAuthApi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns user details from GitHub by validating the access token.
|
|
31
|
+
*/
|
|
32
|
+
async getAuthToken(accessToken: string): Promise<any> {
|
|
33
|
+
const response = await nativeFetch.fetch(this._userProfileEndpoint, {
|
|
34
|
+
headers: {
|
|
35
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
36
|
+
'Accept': 'application/json',
|
|
37
|
+
'User-Agent': 'Quatrain-Auth-Github'
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error(`Failed to fetch Github user profile: ${response.statusText}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return await response.json()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Custom GitHub API call to check repository existence.
|
|
50
|
+
*/
|
|
51
|
+
async checkRepositoryExists(accessToken: string, owner: string, name: string): Promise<boolean> {
|
|
52
|
+
const response = await nativeFetch.fetch(`https://api.github.com/repos/${owner}/${name}`, {
|
|
53
|
+
headers: {
|
|
54
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
55
|
+
'Accept': 'application/json',
|
|
56
|
+
'User-Agent': 'Quatrain-Auth-Github'
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
return response.status === 200
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Custom GitHub API call to create a repository.
|
|
64
|
+
*/
|
|
65
|
+
async createRepository(accessToken: string, name: string, options: { private?: boolean; description?: string; autoInit?: boolean } = {}): Promise<any> {
|
|
66
|
+
const response = await nativeFetch.fetch('https://api.github.com/user/repos', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
'Accept': 'application/json',
|
|
72
|
+
'User-Agent': 'Quatrain-Auth-Github'
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
name,
|
|
76
|
+
private: options.private !== false,
|
|
77
|
+
description: options.description || 'Dépôt créé automatiquement par Modaka',
|
|
78
|
+
auto_init: options.autoInit !== false
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const errData = await response.json().catch(() => ({}))
|
|
84
|
+
throw new Error(`Failed to create repository: ${errData.message || response.statusText}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return await response.json()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { GithubAuthApi } from './GithubAuthApi'
|
|
2
|
+
import { ServerAdapter, ApiRequest, ApiResponse } from '@quatrain/api'
|
|
3
|
+
import { GithubAuthAdapter } from './GithubAuthAdapter'
|
|
4
|
+
|
|
5
|
+
describe('GithubAuthApi', () => {
|
|
6
|
+
let mockRouter: any
|
|
7
|
+
let mockAdapter: any
|
|
8
|
+
let routes: Record<string, Function> = {}
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
routes = {}
|
|
12
|
+
mockRouter = {
|
|
13
|
+
get: jest.fn().mockImplementation((path: string, handler: Function) => {
|
|
14
|
+
routes[path] = handler
|
|
15
|
+
}),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
mockAdapter = {
|
|
19
|
+
getAuthorizationUrl: jest.fn().mockReturnValue('https://github.com/login/authorize'),
|
|
20
|
+
exchangeCodeForToken: jest.fn().mockResolvedValue({
|
|
21
|
+
access_token: 'token-abc',
|
|
22
|
+
}),
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should register /login and /callback routes', () => {
|
|
27
|
+
GithubAuthApi(mockRouter as unknown as ServerAdapter, '/api/auth/github', {
|
|
28
|
+
adapter: mockAdapter,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
expect(mockRouter.get).toHaveBeenCalledWith('/login', expect.any(Function))
|
|
32
|
+
expect(mockRouter.get).toHaveBeenCalledWith('/callback', expect.any(Function))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should handle /login route redirect', async () => {
|
|
36
|
+
GithubAuthApi(mockRouter as unknown as ServerAdapter, '/api/auth/github', {
|
|
37
|
+
adapter: mockAdapter,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const req = {
|
|
41
|
+
query: {
|
|
42
|
+
redirect_uri: 'http://my-callback',
|
|
43
|
+
scopes: 'repo,user',
|
|
44
|
+
state: 'mystate',
|
|
45
|
+
},
|
|
46
|
+
} as unknown as ApiRequest
|
|
47
|
+
|
|
48
|
+
const res = {
|
|
49
|
+
status: jest.fn().mockReturnThis(),
|
|
50
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
51
|
+
send: jest.fn(),
|
|
52
|
+
} as unknown as ApiResponse
|
|
53
|
+
|
|
54
|
+
await routes['/login'](req, res)
|
|
55
|
+
|
|
56
|
+
expect(mockAdapter.getAuthorizationUrl).toHaveBeenCalledWith(
|
|
57
|
+
'http://my-callback',
|
|
58
|
+
['repo', 'user'],
|
|
59
|
+
'mystate'
|
|
60
|
+
)
|
|
61
|
+
expect(res.status).toHaveBeenCalledWith(302)
|
|
62
|
+
expect(res.setHeader).toHaveBeenCalledWith('Location', 'https://github.com/login/authorize')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should handle /callback route token exchange', async () => {
|
|
66
|
+
GithubAuthApi(mockRouter as unknown as ServerAdapter, '/api/auth/github', {
|
|
67
|
+
adapter: mockAdapter,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const req = {
|
|
71
|
+
query: {
|
|
72
|
+
code: 'code-123',
|
|
73
|
+
redirect_uri: 'http://my-callback',
|
|
74
|
+
},
|
|
75
|
+
} as unknown as ApiRequest
|
|
76
|
+
|
|
77
|
+
const res = {
|
|
78
|
+
status: jest.fn().mockReturnThis(),
|
|
79
|
+
json: jest.fn(),
|
|
80
|
+
} as unknown as ApiResponse
|
|
81
|
+
|
|
82
|
+
await routes['/callback'](req, res)
|
|
83
|
+
|
|
84
|
+
expect(mockAdapter.exchangeCodeForToken).toHaveBeenCalledWith('code-123', 'http://my-callback')
|
|
85
|
+
expect(res.status).toHaveBeenCalledWith(200)
|
|
86
|
+
expect(res.json).toHaveBeenCalledWith({ access_token: 'token-abc' })
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should redirect to deep link for mobile platform', async () => {
|
|
90
|
+
GithubAuthApi(mockRouter as unknown as ServerAdapter, '/api/auth/github', {
|
|
91
|
+
adapter: mockAdapter,
|
|
92
|
+
appScheme: 'myapp',
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const req = {
|
|
96
|
+
query: {
|
|
97
|
+
code: 'code-123',
|
|
98
|
+
},
|
|
99
|
+
} as unknown as ApiRequest
|
|
100
|
+
|
|
101
|
+
const res = {
|
|
102
|
+
status: jest.fn().mockReturnThis(),
|
|
103
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
104
|
+
send: jest.fn(),
|
|
105
|
+
} as unknown as ApiResponse
|
|
106
|
+
|
|
107
|
+
await routes['/callback'](req, res)
|
|
108
|
+
|
|
109
|
+
expect(res.status).toHaveBeenCalledWith(302)
|
|
110
|
+
expect(res.setHeader).toHaveBeenCalledWith(
|
|
111
|
+
'Location',
|
|
112
|
+
'myapp://auth/github/callback?token=token-abc'
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should redirect to web URL if webRedirectUri is configured', async () => {
|
|
117
|
+
GithubAuthApi(mockRouter as unknown as ServerAdapter, '/api/auth/github', {
|
|
118
|
+
adapter: mockAdapter,
|
|
119
|
+
webRedirectUri: 'http://localhost:3000/settings?tab=sync',
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const req = {
|
|
123
|
+
query: {
|
|
124
|
+
code: 'code-123',
|
|
125
|
+
},
|
|
126
|
+
} as unknown as ApiRequest
|
|
127
|
+
|
|
128
|
+
const res = {
|
|
129
|
+
status: jest.fn().mockReturnThis(),
|
|
130
|
+
setHeader: jest.fn().mockReturnThis(),
|
|
131
|
+
send: jest.fn(),
|
|
132
|
+
} as unknown as ApiResponse
|
|
133
|
+
|
|
134
|
+
await routes['/callback'](req, res)
|
|
135
|
+
|
|
136
|
+
expect(res.status).toHaveBeenCalledWith(302)
|
|
137
|
+
expect(res.setHeader).toHaveBeenCalledWith(
|
|
138
|
+
'Location',
|
|
139
|
+
'http://localhost:3000/settings?tab=sync&token=token-abc'
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ServerAdapter, ApiRequest, ApiResponse } from '@quatrain/api'
|
|
2
|
+
import { GithubAuthAdapter } from './GithubAuthAdapter'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pluggable router endpoints for Github OAuth authentication.
|
|
6
|
+
* Compatible with any Quatrain ServerAdapter (Astro, Express, etc.).
|
|
7
|
+
*/
|
|
8
|
+
export function GithubAuthApi(router: ServerAdapter, path: string, options: any = {}) {
|
|
9
|
+
const adapter = options.adapter || GithubAuthAdapter.factory(options.config || {})
|
|
10
|
+
if (!adapter) {
|
|
11
|
+
throw new Error('GithubAuthApi requires a valid GithubAuthAdapter or configuration.')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Route: GET [root]/login
|
|
15
|
+
router.get('/login', async (req: ApiRequest, res: ApiResponse) => {
|
|
16
|
+
const redirectUri = req.query.redirect_uri as string
|
|
17
|
+
const scopes = (req.query.scopes as string || 'repo').split(',')
|
|
18
|
+
const state = req.query.state as string
|
|
19
|
+
|
|
20
|
+
const authUrl = adapter.getAuthorizationUrl(redirectUri, scopes, state)
|
|
21
|
+
|
|
22
|
+
const wantsJson = req.query.json === 'true' || req.headers?.accept?.includes('application/json')
|
|
23
|
+
if (wantsJson) {
|
|
24
|
+
res.status(200).json({ url: authUrl })
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
res.status(302).setHeader('Location', authUrl)
|
|
29
|
+
res.send(`Redirecting to GitHub...`)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Route: GET [root]/callback
|
|
33
|
+
router.get('/callback', async (req: ApiRequest, res: ApiResponse) => {
|
|
34
|
+
const code = req.query.code as string
|
|
35
|
+
const redirectUri = req.query.redirect_uri as string
|
|
36
|
+
const platform = req.query.platform as string
|
|
37
|
+
|
|
38
|
+
if (!code) {
|
|
39
|
+
res.status(400).send('Missing authorization code.')
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const tokenData = await adapter.exchangeCodeForToken(code, redirectUri)
|
|
45
|
+
|
|
46
|
+
// Support standard deep link redirect for mobile context if an app scheme is configured
|
|
47
|
+
const appScheme = req.query.app_scheme as string || options.appScheme
|
|
48
|
+
if (appScheme) {
|
|
49
|
+
const redirectUrl = `${appScheme}://auth/github/callback?token=${tokenData.access_token}`
|
|
50
|
+
res.status(302).setHeader('Location', redirectUrl)
|
|
51
|
+
res.send(`Redirecting back to app...`)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Support standard redirect for web context if a target URI is configured
|
|
56
|
+
const webRedirectUri = req.query.web_redirect_uri as string || options.webRedirectUri
|
|
57
|
+
if (webRedirectUri) {
|
|
58
|
+
const separator = webRedirectUri.includes('?') ? '&' : '?'
|
|
59
|
+
const redirectUrl = `${webRedirectUri}${separator}token=${tokenData.access_token}`
|
|
60
|
+
res.status(302).setHeader('Location', redirectUrl)
|
|
61
|
+
res.send(`Redirecting...`)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
res.status(200).json(tokenData)
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
res.status(500).send(`OAuth error: ${err.message}`)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
package/src/index.ts
ADDED