<commit_before>package routes;

import tink.core.Error;
import haxe.crypto.Base64;
import haxe.io.Bytes;

import tink.CoreApi.FutureTrigger;
import tink.http.Response.IncomingResponse;
import tink.CoreApi.Future;
import tink.CoreApi.Promise;
import tink.http.Header.HeaderName;
import tink.http.Header.HeaderField;
import tink.web.routing.*;
import tink.http.clients.SecureSocketClient;
import tink.http.Request;

import jwt.JWT;

import response.NotFoundResponse;

using StringTools;

typedef AccessResult = {
    var access_token:String;
    var token_type:String;
    var expires_in:Int;
    var id_token:String;
};

typedef GoogleIDPayload = {
    var email:String;
    var given_name:String;
    var family_name:String;
    var picture:String;
};

class OAuth2 {
    public function new() {}
    
    @:get('/login/$service') public function login(service:String):Response {
        var redirectURI:String = Server.config.siteroot + '/oauth2/redirect';

        var urlString:String = null;
        var clientID:String = null;
        var scope:String = null;
        var state:String = null;
        switch(service.toLowerCase()) {
            case 'google': {
                urlString = 'https://accounts.google.com/o/oauth2/v2/auth';
                clientID = Server.config.oauth2.google.id;
                scope = 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email';
                state = 'auth-google';
            }

            case 'facebook': {
                urlString = 'https://www.facebook.com/v2.10/dialog/oauth';
                clientID = Server.config.oauth2.facebook.id;
                scope = 'email public_profile';
                state = 'auth-facebook';
            }

            case _: return new NotFoundResponse(service);
        }

        urlString += '?client_id=${clientID.urlEncode()}';
        urlString += '&redirect_uri=${redirectURI.urlEncode()}';
        urlString += '&response_type=code';
        urlString += '&scope=${scope.urlEncode()}';
        urlString += '&access_type=offline';
        urlString += '&state=${state.urlEncode()}';
        var url:tink.Url = urlString;
        return url;
    }

    @:get public function redirect(query:{code:String, state:String}):Promise<Response> {
        var headers:Array<HeaderField> = new Array<HeaderField>();
        headers.push(new HeaderField(HeaderName.ContentType, 'application/x-www-form-urlencoded'));

        var redirectURI:String = Server.config.siteroot + '/oauth2/redirect';
        var host:String = null;
        var uri:String = null;
        var clientID:String = null;
        var clientSecret:String = null;

        switch(query.state.toLowerCase()) {
            case 'auth-google': {
                host = 'www.googleapis.com';
                uri = '/oauth2/v4/token';
                clientID = Server.config.oauth2.google.id;
                clientSecret = Server.config.oauth2.google.secret;
            }

            case 'auth-facebook': {
                host = 'graph.facebook.com';
                uri = '/v2.10/oauth/access_token';
                clientID = Server.config.oauth2.facebook.id;
                clientSecret = Server.config.oauth2.facebook.secret;
            }

            case _: return Future.sync(new NotFoundResponse(query.state));
        }

        var body:String = 'code=${query.code.urlEncode()}';
        body += '&client_id=${clientID.urlEncode()}';
        body += '&client_secret=${clientSecret.urlEncode()}';
        body += '&redirect_uri=${redirectURI.urlEncode()}';
        body += '&grant_type=authorization_code';

        headers.push(new HeaderField(HeaderName.ContentLength, body.length));

        var client = new SecureSocketClient();
        var req:Promise<IncomingResponse> = client.request(new OutgoingRequest(
            new OutgoingRequestHeader(POST, new tink.url.Host(host), uri, headers),
            body
        ));
        var derp = req.next(function(res:IncomingResponse):Promise<Bytes> {
            return res.body.all();
        })

        return switch(query.state.toLowerCase()) {
            case 'auth-google':
                derp.next(function(raw:Bytes) {
                    var result:AccessResult = haxe.Json.parse(raw.toString());
                    var payload:GoogleIDPayload = JWT.extract(result.id_token);

                    var users:List<models.User> = models.User.manager.search($email == payload.email);
                    var user:models.User = if(users.length < 1) {
                        // create a new user!
                        var u:models.User = new models.User();
                        u.name = payload.given_name + ' ' + payload.family_name;
                        u.email = payload.email;
                        u.insert();
                        Log.info('Created new user: ${u.name} <${u.email}>');
                        u;
                    }
                    else {
                        var u:models.User = users.first();
                        Log.info('Google user logged in: ${u.name} <${u.email}>');
                        u;
                    }

                    // TODO: some sort of redirect a la Auth0
                    var token:String = JWT.sign({
                        id: user.id
                    }, Server.config.jwt.secret);

                    return new response.JsonResponse({
                        token: token
                    });
                });

            case 'auth-facebook':
                derp.next(function(raw:Bytes) {
                    var result:Dynamic = haxe.Json.parse(raw.toString());
                    return new response.JsonResponse(result);
                })

            case _: Future.sync(new response.NotFoundResponse(query.state));
        }
    }
}<commit_msg>Finished Facebook OAuth2<commit_after>package routes;

import tink.core.Error;
import haxe.crypto.Base64;
import haxe.io.Bytes;

import tink.CoreApi.FutureTrigger;
import tink.http.Response.IncomingResponse;
import tink.CoreApi.Future;
import tink.CoreApi.Promise;
import tink.http.Header.HeaderName;
import tink.http.Header.HeaderField;
import tink.web.routing.*;
import tink.http.clients.SecureSocketClient;
import tink.http.Request;

import jwt.JWT;

import response.NotFoundResponse;

using StringTools;

class OAuth2 {
    public function new() {}
    
    @:get('/login/$service') public function login(service:String):Response {
        var redirectURI:String = Server.config.siteroot + '/oauth2/redirect';

        var urlString:String = null;
        var clientID:String = null;
        var scope:String = null;
        var state:String = null;
        switch(service.toLowerCase()) {
            case 'google': {
                urlString = 'https://accounts.google.com/o/oauth2/v2/auth';
                clientID = Server.config.oauth2.google.id;
                scope = 'openid profile';
                state = 'auth-google';
            }

            case 'facebook': {
                urlString = 'https://www.facebook.com/v2.10/dialog/oauth';
                clientID = Server.config.oauth2.facebook.id;
                scope = 'public_profile';
                state = 'auth-facebook';
            }

            case _: return new NotFoundResponse(service);
        }

        urlString += '?client_id=${clientID.urlEncode()}';
        urlString += '&redirect_uri=${redirectURI.urlEncode()}';
        urlString += '&response_type=code';
        urlString += '&scope=${scope.urlEncode()}';
        urlString += '&access_type=offline';
        urlString += '&state=${state.urlEncode()}';
        urlString += '&nonce=${Math.random()}';
        var url:tink.Url = urlString;
        return url;
    }

    @:get public function redirect(query:{code:String, state:String}):Promise<Response> {
        var headers:Array<HeaderField> = new Array<HeaderField>();
        headers.push(new HeaderField(HeaderName.ContentType, 'application/x-www-form-urlencoded'));

        var redirectURI:String = Server.config.siteroot + '/oauth2/redirect';
        var host:String = null;
        var uri:String = null;
        var clientID:String = null;
        var clientSecret:String = null;

        switch(query.state.toLowerCase()) {
            case 'auth-google': {
                host = 'www.googleapis.com';
                uri = '/oauth2/v4/token';
                clientID = Server.config.oauth2.google.id;
                clientSecret = Server.config.oauth2.google.secret;
            }

            case 'auth-facebook': {
                host = 'graph.facebook.com';
                uri = '/v2.10/oauth/access_token';
                clientID = Server.config.oauth2.facebook.id;
                clientSecret = Server.config.oauth2.facebook.secret;
            }

            case _: return Future.sync(new NotFoundResponse(query.state));
        }

        var body:String = 'code=${query.code.urlEncode()}';
        body += '&client_id=${clientID.urlEncode()}';
        body += '&client_secret=${clientSecret.urlEncode()}';
        body += '&redirect_uri=${redirectURI.urlEncode()}';
        body += '&grant_type=authorization_code';

        headers.push(new HeaderField(HeaderName.ContentLength, body.length));

        var client = new SecureSocketClient();
        var req:Promise<Bytes> = client.request(new OutgoingRequest(
            new OutgoingRequestHeader(POST, new tink.url.Host(host), uri, headers),
            body
        )).next(function(res:IncomingResponse):Promise<Bytes> {
            return res.body.all();
        });

        return switch(query.state.toLowerCase()) {
            case 'auth-google':
                req.next(function(raw:Bytes):Promise<Response> {
                    var result:Dynamic = haxe.Json.parse(raw.toString());
                    var payload:Dynamic = JWT.extract(result.id_token);
                    var sub:String = payload.sub;

                    // verify the claims of the token
                    if(payload.iss != 'https://accounts.google.com' || payload.aud != Server.config.oauth2.google.id)
                        return new response.UnauthorizedResponse();

                    // TODO: verify using Google's publickey

                    var users:List<models.User> = models.User.manager.search($googleID == sub);
                    var user:models.User = if(users.length < 1) {
                        // create a new user!
                        var u:models.User = new models.User();
                        u.name = payload.name;
                        u.googleID = payload.sub;
                        u.picture = payload.picture;
                        u.insert();
                        Log.info('Created new Google user: ${u.name}');
                        u;
                    }
                    else {
                        var u:models.User = users.first();
                        u.name = payload.name;
                        u.picture = payload.picture;
                        Log.info('Google user logged in: ${u.name}');
                        u;
                    }

                    // TODO: some sort of redirect a la Auth0
                    var token:String = JWT.sign({
                        id: user.id
                    }, Server.config.jwt.secret);

                    return new response.JsonResponse({
                        token: token
                    });
                });

            case 'auth-facebook':
                req.next(function(raw:Bytes):Promise<IncomingResponse> {
                    var result:Dynamic = haxe.Json.parse(raw.toString());
                    var accessToken:String = result.access_token;

                    return client.request(new OutgoingRequest(
                        new OutgoingRequestHeader(
                            GET,
                            new tink.url.Host('graph.facebook.com'),
                            '/v2.5/me?fields=id,name,picture&access_token=${accessToken.urlEncode()}'
                        ), ''
                    ));
                }).next(function(res:IncomingResponse):Promise<Bytes> {
                    return res.body.all();
                }).next(function(raw:Bytes):Promise<Response> {
                    var result:Dynamic = haxe.Json.parse(raw.toString());
                    var fbID:String = result.id;

                    var users:List<models.User> = models.User.manager.search($facebookID == fbID);
                    var user:models.User = if(users.length < 1) {
                        // create a new user!
                        var u:models.User = new models.User();
                        u.name = result.name;
                        u.facebookID = fbID;
                        u.picture = result.picture.data.url;
                        u.insert();
                        Log.info('Created new Facebook user: ${u.name}');
                        u;
                    }
                    else {
                        var u:models.User = users.first();
                        u.name = result.name;
                        if(!result.picture.data.is_silhouette)
                            u.picture = result.picture.data.url;
                        Log.info('Facebook user logged in: ${u.name}');
                        u;
                    }

                    // TODO: some sort of redirect a la Auth0
                    var token:String = JWT.sign({
                        id: user.id
                    }, Server.config.jwt.secret);

                    return new response.JsonResponse({
                        token: token
                    });
                });

            case _: Future.sync(new response.NotFoundResponse(query.state));
        }
    }
}
{'fraction_non_alphanumeric': 0.12015672616456247, 'fraction_numerical': 0.003773037295022493, 'mean_word_length': 1.6183510638297873, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 22, 'https://': 7, 'lorem ipsum': 0, 'www.': 8, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5802969', 'n_tokens_mistral': 3749, 'n_tokens_neox': 3564, 'n_words': 666}