1+ import { Handler , APIGatewayProxyEventV2 , APIGatewayProxyResultV2 } from 'aws-lambda' ;
2+ import fetch from 'cross-fetch' ;
3+ import { left , right , isRight , Either } from 'fp-ts/Either' ;
4+
5+ interface Email {
6+ email : string ,
7+ primary : boolean ,
8+ verified : boolean ,
9+ visibility ?: 'private' | 'public'
10+ }
11+
12+ const getUserId = async ( token : string ) : Promise < Either < number , string > > => {
13+ const response = await fetch ( 'https://api.github.com/user' , {
14+ method : 'GET' ,
15+ headers : {
16+ 'Accept' : 'application/vnd.github+json' ,
17+ 'Authorization' : `Bearer ${ token } `
18+ }
19+ } ) ;
20+ if ( response . status !== 200 ) {
21+ return right ( `Cannot get user ID. status: ${ response . statusText } . ${ response . text ( ) } ` ) ;
22+ }
23+ const id = ( ( await response . json ( ) ) as {
24+ id : number ,
25+ } ) . id ;
26+
27+ return left ( id ) ;
28+ }
29+
30+ const getValidEmail = async ( token : string ) : Promise < Either < Email , string > > => {
31+ const response = await fetch ( 'https://api.github.com/user/emails' , {
32+ method : 'GET' ,
33+ headers : {
34+ 'Accept' : 'application/vnd.github+json' ,
35+ 'Authorization' : `Bearer ${ token } `
36+ }
37+ } ) ;
38+ if ( response . status !== 200 ) {
39+ return right ( `Cannot get mails. status: ${ response . statusText } . ${ response . text ( ) } ` ) ;
40+ }
41+ const emails = ( await response . json ( ) ) as {
42+ email : string ,
43+ primary : boolean ,
44+ verified : boolean ,
45+ visibility ?: 'private' | 'public'
46+ } [ ] ;
47+
48+ const email = emails . find ( it => it . primary && it . verified && it . email . trim ( ) . endsWith ( 'noreply.github.com' ) ) ;
49+ return email ? left ( email ) : right ( '/user/emails returned no valid emails' ) ;
50+ }
51+
52+ export const handler : Handler < APIGatewayProxyEventV2 , APIGatewayProxyResultV2 > = async ( event , _context , _callback ) => {
53+ const headers = event . headers ;
54+ const authHeader = headers [ 'authorization' ] || headers [ 'Authorization' ] ;
55+ if ( ! authHeader ) {
56+ return {
57+ cookies : [ ] ,
58+ statusCode : 400 ,
59+ body : '/userinfo request contained no accessToken' ,
60+ } ;
61+ }
62+ const authHeaderPrefix = authHeader . slice ( 0 , 'bearer ' . length ) ;
63+ if ( authHeaderPrefix . toLowerCase ( ) !== 'bearer ' ) {
64+ return {
65+ cookies : [ ] ,
66+ statusCode : 400 ,
67+ body : 'authorization header does not contain bearer token' ,
68+ } ;
69+ }
70+ const token = authHeader . slice ( 'bearer ' . length ) . trim ( ) ;
71+ if ( ! token ) {
72+ return {
73+ cookies : [ ] ,
74+ statusCode : 400 ,
75+ body : 'authorization header does not contain bearer token' ,
76+ } ;
77+ }
78+
79+ const [ idResult , emailResult ] = await Promise . all ( [
80+ getUserId ( token ) ,
81+ getValidEmail ( token )
82+ ] ) ;
83+ if ( isRight ( idResult ) ) {
84+ return {
85+ cookies : [ ] ,
86+ statusCode : 400 ,
87+ body : `/userinfo ${ idResult . right } ` ,
88+ } ;
89+ }
90+ if ( isRight ( emailResult ) ) {
91+ return {
92+ cookies : [ ] ,
93+ statusCode : 400 ,
94+ body : `/userinfo ${ emailResult . right } ` ,
95+ } ;
96+ }
97+
98+ const id = idResult . left ;
99+ const email = emailResult . left ;
100+ return {
101+ cookies : [ ] ,
102+ statusCode : 200 ,
103+ body : JSON . stringify ( {
104+ sub : id . toString ( ) ,
105+ email : email . email ,
106+ email_verified : email . verified ,
107+ } ) ,
108+ }
109+ }
0 commit comments