From 31954c0c29c01fa830884f256602cfa8d09c9791 Mon Sep 17 00:00:00 2001 From: GitBluub Date: Sat, 5 Mar 2022 00:42:28 +0100 Subject: [PATCH 01/10] feat: add providerid to external tokens --- api/src/Core/User.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/Core/User.hs b/api/src/Core/User.hs index 3e2f59a..b783427 100644 --- a/api/src/Core/User.hs +++ b/api/src/Core/User.hs @@ -43,6 +43,7 @@ data ExternalToken = ExternalToken , refreshToken :: Text , expiresIn :: Int64 , service :: Service + , providerId :: Maybe Text } deriving (Eq, Show, Generic) deriving anyclass (ToJSON, FromJSON) From 0b06eaad8ef3627a558ce4f317c1a15f146f9188 Mon Sep 17 00:00:00 2001 From: GitBluub Date: Sat, 5 Mar 2022 00:43:47 +0100 Subject: [PATCH 02/10] feat: add users.read scope to twitter --- api/src/Api/About.hs | 2 +- api/src/Api/OIDC.hs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/api/src/Api/About.hs b/api/src/Api/About.hs index e24446c..4885b2c 100644 --- a/api/src/Api/About.hs +++ b/api/src/Api/About.hs @@ -45,7 +45,7 @@ $(deriveJSON defaultOptions ''About) servicesDir :: [(FilePath, S.ByteString)] servicesDir = $(embedDir "./services/") --- servicesDir = undefined +--servicesDir = undefined about :: SockAddr -> AppM About about host = do diff --git a/api/src/Api/OIDC.hs b/api/src/Api/OIDC.hs index fd96da9..a70c63d 100644 --- a/api/src/Api/OIDC.hs +++ b/api/src/Api/OIDC.hs @@ -18,11 +18,12 @@ import Utils (UserAuth, AuthRes) import qualified Data.ByteString.Char8 as B8 import Servant.Auth.Server (AuthResult(Authenticated)) import System.Environment.MrEnv (envAsString) +import Control.Monad.Trans.Maybe (MaybeT(runMaybeT)) oauthHandler :: AuthRes -> Service -> Maybe String -> AppM NoContent oauthHandler _ _ Nothing = throwError err400 oauthHandler (Authenticated (User uid _ _)) service (Just code) = do - tokens <- liftIO $ getOauthTokens service code + tokens <- liftIO $ runMaybeT $ getOauthTokens service code case tokens of Nothing -> throwError err403 Just t -> do @@ -57,7 +58,7 @@ urlHandler Twitter (Just r) = do clientId <- liftIO $ envAsString "TWITTER_CLIENT_ID" "" backRedirect <- liftIO $ envAsString "BACK_URL" "" throwError $ err302 { errHeaders = - [("Location", B8.pack $ "https://twitter.com/i/oauth2/authorize?response_type=code&scope=like.write like.read follows.read follows.write offline.access tweet.read tweet.write&code_challenge=challenge&code_challenge_method=plain&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] } + [("Location", B8.pack $ "https://twitter.com/i/oauth2/authorize?response_type=code&scope=like.write like.read follows.read follows.write offline.access tweet.read tweet.write users.read&code_challenge=challenge&code_challenge_method=plain&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] } urlHandler Spotify (Just r) = do clientId <- liftIO $ envAsString "SPOTIFY_CLIENT_ID" "" backRedirect <- liftIO $ envAsString "BACK_URL" "" From 0705ca3185ec4ef2405e9369b7b7241c39088272 Mon Sep 17 00:00:00 2001 From: GitBluub Date: Sat, 5 Mar 2022 00:44:18 +0100 Subject: [PATCH 03/10] feat: worker doesn t send provider id --- api/src/Api/Worker.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/Api/Worker.hs b/api/src/Api/Worker.hs index 27fa3ed..9b2783d 100644 --- a/api/src/Api/Worker.hs +++ b/api/src/Api/Worker.hs @@ -113,7 +113,7 @@ refreshHandler :: Service -> UserId -> Maybe String -> RefreshBody -> AppM NoCon refreshHandler service uid (Just key) (RefreshBody at rt ex) = do k <- liftIO $ envAsString "WORKER_API_KEY" "" if k == key then do - updateTokens uid $ ExternalToken at rt ex service + updateTokens uid $ ExternalToken at rt ex service Nothing return NoContent else throwError err403 refreshHandler _ _ _ _ = throwError err403 From 924ec6b4251cb665fe825011a71ac9f4639fd6ce Mon Sep 17 00:00:00 2001 From: GitBluub Date: Sat, 5 Mar 2022 00:45:08 +0100 Subject: [PATCH 04/10] feat: get provider id Works for every service except anilist --- api/src/Core/OIDC.hs | 196 +++++++++++++++++++++++++++++++------------ api/src/Utils.hs | 11 ++- 2 files changed, 152 insertions(+), 55 deletions(-) diff --git a/api/src/Core/OIDC.hs b/api/src/Core/OIDC.hs index 15affb2..51222ef 100644 --- a/api/src/Core/OIDC.hs +++ b/api/src/Core/OIDC.hs @@ -6,13 +6,16 @@ import qualified Data.ByteString.Char8 as B8 import qualified Data.HashMap.Strict as HM import App (AppM) -import Core.User (ExternalToken (ExternalToken), Service (Github, Discord, Spotify, Google, Twitter, Anilist)) +import Core.User (ExternalToken (ExternalToken, accessToken, providerId), Service (Github, Discord, Spotify, Google, Twitter, Anilist)) import Data.Aeson.Types (Object, Value (String)) import Data.Text (Text, pack, unpack) import Network.HTTP.Simple (JSONException, addRequestHeader, getResponseBody, httpJSONEither, parseRequest, setRequestMethod, setRequestQueryString, setRequestBodyURLEncoded) import System.Environment.MrEnv (envAsBool, envAsInt, envAsInteger, envAsString) -import Utils (lookupObjString) +import Utils (lookupObjString, lookupObjObject, lookupObjInt) import Data.ByteString.Base64 +import Control.Monad.Trans.Maybe (MaybeT (MaybeT, runMaybeT)) +import Control.Monad.IO.Class (MonadIO(liftIO)) +import Control.Monad (MonadPlus (mzero)) data OAuth2Conf = OAuth2Conf { oauthClientId :: String , oauthClientSecret :: String @@ -32,6 +35,10 @@ tokenEndpoint code oc = , code ] +liftMaybe :: (MonadPlus m) => Maybe a -> m a +liftMaybe = maybe mzero return + + -- GITHUB getGithubConfig :: IO OAuth2Conf getGithubConfig = @@ -40,8 +47,8 @@ getGithubConfig = <*> envAsString "GITHUB_SECRET" "" <*> pure "https://github.com/login/oauth/access_token" -getGithubTokens :: String -> IO (Maybe ExternalToken) -getGithubTokens code = do +getGithubTokens :: String -> MaybeT IO ExternalToken +getGithubTokens code = MaybeT $ do gh <- getGithubConfig let endpoint = tokenEndpoint code gh request' <- parseRequest endpoint @@ -55,11 +62,31 @@ getGithubTokens code = do ] request' response <- httpJSONEither request - return $ case (getResponseBody response :: Either JSONException Object) of - Left _ -> Nothing - Right obj -> do - access <- lookupObjString obj "access_token" - Just $ ExternalToken (pack access) "" 0 Github + let (Right obj) = (getResponseBody response :: Either JSONException Object) + access <- liftMaybe $ lookupObjString obj "access_token" + let t = ExternalToken access "" 0 Github Nothing + githubId <- runMaybeT $ getGithubId t + return $ Just $ t { providerId = githubId } + +rightToMaybe :: Either a b -> Maybe b +rightToMaybe = either (const Nothing) Just + +getGithubId :: ExternalToken -> MaybeT IO Text +getGithubId t = MaybeT $ do + let endpoint = "https://api.github.com/user" + request' <- parseRequest endpoint + let request = + addRequestHeader "Authorization" (B8.pack ("token " ++ unpack (accessToken t))) $ + addRequestHeader "Accept" "application/json" $ + addRequestHeader "User-Agent" "aeris-server" + request' + response <- httpJSONEither request + print $ accessToken t + case (getResponseBody response :: Either JSONException Object) of + Left err -> return Nothing + Right obj -> case lookupObjInt obj "id" of + Just githubId -> return $ Just $ pack $ show githubId + _ -> return Nothing -- DISCORD getDiscordConfig :: IO OAuth2Conf @@ -69,8 +96,8 @@ getDiscordConfig = <*> envAsString "DISCORD_SECRET" "" <*> pure "https://discord.com/api/oauth2/token" -getDiscordTokens :: String -> IO (Maybe ExternalToken) -getDiscordTokens code = do +getDiscordTokens :: String -> MaybeT IO ExternalToken +getDiscordTokens code = MaybeT $ do cfg <- getDiscordConfig let endpoint = tokenEndpoint code cfg request' <- parseRequest endpoint @@ -86,12 +113,26 @@ getDiscordTokens code = do ] request' response <- httpJSONEither request - return $ case (getResponseBody response :: Either JSONException Object) of - Left _ -> Nothing - Right obj -> do - access <- lookupObjString obj "access_token" - refresh <- lookupObjString obj "refresh_token" - Just $ ExternalToken (pack access) (pack refresh) 0 Discord + let (Right obj) = (getResponseBody response :: Either JSONException Object) + access <- liftMaybe $ lookupObjString obj "access_token" + refresh <- liftMaybe $ lookupObjString obj "refresh_token" + let t = ExternalToken access refresh 0 Discord Nothing + discordId <- runMaybeT $ getDiscordId t + return $ Just $ t { providerId = discordId } + + +getDiscordId :: ExternalToken -> MaybeT IO Text +getDiscordId t = MaybeT $ do + let endpoint = "https://discord.com/api/users/@me" + request' <- parseRequest endpoint + let request = + addRequestHeader "Accept" "application/json" $ + addRequestHeader "Authorization" (B8.pack $ "Bearer " ++ unpack (accessToken t)) + request' + response <- httpJSONEither request + let (Right obj) = (getResponseBody response :: Either JSONException Object) + return $ lookupObjString obj "id" + -- GOOGLE getGoogleConfig :: IO OAuth2Conf @@ -101,8 +142,8 @@ getGoogleConfig = <*> envAsString "GOOGLE_SECRET" "" <*> pure "https://oauth2.googleapis.com/token" -getGoogleTokens :: String -> IO (Maybe ExternalToken) -getGoogleTokens code = do +getGoogleTokens :: String -> MaybeT IO ExternalToken +getGoogleTokens code = MaybeT $ do cfg <- getGoogleConfig let endpoint = tokenEndpoint code cfg request' <- parseRequest endpoint @@ -118,12 +159,26 @@ getGoogleTokens code = do ] request' response <- httpJSONEither request - return $ case (getResponseBody response :: Either JSONException Object) of - Left _ -> Nothing - Right obj -> do - access <- lookupObjString obj "access_token" - refresh <- lookupObjString obj "refresh_token" - Just $ ExternalToken (pack access) (pack refresh) 0 Google + let (Right obj) = (getResponseBody response :: Either JSONException Object) + access <- liftMaybe $ lookupObjString obj "access_token" + refresh <- liftMaybe $ lookupObjString obj "refresh_token" + let t = ExternalToken access refresh 0 Google Nothing + googleId <- runMaybeT $ getGoogleId t + return $ Just $ t { providerId = googleId } + +getGoogleId :: ExternalToken -> MaybeT IO Text +getGoogleId t = MaybeT $ do + let endpoint = "https://oauth2.googleapis.com/tokeninfo" + request' <- parseRequest endpoint + let request = + addRequestHeader "Accept" "application/json" $ + setRequestQueryString + [ ("access_token", Just . B8.pack . unpack $ accessToken t) + ] + request' + response <- httpJSONEither request + let (Right obj) = (getResponseBody response :: Either JSONException Object) + return $ lookupObjString obj "sub" -- SPOTIFY getSpotifyConfig :: IO OAuth2Conf @@ -133,10 +188,9 @@ getSpotifyConfig = <*> envAsString "SPOTIFY_SECRET" "" <*> pure "https://accounts.spotify.com/api/token" -getSpotifyTokens :: String -> IO (Maybe ExternalToken) -getSpotifyTokens code = do +getSpotifyTokens :: String -> MaybeT IO ExternalToken +getSpotifyTokens code = MaybeT $ do cfg <- getSpotifyConfig - let basicAuth = encodeBase64 $ B8.pack $ oauthClientId cfg ++ ":" ++ oauthClientSecret cfg let endpoint = tokenEndpoint code cfg request' <- parseRequest endpoint @@ -151,12 +205,24 @@ getSpotifyTokens code = do ] request' response <- httpJSONEither request - return $ case (getResponseBody response :: Either JSONException Object) of - Left _ -> Nothing - Right obj -> do - access <- lookupObjString obj "access_token" - refresh <- lookupObjString obj "refresh_token" - Just $ ExternalToken (pack access) (pack refresh) 0 Spotify + let (Right obj) = (getResponseBody response :: Either JSONException Object) + access <- liftMaybe $ lookupObjString obj "access_token" + refresh <- liftMaybe $ lookupObjString obj "refresh_token" + let t = ExternalToken access refresh 0 Spotify Nothing + spotifyId <- runMaybeT $ getSpotifyId t + return $ Just $ t { providerId = spotifyId } + +getSpotifyId :: ExternalToken -> MaybeT IO Text +getSpotifyId t = MaybeT $ do + let endpoint = "https://api.spotify.com/v1/me" + request' <- parseRequest endpoint + let request = + addRequestHeader "Content-Type" "application/json" $ + addRequestHeader "Authorization" (B8.pack $ "Bearer " ++ unpack (accessToken t)) + request' + response <- httpJSONEither request + let (Right obj) = (getResponseBody response :: Either JSONException Object) + return $ lookupObjString obj "id" -- TWITTER getTwitterConfig :: IO OAuth2Conf @@ -166,8 +232,8 @@ getTwitterConfig = <*> envAsString "TWITTER_SECRET" "" <*> pure "https://api.twitter.com/2/oauth2/token" -getTwitterTokens :: String -> IO (Maybe ExternalToken) -getTwitterTokens code = do +getTwitterTokens :: String -> MaybeT IO ExternalToken +getTwitterTokens code = MaybeT $ do cfg <- getTwitterConfig let basicAuth = encodeBase64 $ B8.pack $ "Basic " ++ oauthClientId cfg ++ ":" ++ oauthClientSecret cfg let endpoint = tokenEndpoint code cfg @@ -184,12 +250,26 @@ getTwitterTokens code = do ] request' response <- httpJSONEither request - return $ case (getResponseBody response :: Either JSONException Object) of - Left _ -> Nothing - Right obj -> do - access <- lookupObjString obj "access_token" - refresh <- lookupObjString obj "refresh_token" - Just $ ExternalToken (pack access) (pack refresh) 0 Twitter + let (Right obj) = (getResponseBody response :: Either JSONException Object) + access <- liftMaybe $ lookupObjString obj "access_token" + refresh <- liftMaybe $ lookupObjString obj "refresh_token" + let t = ExternalToken access refresh 0 Twitter Nothing + twitterId <- runMaybeT $ getTwitterId t + return $ Just $ t { providerId = twitterId } + +getTwitterId :: ExternalToken -> MaybeT IO Text +getTwitterId t = MaybeT $ do + let endpoint = "https://api.twitter.com/2/users/me" + request' <- parseRequest endpoint + let request = + addRequestHeader "Content-Type" "application/json" $ + addRequestHeader "Authorization" (B8.pack $ "Bearer " ++ unpack (accessToken t)) + request' + response <- httpJSONEither request + let (Right obj) = (getResponseBody response :: Either JSONException Object) + case lookupObjObject obj "data" of + Just dataBody -> return $ lookupObjString dataBody "id" + _ -> return Nothing -- ANILIST getAnilistConfig :: IO OAuth2Conf @@ -199,8 +279,8 @@ getAnilistConfig = <*> envAsString "ANILIST_SECRET" "" <*> pure "https://anilist.co/api/v2/oauth/token" -getAnilistTokens :: String -> IO (Maybe ExternalToken) -getAnilistTokens code = do +getAnilistTokens :: String -> MaybeT IO ExternalToken +getAnilistTokens code = MaybeT $ do cfg <- getAnilistConfig let endpoint = tokenEndpoint code cfg request' <- parseRequest endpoint @@ -216,18 +296,30 @@ getAnilistTokens code = do ] request' response <- httpJSONEither request - return $ case (getResponseBody response :: Either JSONException Object) of - Left _ -> Nothing - Right obj -> do - access <- lookupObjString obj "access_token" - refresh <- lookupObjString obj "refresh_token" - Just $ ExternalToken (pack access) (pack refresh) 0 Anilist - + let (Right obj) = (getResponseBody response :: Either JSONException Object) + access <- liftMaybe $ lookupObjString obj "access_token" + refresh <- liftMaybe $ lookupObjString obj "refresh_token" + let t = ExternalToken access refresh 0 Anilist Nothing + anilistId <- runMaybeT $ getAnilistId t + return $ Just $ t { providerId = anilistId } +getAnilistId :: ExternalToken -> MaybeT IO Text +getAnilistId t = MaybeT $ do + let endpoint = "https://api.twitter.com/2/users/me" + request' <- parseRequest endpoint + let request = + addRequestHeader "Content-Type" "application/json" $ + addRequestHeader "Authorization" (B8.pack $ "Bearer " ++ unpack (accessToken t)) + request' + response <- httpJSONEither request + let (Right obj) = (getResponseBody response :: Either JSONException Object) + dataBody <- liftMaybe $ lookupObjObject obj "data" + viewer <- liftMaybe $ lookupObjObject obj "Viewer" + return . Just . pack . show $ lookupObjInt viewer "id" -- General -getOauthTokens :: Service -> String -> IO (Maybe ExternalToken) +getOauthTokens :: Service -> String -> MaybeT IO ExternalToken getOauthTokens Github = getGithubTokens getOauthTokens Discord = getDiscordTokens getOauthTokens Spotify = getSpotifyTokens diff --git a/api/src/Utils.hs b/api/src/Utils.hs index 16a04be..a3565d2 100644 --- a/api/src/Utils.hs +++ b/api/src/Utils.hs @@ -25,15 +25,20 @@ import Data.Scientific ( toBoundedInteger ) mapInd :: (a -> Int -> b) -> [a] -> [b] mapInd f l = zipWith f l [0 ..] -lookupObjString :: Object -> Text -> Maybe String +lookupObjString :: Object -> Text -> Maybe Text lookupObjString obj key = case Data.HashMap.Strict.lookup key obj of - Just (String x) -> Just . unpack $ x + Just (String x) -> Just x + _ -> Nothing + +lookupObjObject :: Object -> Text -> Maybe Object +lookupObjObject obj key = case Data.HashMap.Strict.lookup key obj of + Just (Object x) -> Just x _ -> Nothing lookupObjInt :: Object -> Text -> Maybe Int64 lookupObjInt obj key = case Data.HashMap.Strict.lookup key obj of - Just (Number x) -> toBoundedInteger $ x + Just (Number x) -> toBoundedInteger x _ -> Nothing uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d From 72c08038f659cc559a673cc1e89eb3fe0b071b84 Mon Sep 17 00:00:00 2001 From: GitBluub Date: Sat, 5 Mar 2022 12:15:02 +0100 Subject: [PATCH 05/10] feat: anilist id --- api/src/Core/OIDC.hs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/api/src/Core/OIDC.hs b/api/src/Core/OIDC.hs index 51222ef..5ed8538 100644 --- a/api/src/Core/OIDC.hs +++ b/api/src/Core/OIDC.hs @@ -9,13 +9,14 @@ import App (AppM) import Core.User (ExternalToken (ExternalToken, accessToken, providerId), Service (Github, Discord, Spotify, Google, Twitter, Anilist)) import Data.Aeson.Types (Object, Value (String)) import Data.Text (Text, pack, unpack) -import Network.HTTP.Simple (JSONException, addRequestHeader, getResponseBody, httpJSONEither, parseRequest, setRequestMethod, setRequestQueryString, setRequestBodyURLEncoded) +import Network.HTTP.Simple (JSONException, addRequestHeader, getResponseBody, httpJSONEither, parseRequest, setRequestMethod, setRequestQueryString, setRequestBodyURLEncoded, setRequestBodyJSON, setRequestBodyLBS) import System.Environment.MrEnv (envAsBool, envAsInt, envAsInteger, envAsString) import Utils (lookupObjString, lookupObjObject, lookupObjInt) import Data.ByteString.Base64 import Control.Monad.Trans.Maybe (MaybeT (MaybeT, runMaybeT)) import Control.Monad.IO.Class (MonadIO(liftIO)) import Control.Monad (MonadPlus (mzero)) +import Data.Aeson (decode) data OAuth2Conf = OAuth2Conf { oauthClientId :: String , oauthClientSecret :: String @@ -306,17 +307,26 @@ getAnilistTokens code = MaybeT $ do getAnilistId :: ExternalToken -> MaybeT IO Text getAnilistId t = MaybeT $ do - let endpoint = "https://api.twitter.com/2/users/me" + let endpoint = "https://graphql.anilist.co" request' <- parseRequest endpoint + let query = (decode "{Viewer {id,}}" :: Maybe Object) let request = addRequestHeader "Content-Type" "application/json" $ - addRequestHeader "Authorization" (B8.pack $ "Bearer " ++ unpack (accessToken t)) + addRequestHeader "Authorization" (B8.pack $ "Bearer " ++ unpack (accessToken t)) $ + addRequestHeader "User-Agent" "aeris-server" $ + setRequestMethod "POST" $ + setRequestBodyLBS "{\"query\": \"{Viewer {id}}\"}" request' response <- httpJSONEither request let (Right obj) = (getResponseBody response :: Either JSONException Object) - dataBody <- liftMaybe $ lookupObjObject obj "data" - viewer <- liftMaybe $ lookupObjObject obj "Viewer" - return . Just . pack . show $ lookupObjInt viewer "id" + case lookupObjObject obj "data" of + Just dataBody -> case liftMaybe $ lookupObjObject dataBody "Viewer" of + Just viewer -> case lookupObjInt viewer "id" of + Just anilistId -> return . Just . pack . show $ anilistId + _ -> return Nothing + _ -> return Nothing + _ -> return Nothing + -- General getOauthTokens :: Service -> String -> MaybeT IO ExternalToken From 63f446d3ed08e2b9bcb016358ac8af6c3bcb563a Mon Sep 17 00:00:00 2001 From: GitBluub Date: Sat, 5 Mar 2022 16:21:51 +0100 Subject: [PATCH 06/10] feat: signin route --- api/src/Api/Auth.hs | 48 +++++++++++++++++++++++++++----------- api/src/Api/OIDC.hs | 11 +++++---- api/src/Repository/User.hs | 21 +++++++++++++++-- api/src/Utils.hs | 7 ++++-- 4 files changed, 65 insertions(+), 22 deletions(-) diff --git a/api/src/Api/Auth.hs b/api/src/Api/Auth.hs index c2d4a5c..dab4bb6 100644 --- a/api/src/Api/Auth.hs +++ b/api/src/Api/Auth.hs @@ -18,12 +18,16 @@ import Servant ( Post, ReqBody, err401, + err400, + err403, throwError, type (:<|>) (..), - type (:>), + type (:>), Capture, QueryParam ) import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Maybe (MaybeT(runMaybeT)) +import Core.OIDC ( getOauthTokens ) import Data.Aeson (FromJSON, ToJSON) import Db.User (User', UserDB (UserDB), password, toUser) import GHC.Generics (Generic) @@ -34,10 +38,10 @@ import Servant.Server.Generic (AsServerT) import Data.ByteString.Lazy.Char8 ( unpack ) import Api.OIDC (OauthAPI, oauth) import App (AppM) -import Core.User (User, UserId (UserId)) +import Core.User (User, UserId (UserId), Service) import Data.Text (pack) import Password (hashPassword'', toPassword, validatePassword') -import Repository (createUser, getUserByName') +import Repository (createUser, getUserByName', getUserByToken) import Utils (UserAuth, AuthRes) data LoginUser = LoginUser @@ -57,15 +61,16 @@ newtype LoginResponse = LoginResponse } deriving (Eq, Show, Read, Generic) + +instance ToJSON LoginResponse +instance FromJSON LoginResponse + instance ToJSON LoginUser instance FromJSON LoginUser instance ToJSON SignupUser instance FromJSON SignupUser -instance ToJSON LoginResponse -instance FromJSON LoginResponse - type Protected = "me" :> Get '[JSON] User @@ -80,6 +85,10 @@ type Unprotected = :<|> "signup" :> ReqBody '[JSON] SignupUser :> Post '[JSON] NoContent + :<|> Capture "service" Service :> "signin" + :> QueryParam "code" String + :> Post '[JSON] LoginResponse + loginHandler :: CookieSettings -> @@ -95,11 +104,23 @@ loginHandler cs jwts (LoginUser username p) = do case etoken of Left e -> throwError err401 Right v -> return $ LoginResponse $ unpack v - {--mApplyCookies <- liftIO $ acceptLogin cs jwts (toUser usr) - case mApplyCookies of - Nothing -> throwError err401 - Just applyCookies -> return $ applyCookies NoContent - --}else throwError err401 + else throwError err401 + + +loginOauthHandler :: JWTSettings -> Service -> Maybe String -> AppM LoginResponse +loginOauthHandler jwts _ Nothing = throwError err400 +loginOauthHandler jwts service (Just code) = do + tokens <- liftIO $ runMaybeT $ getOauthTokens service code + case tokens of + Nothing -> throwError err403 + Just t -> do + user <- getUserByToken t + etoken <- liftIO $ makeJWT (toUser user) jwts Nothing + case etoken of + Left e -> throwError err401 + Right v -> return $ LoginResponse $ unpack v + + signupHandler :: SignupUser -> @@ -111,8 +132,9 @@ signupHandler (SignupUser name p) = do unprotected :: CookieSettings -> JWTSettings -> ServerT Unprotected AppM unprotected cs jwts = - loginHandler cs jwts - :<|> signupHandler + loginHandler cs jwts + :<|> signupHandler + :<|> loginOauthHandler jwts data AuthAPI mode = AuthAPI { protectedApi :: mode :- UserAuth :> Protected diff --git a/api/src/Api/OIDC.hs b/api/src/Api/OIDC.hs index a70c63d..43d9880 100644 --- a/api/src/Api/OIDC.hs +++ b/api/src/Api/OIDC.hs @@ -8,17 +8,18 @@ module Api.OIDC where import App (AppM) import Control.Monad.IO.Class (liftIO) import Core.User (ExternalToken (ExternalToken, service), Service (Github, Spotify, Twitter, Google, Anilist, Discord), UserId (UserId), User (User)) -import Data.Text (pack) +import Data.Text (pack, unpack) import Core.OIDC ( getOauthTokens ) import Repository.User (updateTokens, getTokensByUserId, delTokens) -import Servant (Capture, Get, GetNoContent, JSON, NoContent (NoContent), QueryParam, ServerT, err400, throwError, type (:<|>) ((:<|>)), type (:>), err401, err403, ServerError (errHeaders), err302, Delete) +import Servant (Capture, Get, GetNoContent, JSON, NoContent (NoContent), QueryParam, ServerT, err400, throwError, type (:<|>) ((:<|>)), type (:>), err401, err403, ServerError (errHeaders), err302, Delete, Post) import Servant.API.Generic (type (:-)) import Servant.Server.Generic (AsServerT) import Utils (UserAuth, AuthRes) import qualified Data.ByteString.Char8 as B8 -import Servant.Auth.Server (AuthResult(Authenticated)) +import Servant.Auth.Server (AuthResult(Authenticated), makeJWT) import System.Environment.MrEnv (envAsString) import Control.Monad.Trans.Maybe (MaybeT(runMaybeT)) +import Db.User (toUser) oauthHandler :: AuthRes -> Service -> Maybe String -> AppM NoContent oauthHandler _ _ Nothing = throwError err400 @@ -89,10 +90,10 @@ type OauthAPI = UserAuth :> Capture "service" Service :> QueryParam "code" Strin :<|> Capture "service" Service :> "url" :> QueryParam "redirect_uri" String :> Get '[JSON] NoContent :<|> UserAuth :> "services" :> Get '[JSON] [String] :<|> "redirect" :> QueryParam "code" String :> QueryParam "state" String :> Get '[JSON] NoContent - + oauth :: ServerT OauthAPI AppM oauth = oauthHandler :<|> oauthDelHandler :<|> urlHandler :<|> servicesHandler - :<|> redirectHandler + :<|> redirectHandler \ No newline at end of file diff --git a/api/src/Repository/User.hs b/api/src/Repository/User.hs index ce3ef9b..a3ba3fe 100644 --- a/api/src/Repository/User.hs +++ b/api/src/Repository/User.hs @@ -1,12 +1,14 @@ module Repository.User where import App (AppM) -import Core.User (ExternalToken, UserId, Service) +import Core.User (ExternalToken (service, providerId), UserId, Service) import Data.Text (Text) -import Db.User (User', getUserByName, getUserTokensById, insertUser, selectAllUser, updateUserTokens, getUserById, updateDelTokens) +import Db.User (User', getUserByName, getUserTokensById, insertUser, selectAllUser, updateUserTokens, getUserById, updateDelTokens, UserDB (externalTokens)) import Rel8 (insert, select, update, limit, lit) import Repository.Utils (runQuery) import Data.Int (Int64) +import Data.List (find) +import Servant (err401, throwError) users :: AppM [User'] users = runQuery (select selectAllUser) @@ -19,6 +21,21 @@ getUserById' uid = do res <- runQuery (select $ limit 1 $ getUserById (lit uid)) return $ head res +getUserByToken :: ExternalToken -> AppM User' +getUserByToken t = do + users' <- users + case find findByToken users' of + Nothing -> throwError err401 + Just x -> return x + where + findByToken :: User' -> Bool + findByToken usr = do + let userTokens = externalTokens usr + case find (\tok -> service tok == service t) userTokens of + Nothing -> False + Just tok -> providerId tok == providerId t + + createUser :: User' -> AppM [UserId] createUser user = runQuery (insert $ insertUser user) diff --git a/api/src/Utils.hs b/api/src/Utils.hs index a3565d2..bb6ab89 100644 --- a/api/src/Utils.hs +++ b/api/src/Utils.hs @@ -3,6 +3,7 @@ {-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-} {-# LANGUAGE FlexibleInstances #-} + module Utils where import Data.Aeson.Types (Value (String), Object) @@ -18,9 +19,10 @@ import Db.Pipeline (Pipeline (Pipeline), PipelineId (PipelineId), pipelineLastTr import Core.Pipeline (PipelineParams (PipelineParams)) import Data.Time (UTCTime (UTCTime), fromGregorian, secondsToDiffTime) import Data.Default (Default, def) -import Data.Aeson (Value(Number, Object), decode) +import Data.Aeson (Value(Number, Object), decode, ToJSON, FromJSON) import Data.Int (Int64) import Data.Scientific ( toBoundedInteger ) +import GHC.Generics (Generic) mapInd :: (a -> Int -> b) -> [a] -> [b] mapInd f l = zipWith f l [0 ..] @@ -60,4 +62,5 @@ instance Default (Pipeline Identity) where } type UserAuth = Servant.Auth.Server.Auth '[JWT] User -type AuthRes = Servant.Auth.Server.AuthResult User \ No newline at end of file +type AuthRes = Servant.Auth.Server.AuthResult User + From 9e71e1de0b7e0d787619d4480d8f6860f54bbf54 Mon Sep 17 00:00:00 2001 From: 0Nom4D Date: Sat, 5 Mar 2022 19:31:32 +0100 Subject: [PATCH 07/10] Aeris : feat/oauth-sign - Exchanging Files cause of Spotify Error --- .../components/Authorizations/ServiceAuth.tsx | 23 +++++++++++++++++ web-app/src/index.tsx | 19 ++++++++------ web-app/src/pages/Login/LoginPage.tsx | 25 +++++++++++++++++-- 3 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 web-app/src/components/Authorizations/ServiceAuth.tsx diff --git a/web-app/src/components/Authorizations/ServiceAuth.tsx b/web-app/src/components/Authorizations/ServiceAuth.tsx new file mode 100644 index 0000000..28eed19 --- /dev/null +++ b/web-app/src/components/Authorizations/ServiceAuth.tsx @@ -0,0 +1,23 @@ +import { useNavigate, useSearchParams } from "react-router-dom"; +import { sendServiceAuthToken } from "../../utils/utils"; +import { useEffect } from "react"; + +interface ServiceAuthProps { + service: string + redirect_uri: string + navigate_to: string +} + +export default function ServiceAuth({ service, redirect_uri, navigate_to }: ServiceAuthProps) { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const authCode = searchParams.get("code") as string; + + useEffect(() => { + sendServiceAuthToken(authCode, "/auth/" + service, `${window.location.origin}/${redirect_uri}`).then((ok) => { + navigate(navigate_to); + }) + }, []); + + return
; +} \ No newline at end of file diff --git a/web-app/src/index.tsx b/web-app/src/index.tsx index 236f033..ddf7bc1 100644 --- a/web-app/src/index.tsx +++ b/web-app/src/index.tsx @@ -1,8 +1,9 @@ -import { StrictMode } from "react"; +import {StrictMode, useState} from "react"; import ReactDOM from "react-dom"; import "./App.css"; import App from "./App"; +import ServiceAuth from "./components/Authorizations/ServiceAuth"; import GithubAuth from "./components/Authorizations/GithubAuth"; import SpotifyAuth from "./components/Authorizations/SpotifyAuth"; import GoogleAuth from "./components/Authorizations/YoutubeAuth"; @@ -15,11 +16,15 @@ import PipelinePage from "./pages/HomePage"; import { ThemeProvider } from "@mui/material"; import theme from "./Aeris.theme"; +import {AppServices} from "./utils/globals"; +import {AppServiceType} from "./utils/types"; /** * Creates the routing tree. */ function AerisRouter() { + const [possibleServices, setServices] = useState>(AppServices) + return (
@@ -29,12 +34,12 @@ function AerisRouter() { } /> } /> } /> - } /> - } /> - } /> - } /> - } /> - } /> + {possibleServices.map((elem, index) => { + return (} />); + })} + {/*{possibleServices.map((elem, index) => {*/} + {/* return (} />);*/} + {/*})}*/} diff --git a/web-app/src/pages/Login/LoginPage.tsx b/web-app/src/pages/Login/LoginPage.tsx index 325d516..07e08fc 100644 --- a/web-app/src/pages/Login/LoginPage.tsx +++ b/web-app/src/pages/Login/LoginPage.tsx @@ -4,9 +4,9 @@ import { makeStyles, Theme } from "@material-ui/core/styles"; import { useNavigate, Link as RouterLink } from "react-router-dom"; import { AccountCircle, Cookie, Lock } from "@mui/icons-material"; -import { InputAdornment } from "@mui/material"; +import {CardMedia, Divider, InputAdornment, Typography} from "@mui/material"; -import { API_ROUTE } from "../../utils/globals"; +import {API_ROUTE, AppServices} from "../../utils/globals"; import CardContent from "@material-ui/core/CardContent"; import CardActions from "@material-ui/core/CardActions"; @@ -20,6 +20,7 @@ import { setCookie, getCookie } from "../../utils/utils"; import { useTranslation } from "react-i18next"; import '../../i18n/config'; +import {AppServiceType} from "../../utils/types"; const useStyles = makeStyles((theme: Theme) => ({ container: { @@ -91,6 +92,7 @@ export default function AuthComponent() { const { t } = useTranslation(); const classes = useStyles(); const navigate = useNavigate(); + const [servicesData, setServicesData] = useState>(AppServices); const [authData, setAuthData] = useState({ username: "", password: "", @@ -253,6 +255,25 @@ export default function AuthComponent() { {authData.authMode === "login" ? t('signUp') : t('connectToAeris')} + + + Or you can sign up with this services: + + + {servicesData.map((elem, index) => { + if (elem.uid === "utils") + return (
); + return ( + + ); + })} +
From e3a94aa17f462049d328779e859accf0f818263b Mon Sep 17 00:00:00 2001 From: 0Nom4D Date: Sat, 5 Mar 2022 22:35:04 +0100 Subject: [PATCH 08/10] Aeris : feat/oauth-signin - Adding Sign In with Services --- .../components/Authorizations/AnilistAuth.tsx | 18 ------------- .../components/Authorizations/DiscordAuth.tsx | 19 ------------- .../components/Authorizations/GithubAuth.tsx | 18 ------------- .../components/Authorizations/ServiceAuth.tsx | 5 ++-- .../Authorizations/ServiceSignIn.tsx | 27 +++++++++++++++++++ .../components/Authorizations/SpotifyAuth.tsx | 19 ------------- .../components/Authorizations/TwitterAuth.tsx | 19 ------------- .../components/Authorizations/YoutubeAuth.tsx | 19 ------------- web-app/src/index.tsx | 15 ++++------- web-app/src/pages/Login/LoginPage.tsx | 2 +- web-app/src/utils/globals.tsx | 18 +++++++++++-- web-app/src/utils/types.tsx | 2 ++ web-app/src/utils/utils.tsx | 20 ++++++++++++++ 13 files changed, 74 insertions(+), 127 deletions(-) delete mode 100644 web-app/src/components/Authorizations/AnilistAuth.tsx delete mode 100644 web-app/src/components/Authorizations/DiscordAuth.tsx delete mode 100644 web-app/src/components/Authorizations/GithubAuth.tsx create mode 100644 web-app/src/components/Authorizations/ServiceSignIn.tsx delete mode 100644 web-app/src/components/Authorizations/SpotifyAuth.tsx delete mode 100644 web-app/src/components/Authorizations/TwitterAuth.tsx delete mode 100644 web-app/src/components/Authorizations/YoutubeAuth.tsx diff --git a/web-app/src/components/Authorizations/AnilistAuth.tsx b/web-app/src/components/Authorizations/AnilistAuth.tsx deleted file mode 100644 index 278e038..0000000 --- a/web-app/src/components/Authorizations/AnilistAuth.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { getCookie, sendServiceAuthToken } from "../../utils/utils"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import React, { useEffect } from "react"; -import { API_ROUTE } from "../../utils/globals"; - -export default function Anilist() { - const [searchParams, setSearchParams] = useSearchParams(); - const navigate = useNavigate(); - const authCode = searchParams.get("code") as string; - - useEffect(() => { - sendServiceAuthToken(authCode, "/auth/anilist", `${window.location.origin}/authorization/anilist`).then((ok) => { - navigate('/pipelines'); - }); - }, []); - - return
; -} diff --git a/web-app/src/components/Authorizations/DiscordAuth.tsx b/web-app/src/components/Authorizations/DiscordAuth.tsx deleted file mode 100644 index c22fb17..0000000 --- a/web-app/src/components/Authorizations/DiscordAuth.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { getCookie, sendServiceAuthToken } from "../../utils/utils"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import { useEffect } from "react"; -import { API_ROUTE } from "../../utils/globals"; - -export default function DiscordAuth() { - const [searchParams, setSearchParams] = useSearchParams(); - const navigate = useNavigate(); - - const authToken = searchParams.get("code") as string; - - useEffect(() => { - sendServiceAuthToken(authToken, "/auth/discord", `${window.location.origin}/authorization/discord`).then((ok) => { - navigate('/pipelines'); - }); - }, []); - - return
; -} diff --git a/web-app/src/components/Authorizations/GithubAuth.tsx b/web-app/src/components/Authorizations/GithubAuth.tsx deleted file mode 100644 index e9c2757..0000000 --- a/web-app/src/components/Authorizations/GithubAuth.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { getCookie, sendServiceAuthToken } from "../../utils/utils"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import { useEffect } from "react"; -import { API_ROUTE } from "../../utils/globals"; - -export default function GithubAuth() { - const [searchParams, setSearchParams] = useSearchParams(); - const navigate = useNavigate(); - const authCode = searchParams.get("code") as string; - - useEffect(() => { - sendServiceAuthToken(authCode, "/auth/github", `${window.location.origin}/authorization/github`).then((ok) => { - navigate('/pipelines'); - }); - }, []); - - return
; -} diff --git a/web-app/src/components/Authorizations/ServiceAuth.tsx b/web-app/src/components/Authorizations/ServiceAuth.tsx index 28eed19..04732d2 100644 --- a/web-app/src/components/Authorizations/ServiceAuth.tsx +++ b/web-app/src/components/Authorizations/ServiceAuth.tsx @@ -4,17 +4,18 @@ import { useEffect } from "react"; interface ServiceAuthProps { service: string + endpoint: string redirect_uri: string navigate_to: string } -export default function ServiceAuth({ service, redirect_uri, navigate_to }: ServiceAuthProps) { +export default function ServiceAuth({ service, endpoint, redirect_uri, navigate_to }: ServiceAuthProps) { const [searchParams] = useSearchParams(); const navigate = useNavigate(); const authCode = searchParams.get("code") as string; useEffect(() => { - sendServiceAuthToken(authCode, "/auth/" + service, `${window.location.origin}/${redirect_uri}`).then((ok) => { + sendServiceAuthToken(authCode, "/auth/" + service + endpoint, `${window.location.origin}/${redirect_uri}`).then((ok) => { navigate(navigate_to); }) }, []); diff --git a/web-app/src/components/Authorizations/ServiceSignIn.tsx b/web-app/src/components/Authorizations/ServiceSignIn.tsx new file mode 100644 index 0000000..b82f243 --- /dev/null +++ b/web-app/src/components/Authorizations/ServiceSignIn.tsx @@ -0,0 +1,27 @@ +import { useNavigate, useSearchParams } from "react-router-dom"; +import {sendServiceAuthToken, setCookie, signInService} from "../../utils/utils"; +import { useEffect } from "react"; + +interface ServiceAuthProps { + service: string + endpoint: string + redirect_uri: string + navigate_to: string +} + +export default function ServiceSignIn({ service, endpoint, redirect_uri, navigate_to }: ServiceAuthProps) { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const authCode = searchParams.get("code") as string; + + useEffect(() => { + signInService(authCode, "/auth/" + service + endpoint, `${window.location.origin}/${redirect_uri}`).then((ok) => { + if (ok) + navigate(navigate_to); + else + console.warn('An error occurred when signing in with a service.'); + }) + }, []); + + return
; +} \ No newline at end of file diff --git a/web-app/src/components/Authorizations/SpotifyAuth.tsx b/web-app/src/components/Authorizations/SpotifyAuth.tsx deleted file mode 100644 index 69ce0d7..0000000 --- a/web-app/src/components/Authorizations/SpotifyAuth.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { getCookie, sendServiceAuthToken } from "../../utils/utils"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import { useEffect } from "react"; -import { API_ROUTE } from "../../utils/globals"; - -export default function SpotifyAuth() { - const [searchParams, setSearchParams] = useSearchParams(); - const navigate = useNavigate(); - - const authCode = searchParams.get("code") as string; - - useEffect(() => { - sendServiceAuthToken(authCode, "/auth/spotify", `${window.location.origin}/authorization/spotify`).then((ok) => { - navigate('/pipelines'); - }); - }, []); - - return
; -} diff --git a/web-app/src/components/Authorizations/TwitterAuth.tsx b/web-app/src/components/Authorizations/TwitterAuth.tsx deleted file mode 100644 index c606adf..0000000 --- a/web-app/src/components/Authorizations/TwitterAuth.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { getCookie, sendServiceAuthToken } from "../../utils/utils"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import { useEffect } from "react"; -import { API_ROUTE } from "../../utils/globals"; - -export default function TwitterAuth() { - const [searchParams, setSearchParams] = useSearchParams(); - const navigate = useNavigate(); - - const authCode = searchParams.get("code") as string; - - useEffect(() => { - sendServiceAuthToken(authCode, "/auth/twitter", `${window.location.origin}/authorization/twitter`).then((ok) => { - navigate('/pipelines'); - }); - }, []); - - return
; -} diff --git a/web-app/src/components/Authorizations/YoutubeAuth.tsx b/web-app/src/components/Authorizations/YoutubeAuth.tsx deleted file mode 100644 index e81da32..0000000 --- a/web-app/src/components/Authorizations/YoutubeAuth.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { getCookie, sendServiceAuthToken } from "../../utils/utils"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import { useEffect } from "react"; -import { API_ROUTE } from "../../utils/globals"; - -export default function GoogleAuth() { - const [searchParams, setSearchParams] = useSearchParams(); - const navigate = useNavigate(); - - const authCode = searchParams.get("code") as string; - - useEffect(() => { - sendServiceAuthToken(authCode, "/auth/google", `${window.location.origin}/authorization/google`).then((ok) => { - navigate('/pipelines'); - }); - }, []); - - return
; -} diff --git a/web-app/src/index.tsx b/web-app/src/index.tsx index ddf7bc1..eb57d37 100644 --- a/web-app/src/index.tsx +++ b/web-app/src/index.tsx @@ -4,12 +4,6 @@ import "./App.css"; import App from "./App"; import ServiceAuth from "./components/Authorizations/ServiceAuth"; -import GithubAuth from "./components/Authorizations/GithubAuth"; -import SpotifyAuth from "./components/Authorizations/SpotifyAuth"; -import GoogleAuth from "./components/Authorizations/YoutubeAuth"; -import TwitterAuth from "./components/Authorizations/TwitterAuth"; -import DiscordAuth from "./components/Authorizations/DiscordAuth"; -import AnilistAuth from "./components/Authorizations/AnilistAuth"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import AuthComponent from "./pages/Login/LoginPage"; import PipelinePage from "./pages/HomePage"; @@ -18,6 +12,7 @@ import { ThemeProvider } from "@mui/material"; import theme from "./Aeris.theme"; import {AppServices} from "./utils/globals"; import {AppServiceType} from "./utils/types"; +import ServiceSignIn from "./components/Authorizations/ServiceSignIn"; /** * Creates the routing tree. @@ -35,11 +30,11 @@ function AerisRouter() { } /> } /> {possibleServices.map((elem, index) => { - return (} />); + return (} />); + })} + {possibleServices.map((elem, index) => { + return (} />); })} - {/*{possibleServices.map((elem, index) => {*/} - {/* return (} />);*/} - {/*})}*/} diff --git a/web-app/src/pages/Login/LoginPage.tsx b/web-app/src/pages/Login/LoginPage.tsx index 07e08fc..03935e2 100644 --- a/web-app/src/pages/Login/LoginPage.tsx +++ b/web-app/src/pages/Login/LoginPage.tsx @@ -265,7 +265,7 @@ export default function AuthComponent() { return (
); return ( + + + +
+ ); +} \ No newline at end of file diff --git a/web-app/src/index.tsx b/web-app/src/index.tsx index eb57d37..c3eb0e3 100644 --- a/web-app/src/index.tsx +++ b/web-app/src/index.tsx @@ -13,6 +13,7 @@ import theme from "./Aeris.theme"; import {AppServices} from "./utils/globals"; import {AppServiceType} from "./utils/types"; import ServiceSignIn from "./components/Authorizations/ServiceSignIn"; +import ServiceSignUp from "./components/Authorizations/ServiceSignUp"; /** * Creates the routing tree. @@ -35,6 +36,9 @@ function AerisRouter() { {possibleServices.map((elem, index) => { return (} />); })} + {possibleServices.map((elem, index) => { + return (} />); + })} diff --git a/web-app/src/pages/Login/LoginPage.tsx b/web-app/src/pages/Login/LoginPage.tsx index 03935e2..bec316d 100644 --- a/web-app/src/pages/Login/LoginPage.tsx +++ b/web-app/src/pages/Login/LoginPage.tsx @@ -265,7 +265,9 @@ export default function AuthComponent() { return (
); return (