11import cookie from "cookie" ;
2- import {
3- assertRequestBody ,
4- defineRoute ,
5- HTTPError ,
6- Store ,
7- Structure ,
8- } from "gruber" ;
2+ import { assertRequestBody , defineRoute , HTTPError , Structure } from "gruber" ;
93
104import {
115 commponDependencies ,
126 ConferenceRecord ,
13- EmailService ,
147 emailStructure ,
8+ GOOGLE_CALENDAR_SCOPE ,
9+ GoogleRepo ,
1510 trimEmail ,
11+ undefinedStructure ,
1612} from "../lib/mod.ts" ;
17- import { LoginRequest } from "./auth-lib.ts" ;
13+ import { AuthLib , LoginRequest , parseScopes } from "./auth-lib.ts" ;
1814import { AuthRepo } from "./auth-repo.ts" ;
1915
2016const LoginBody = Structure . union ( [
2117 Structure . object ( {
18+ type : Structure . literal ( "email" ) ,
2219 emailAddress : emailStructure ( ) ,
2320 redirectUri : Structure . string ( ) ,
2421 conferenceId : Structure . union ( [ Structure . null ( ) , Structure . number ( ) ] ) ,
2522 } ) ,
23+ Structure . object ( {
24+ type : Structure . literal ( "oauth" ) ,
25+ provider : Structure . literal ( "google" ) ,
26+ redirectUri : Structure . string ( ) ,
27+ conferenceId : Structure . union ( [ Structure . null ( ) , Structure . number ( ) ] ) ,
28+ scope : Structure . union ( [ Structure . string ( ) , undefinedStructure ( ) ] ) ,
29+ } ) ,
2630] ) ;
2731
28- // This is exported so that it can be used in the tito webhook too
29- export async function _startEmailLogin (
30- store : Store ,
31- email : EmailService ,
32- login : Omit < LoginRequest , "method" | "payload" > ,
33- emailAddress : string ,
34- maxAge : number ,
35- ) {
36- // Store the login to be retrieved on clicking through from the email
37- store . set < LoginRequest > (
38- `/auth/request/${ login . token } ` ,
39- {
40- ...login ,
41- method : "email" ,
42- payload : { emailAddress } ,
43- } ,
44- { maxAge } ,
45- ) ;
46-
47- // Generate the magic link to send the client with the token + code in it
48- const magicLink = new URL ( login . redirectUri ) ;
49- magicLink . searchParams . set ( "method" , "email" ) ;
50- magicLink . searchParams . set ( "token" , login . token ) ;
51- magicLink . searchParams . set ( "code" , login . code . toString ( ) ) ;
52-
53- // Send the email
54- const sent = await email . sendTemplated ( {
55- to : { emailAddress } ,
56- type : "login" ,
57- arguments : {
58- oneTimeCode : login . code ,
59- magicLink : magicLink . toString ( ) ,
60- } ,
61- } ) ;
62-
63- if ( ! sent ) {
64- throw HTTPError . internalServerError ( "login email failed" ) ;
65- }
66- }
67-
6832function stripRedirect ( input : string ) {
6933 const url = new URL ( input ) ;
7034 url . search = "" ;
@@ -89,13 +53,14 @@ export const loginRoute = defineRoute({
8953 dependencies : {
9054 ...commponDependencies ,
9155 repo : AuthRepo . use ,
56+ lib : AuthLib . use ,
57+ google : GoogleRepo . use ,
9258 } ,
93- async handler ( { request, authz , store, random, email , appConfig, repo } ) {
59+ async handler ( { request, store, random, lib , appConfig, repo, google } ) {
9460 // NOTE: it previously short-circuited the login if there was an active session
9561 // this cased confusion so was taken out
9662
9763 const body = await assertRequestBody ( LoginBody , request ) ;
98- const emailAddress = trimEmail ( body . emailAddress ) ;
9964
10065 const conference = body . conferenceId
10166 ? await repo . getConference ( body . conferenceId )
@@ -110,28 +75,32 @@ export const loginRoute = defineRoute({
11075 uses : 5 ,
11176 } ;
11277
113- if ( body . emailAddress ) {
78+ const cookieOptions = {
79+ httpOnly : true ,
80+ maxAge : appConfig . auth . loginMaxAge / 1_000 ,
81+ secure : appConfig . server . url . protocol === "https:" ,
82+ } ;
83+
84+ if ( body . type === "email" ) {
85+ const emailAddress = trimEmail ( body . emailAddress ) ;
86+
11487 // TODO: should it verify conference registration too?
11588 const user = await repo . getUserByEmail ( emailAddress ) ;
11689 if ( ! user ) throw HTTPError . unauthorized ( ) ;
11790
11891 // Send the email login, throwing if it fails
119- await _startEmailLogin (
120- store ,
121- email ,
122- login ,
123- emailAddress ,
124- appConfig . auth . loginMaxAge ,
125- ) ;
92+ await lib . startEmailLogin ( {
93+ ...login ,
94+ method : "email" ,
95+ payload : { emailAddress } ,
96+ } ) ;
12697
12798 // Set the login cookie on the client
12899 const headers = new Headers ( ) ;
129100 headers . set (
130101 "Set-Cookie" ,
131102 cookie . serialize ( appConfig . auth . loginCookie , login . token , {
132- httpOnly : true ,
133- maxAge : appConfig . auth . loginMaxAge / 1_000 ,
134- secure : appConfig . server . url . protocol === "https:" ,
103+ ...cookieOptions ,
135104 } ) ,
136105 ) ;
137106
@@ -140,6 +109,40 @@ export const loginRoute = defineRoute({
140109 return Response . json ( { token : login . token } , { headers } ) ;
141110 }
142111
112+ if ( body . type === "oauth" ) {
113+ const scope = [
114+ "https://www.googleapis.com/auth/userinfo.email" ,
115+ "openid" ,
116+ "profile" ,
117+ ] ;
118+
119+ const requested = body . scope
120+ ? parseScopes ( body . scope )
121+ : new Set < string > ( ) ;
122+
123+ if ( requested . has ( "calendar" ) ) {
124+ scope . push ( GOOGLE_CALENDAR_SCOPE ) ;
125+ }
126+
127+ await store . set < LoginRequest > ( `/auth/request/${ login . token } ` , {
128+ ...login ,
129+ method : "oauth" ,
130+ provider : "google" ,
131+ } ) ;
132+
133+ const headers = new Headers ( ) ;
134+ headers . set (
135+ "Set-Cookie" ,
136+ cookie . serialize ( "oauth2-state" , login . token , cookieOptions ) ,
137+ ) ;
138+
139+ const url = google . authUrl ( scope , login . token , requested ) ;
140+
141+ // NOTE: you cannot access headers.location from JavaScript
142+ // This method would make more sense as a GET request, then cookies can be set too
143+ return Response . json ( { location : url } , { headers } ) ;
144+ }
145+
143146 throw HTTPError . notImplemented ( ) ;
144147 } ,
145148} ) ;
0 commit comments