mirror of
https://github.com/zoriya/Aeris.git
synced 2026-06-04 03:16:05 +00:00
Merge pull request #112 from AnonymusRaccoon/feat/oauth-signin
Signin / Signup / Authorizations Redirect with Services
This commit is contained in:
@@ -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
|
||||
|
||||
+53
-14
@@ -18,14 +18,18 @@ 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 Db.User (User', UserDB (UserDB, userDBId), password, toUser)
|
||||
import GHC.Generics (Generic)
|
||||
import Servant.API.Generic (ToServantApi, (:-))
|
||||
import Servant.Auth.Server (CookieSettings, JWT, JWTSettings, SetCookie, ThrowAll (throwAll), acceptLogin, AuthResult (Authenticated), makeJWT)
|
||||
@@ -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, updateTokens)
|
||||
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,14 @@ type Unprotected =
|
||||
:<|> "signup"
|
||||
:> ReqBody '[JSON] SignupUser
|
||||
:> Post '[JSON] NoContent
|
||||
:<|> Capture "service" Service :> "signin"
|
||||
:> QueryParam "code" String
|
||||
:> Post '[JSON] LoginResponse
|
||||
:<|> Capture "service" Service :> "signup"
|
||||
:> QueryParam "code" String
|
||||
:> ReqBody '[JSON] SignupUser
|
||||
:> Post '[JSON] LoginResponse
|
||||
|
||||
|
||||
loginHandler ::
|
||||
CookieSettings ->
|
||||
@@ -95,11 +108,35 @@ 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
|
||||
|
||||
signupOauthHandler :: JWTSettings -> Service -> Maybe String -> SignupUser -> AppM LoginResponse
|
||||
signupOauthHandler jwts service (Just code) (SignupUser name p) = do
|
||||
hashed <- hashPassword'' $ toPassword $ pack p
|
||||
user <- createUser $ UserDB (UserId 1) (pack name) hashed (pack name) []
|
||||
tokens <- liftIO $ runMaybeT $ getOauthTokens service code
|
||||
case tokens of
|
||||
Nothing -> throwError err403
|
||||
Just t -> do
|
||||
updateTokens (userDBId user) t
|
||||
etoken <- liftIO $ makeJWT (toUser user) jwts Nothing
|
||||
case etoken of
|
||||
Left e -> throwError err401
|
||||
Right v -> return $ LoginResponse $ unpack v
|
||||
signupOauthHandler _ _ _ _ = throwError err400
|
||||
|
||||
signupHandler ::
|
||||
SignupUser ->
|
||||
@@ -111,8 +148,10 @@ signupHandler (SignupUser name p) = do
|
||||
|
||||
unprotected :: CookieSettings -> JWTSettings -> ServerT Unprotected AppM
|
||||
unprotected cs jwts =
|
||||
loginHandler cs jwts
|
||||
:<|> signupHandler
|
||||
loginHandler cs jwts
|
||||
:<|> signupHandler
|
||||
:<|> loginOauthHandler jwts
|
||||
:<|> signupOauthHandler jwts
|
||||
|
||||
data AuthAPI mode = AuthAPI
|
||||
{ protectedApi :: mode :- UserAuth :> Protected
|
||||
|
||||
+8
-6
@@ -8,21 +8,23 @@ 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
|
||||
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
|
||||
@@ -89,10 +91,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
|
||||
@@ -114,7 +114,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
|
||||
|
||||
+166
-64
@@ -6,13 +6,17 @@ import qualified Data.ByteString.Char8 as B8
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
|
||||
import App (AppM)
|
||||
import Core.User (ExternalToken (ExternalToken, expiresAt), Service (Github, Discord, Spotify, Google, Twitter, Anilist))
|
||||
import Core.User (ExternalToken (ExternalToken, accessToken, providerId, expiresAt), 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, lookupObjInt)
|
||||
import Data.ByteString.Base64
|
||||
import Utils (lookupObjString, lookupObjObject, lookupObjInt)
|
||||
import Control.Monad.Trans.Maybe (MaybeT (MaybeT, runMaybeT))
|
||||
import Control.Monad.IO.Class (MonadIO(liftIO))
|
||||
import Control.Monad (MonadPlus (mzero))
|
||||
import Data.Aeson (decode)
|
||||
import Data.ByteString.Base64 ( encodeBase64 )
|
||||
import Data.Time (getCurrentTime, addUTCTime)
|
||||
data OAuth2Conf = OAuth2Conf
|
||||
{ oauthClientId :: String
|
||||
@@ -33,6 +37,10 @@ tokenEndpoint code oc =
|
||||
, code
|
||||
]
|
||||
|
||||
liftMaybe :: (MonadPlus m) => Maybe a -> m a
|
||||
liftMaybe = maybe mzero return
|
||||
|
||||
|
||||
-- GITHUB
|
||||
getGithubConfig :: IO OAuth2Conf
|
||||
getGithubConfig =
|
||||
@@ -41,8 +49,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
|
||||
@@ -57,11 +65,30 @@ getGithubTokens code = do
|
||||
request'
|
||||
response <- httpJSONEither request
|
||||
currTime <- getCurrentTime
|
||||
return $ case (getResponseBody response :: Either JSONException Object) of
|
||||
Left _ -> Nothing
|
||||
Right obj -> do
|
||||
access <- lookupObjString obj "access_token"
|
||||
Just $ ExternalToken (pack access) "" currTime Github
|
||||
let (Right obj) = (getResponseBody response :: Either JSONException Object)
|
||||
access <- liftMaybe $ lookupObjString obj "access_token"
|
||||
let t = ExternalToken access "" currTime 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
|
||||
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
|
||||
@@ -71,8 +98,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
|
||||
backUrl <- envAsString "BACK_URL" ""
|
||||
let endpoint = tokenEndpoint code cfg
|
||||
@@ -89,15 +116,30 @@ getDiscordTokens code = do
|
||||
]
|
||||
request'
|
||||
response <- httpJSONEither request
|
||||
let (Right obj) = (getResponseBody response :: Either JSONException Object)
|
||||
access <- liftMaybe $ lookupObjString obj "access_token"
|
||||
refresh <- liftMaybe $ lookupObjString obj "refresh_token"
|
||||
|
||||
currTime <- getCurrentTime
|
||||
return $ case (getResponseBody response :: Either JSONException Object) of
|
||||
Left _ -> Nothing
|
||||
Right obj -> do
|
||||
access <- lookupObjString obj "access_token"
|
||||
refresh <- lookupObjString obj "refresh_token"
|
||||
expiresIn <- lookupObjInt obj "expires_in"
|
||||
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
|
||||
Just $ ExternalToken (pack access) (pack refresh) expiresAt Discord
|
||||
|
||||
expiresIn <- liftMaybe $ lookupObjInt obj "expires_in"
|
||||
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
|
||||
let t = ExternalToken access refresh expiresAt 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
|
||||
@@ -107,8 +149,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
|
||||
backUrl <- envAsString "BACK_URL" ""
|
||||
let endpoint = tokenEndpoint code cfg
|
||||
@@ -126,14 +168,28 @@ getGoogleTokens code = do
|
||||
request'
|
||||
response <- httpJSONEither request
|
||||
currTime <- getCurrentTime
|
||||
return $ case (getResponseBody response :: Either JSONException Object) of
|
||||
Left _ -> Nothing
|
||||
Right obj -> do
|
||||
access <- lookupObjString obj "access_token"
|
||||
refresh <- lookupObjString obj "refresh_token"
|
||||
expiresIn <- lookupObjInt obj "expires_in"
|
||||
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
|
||||
Just $ ExternalToken (pack access) (pack refresh) expiresAt Google
|
||||
let (Right obj) = (getResponseBody response :: Either JSONException Object)
|
||||
access <- liftMaybe $ lookupObjString obj "access_token"
|
||||
refresh <- liftMaybe $ lookupObjString obj "refresh_token"
|
||||
expiresIn <- liftMaybe $ lookupObjInt obj "expires_in"
|
||||
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
|
||||
let t = ExternalToken access refresh expiresAt 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
|
||||
@@ -143,8 +199,8 @@ 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
|
||||
backUrl <- envAsString "BACK_URL" ""
|
||||
let basicAuth = encodeBase64 $ B8.pack $ oauthClientId cfg ++ ":" ++ oauthClientSecret cfg
|
||||
@@ -162,14 +218,26 @@ getSpotifyTokens code = do
|
||||
request'
|
||||
response <- httpJSONEither request
|
||||
currTime <- getCurrentTime
|
||||
return $ case (getResponseBody response :: Either JSONException Object) of
|
||||
Left _ -> Nothing
|
||||
Right obj -> do
|
||||
access <- lookupObjString obj "access_token"
|
||||
refresh <- lookupObjString obj "refresh_token"
|
||||
expiresIn <- lookupObjInt obj "expires_in"
|
||||
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
|
||||
Just $ ExternalToken (pack access) (pack refresh) expiresAt Spotify
|
||||
let (Right obj) = (getResponseBody response :: Either JSONException Object)
|
||||
access <- liftMaybe $ lookupObjString obj "access_token"
|
||||
refresh <- liftMaybe $ lookupObjString obj "refresh_token"
|
||||
expiresIn <- liftMaybe $ lookupObjInt obj "expires_in"
|
||||
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
|
||||
let t = ExternalToken access refresh expiresAt 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
|
||||
@@ -179,8 +247,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
|
||||
backUrl <- envAsString "BACK_URL" ""
|
||||
let basicAuth = encodeBase64 $ B8.pack $ "Basic " ++ oauthClientId cfg ++ ":" ++ oauthClientSecret cfg
|
||||
@@ -199,14 +267,28 @@ getTwitterTokens code = do
|
||||
request'
|
||||
response <- httpJSONEither request
|
||||
currTime <- getCurrentTime
|
||||
return $ case (getResponseBody response :: Either JSONException Object) of
|
||||
Left _ -> Nothing
|
||||
Right obj -> do
|
||||
access <- lookupObjString obj "access_token"
|
||||
refresh <- lookupObjString obj "refresh_token"
|
||||
expiresIn <- lookupObjInt obj "expires_in"
|
||||
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
|
||||
Just $ ExternalToken (pack access) (pack refresh) expiresAt Twitter
|
||||
let (Right obj) = (getResponseBody response :: Either JSONException Object)
|
||||
access <- liftMaybe $ lookupObjString obj "access_token"
|
||||
refresh <- liftMaybe $ lookupObjString obj "refresh_token"
|
||||
expiresIn <- liftMaybe $ lookupObjInt obj "expires_in"
|
||||
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
|
||||
let t = ExternalToken access refresh expiresAt 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
|
||||
@@ -216,8 +298,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
|
||||
backUrl <- envAsString "BACK_URL" ""
|
||||
let endpoint = tokenEndpoint code cfg
|
||||
@@ -235,20 +317,40 @@ getAnilistTokens code = do
|
||||
request'
|
||||
response <- httpJSONEither request
|
||||
currTime <- getCurrentTime
|
||||
return $ case (getResponseBody response :: Either JSONException Object) of
|
||||
Left _ -> Nothing
|
||||
Right obj -> do
|
||||
access <- lookupObjString obj "access_token"
|
||||
refresh <- lookupObjString obj "refresh_token"
|
||||
expiresIn <- lookupObjInt obj "expires_in"
|
||||
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
|
||||
Just $ ExternalToken (pack access) (pack refresh) expiresAt Anilist
|
||||
|
||||
|
||||
let (Right obj) = (getResponseBody response :: Either JSONException Object)
|
||||
access <- liftMaybe $ lookupObjString obj "access_token"
|
||||
refresh <- liftMaybe $ lookupObjString obj "refresh_token"
|
||||
expiresIn <- liftMaybe $ lookupObjInt obj "expires_in"
|
||||
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
|
||||
let t = ExternalToken access refresh expiresAt Anilist Nothing
|
||||
anilistId <- runMaybeT $ getAnilistId t
|
||||
return $ Just $ t { providerId = anilistId }
|
||||
|
||||
getAnilistId :: ExternalToken -> MaybeT IO Text
|
||||
getAnilistId t = MaybeT $ do
|
||||
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 "User-Agent" "aeris-server" $
|
||||
setRequestMethod "POST" $
|
||||
setRequestBodyLBS "{\"query\": \"{Viewer {id}}\"}"
|
||||
request'
|
||||
response <- httpJSONEither request
|
||||
let (Right obj) = (getResponseBody response :: Either JSONException Object)
|
||||
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 -> IO (Maybe ExternalToken)
|
||||
getOauthTokens :: Service -> String -> MaybeT IO ExternalToken
|
||||
getOauthTokens Github = getGithubTokens
|
||||
getOauthTokens Discord = getDiscordTokens
|
||||
getOauthTokens Spotify = getSpotifyTokens
|
||||
|
||||
@@ -44,6 +44,7 @@ data ExternalToken = ExternalToken
|
||||
, refreshToken :: Text
|
||||
, expiresAt :: UTCTime
|
||||
, service :: Service
|
||||
, providerId :: Maybe Text
|
||||
}
|
||||
deriving (Eq, Show, Generic)
|
||||
deriving anyclass (ToJSON, FromJSON)
|
||||
|
||||
@@ -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,8 +21,25 @@ getUserById' uid = do
|
||||
res <- runQuery (select $ limit 1 $ getUserById (lit uid))
|
||||
return $ head res
|
||||
|
||||
createUser :: User' -> AppM [UserId]
|
||||
createUser user = runQuery (insert $ insertUser user)
|
||||
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 User'
|
||||
createUser user = do
|
||||
ids <- runQuery (insert $ insertUser user)
|
||||
getUserById' $ head ids
|
||||
|
||||
getTokensByUserId :: UserId -> AppM [ExternalToken]
|
||||
getTokensByUserId uid = do
|
||||
|
||||
+12
-4
@@ -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,16 +19,22 @@ 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 ..]
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -55,4 +62,5 @@ instance Default (Pipeline Identity) where
|
||||
}
|
||||
|
||||
type UserAuth = Servant.Auth.Server.Auth '[JWT] User
|
||||
type AuthRes = Servant.Auth.Server.AuthResult User
|
||||
type AuthRes = Servant.Auth.Server.AuthResult User
|
||||
|
||||
|
||||
@@ -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 <div />;
|
||||
}
|
||||
@@ -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 <div />;
|
||||
}
|
||||
@@ -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 <div />;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { sendServiceAuthToken } from "../../utils/utils";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface ServiceAuthProps {
|
||||
service: string
|
||||
endpoint: string
|
||||
redirect_uri: string
|
||||
navigate_to: string
|
||||
}
|
||||
|
||||
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 + endpoint, `${window.location.origin}/${redirect_uri}`).then((ok) => {
|
||||
navigate(navigate_to);
|
||||
})
|
||||
}, []);
|
||||
|
||||
return <div />;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import {sendServiceAuthToken, setCookie, signInService} from "../../utils/utils";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface ServiceSignInProps {
|
||||
service: string
|
||||
endpoint: string
|
||||
redirect_uri: string
|
||||
navigate_to: string
|
||||
}
|
||||
|
||||
export default function ServiceSignIn({ service, endpoint, redirect_uri, navigate_to }: ServiceSignInProps) {
|
||||
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 <div />;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import {getCookie, sendServiceAuthToken, setCookie, signInService} from "../../utils/utils";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import {Divider, InputAdornment, Typography} from "@mui/material";
|
||||
import {AccountCircle, Lock} from "@mui/icons-material";
|
||||
import CardActions from "@material-ui/core/CardActions";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import {makeStyles, Theme} from "@material-ui/core/styles";
|
||||
import aerisTheme from "../../Aeris.theme";
|
||||
import {t} from "i18next";
|
||||
import {API_ROUTE} from "../../utils/globals";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
container: {
|
||||
display: "absolute",
|
||||
flex: 0.5,
|
||||
margin: `${theme.spacing(0)} auto`,
|
||||
},
|
||||
loginBtn: {
|
||||
display: "absolute",
|
||||
backgroundColor: aerisTheme.palette.secondary.main,
|
||||
color: aerisTheme.palette.primary.contrastText,
|
||||
minWidth: 150,
|
||||
margin: `${theme.spacing(0)} auto`,
|
||||
'&:hover': {
|
||||
backgroundColor: aerisTheme.palette.secondary.light
|
||||
}
|
||||
},
|
||||
switchBtn: {
|
||||
backgroundColor: aerisTheme.palette.primary.main,
|
||||
color: aerisTheme.palette.primary.contrastText,
|
||||
minWidth: 150,
|
||||
margin: `${theme.spacing(0)} auto`,
|
||||
'&:hover': {
|
||||
backgroundColor: aerisTheme.palette.primary.light
|
||||
}
|
||||
},
|
||||
media: {
|
||||
display: "absolute",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 354.75,
|
||||
height: 478.5,
|
||||
marginBottom: 5,
|
||||
},
|
||||
card: {
|
||||
display: "absolute",
|
||||
margin: `${theme.spacing(0)} auto`,
|
||||
},
|
||||
}));
|
||||
|
||||
interface ServiceSignUpProps {
|
||||
service: string
|
||||
endpoint: string
|
||||
redirect_uri: string
|
||||
navigate_to: string
|
||||
}
|
||||
|
||||
type SignUpFormData = {
|
||||
username: string;
|
||||
password: string;
|
||||
confirmedPassword: string;
|
||||
isButtonDisabled: boolean;
|
||||
helperText: string;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
const requestSignUpWithService = async (username: string, password: string, service: string, authToken: string): Promise<boolean> => {
|
||||
const response = await fetch(`${API_ROUTE}/auth/${service}/signup?code=${authToken}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ username: username, password: password })
|
||||
});
|
||||
|
||||
if (!response.ok) return false;
|
||||
let json = await response.json();
|
||||
setCookie("aeris_jwt", json["jwt"], 365);
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function ServiceSignUp({ service, endpoint, redirect_uri, navigate_to }: ServiceSignUpProps) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const classes = useStyles();
|
||||
|
||||
const [signUpData, setSignUpData] = useState<SignUpFormData>({
|
||||
username: "",
|
||||
password: "",
|
||||
confirmedPassword: "",
|
||||
isButtonDisabled: true,
|
||||
helperText: "",
|
||||
isError: false
|
||||
});
|
||||
|
||||
const authCode = searchParams.get("code") as string;
|
||||
|
||||
useEffect(() => {
|
||||
setSignUpData((prevState) => {
|
||||
return {
|
||||
...prevState,
|
||||
isButtonDisabled: !(
|
||||
signUpData.username.trim() &&
|
||||
signUpData.password.trim() &&
|
||||
(signUpData.confirmedPassword.trim() == signUpData.password.trim())
|
||||
),
|
||||
};
|
||||
});
|
||||
}, [signUpData.username, signUpData.password, signUpData.confirmedPassword]);
|
||||
|
||||
|
||||
const handleSignUp = async () => {
|
||||
if (await requestSignUpWithService(signUpData.username, signUpData.password, service, authCode)) {
|
||||
setSignUpData((prevState) => {
|
||||
return { ...prevState, isError: false, helperText: t('loginSuccess') };
|
||||
});
|
||||
window.location.href = "/pipelines";
|
||||
} else {
|
||||
setSignUpData((prevState) => {
|
||||
return { ...prevState, isError: true, helperText: t('usernameOrPasswordIncorrect') };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (event: React.KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
signUpData.isButtonDisabled || handleSignUp();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUsernameChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
setSignUpData((prevState) => {
|
||||
return { ...prevState, username: event.target.value };
|
||||
});
|
||||
};
|
||||
|
||||
const handlePasswordChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
setSignUpData((prevState) => {
|
||||
return { ...prevState, password: event.target.value };
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirmedPasswordChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
setSignUpData((prevState) => {
|
||||
return { ...prevState, confirmedPassword: event.target.value };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Box component="img" className={classes.media} alt="Aeris Logo" src={require("../../assets/logo-white.png")} />
|
||||
<form className={classes.container} noValidate autoComplete="on">
|
||||
<Card className={classes.card}>
|
||||
<CardContent>
|
||||
<div>
|
||||
<TextField
|
||||
error={signUpData.isError}
|
||||
required
|
||||
type="username"
|
||||
label={t("username") as string}
|
||||
placeholder={t("username") as string}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<AccountCircle />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onKeyPress={handleKeyPress}
|
||||
onChange={handleUsernameChange}
|
||||
/>
|
||||
<br />
|
||||
<TextField
|
||||
className="inputRounded"
|
||||
error={signUpData.isError}
|
||||
required
|
||||
type="password"
|
||||
label={t("password") as string}
|
||||
placeholder={t("password") as string}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
helperText={signUpData.helperText}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Lock />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onKeyPress={handleKeyPress}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
<br />
|
||||
<TextField
|
||||
className="inputRounded"
|
||||
error={signUpData.isError}
|
||||
required
|
||||
type="password"
|
||||
label={t('confirm_password') as string}
|
||||
placeholder={t("password") as string}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
helperText={signUpData.helperText}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Lock />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onKeyPress={handleKeyPress}
|
||||
onChange={handleConfirmedPasswordChange}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
className={classes.loginBtn}
|
||||
onClick={handleSignUp}
|
||||
disabled={signUpData.isButtonDisabled}>
|
||||
{t('signUp')}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 <div />;
|
||||
}
|
||||
@@ -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 <div />;
|
||||
}
|
||||
@@ -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 <div />;
|
||||
}
|
||||
+17
-13
@@ -1,25 +1,26 @@
|
||||
import { StrictMode } from "react";
|
||||
import {StrictMode, useState} from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./App.css";
|
||||
|
||||
import App from "./App";
|
||||
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 ServiceAuth from "./components/Authorizations/ServiceAuth";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import AuthComponent from "./pages/Login/LoginPage";
|
||||
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";
|
||||
import ServiceSignIn from "./components/Authorizations/ServiceSignIn";
|
||||
import ServiceSignUp from "./components/Authorizations/ServiceSignUp";
|
||||
|
||||
/**
|
||||
* Creates the routing tree.
|
||||
*/
|
||||
function AerisRouter() {
|
||||
const [possibleServices, setServices] = useState<Array<AppServiceType>>(AppServices)
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<div className="App">
|
||||
@@ -29,12 +30,15 @@ function AerisRouter() {
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/auth" element={<AuthComponent />} />
|
||||
<Route path="/pipelines" element={<PipelinePage />} />
|
||||
<Route path="/authorization/github" element={<GithubAuth />} />
|
||||
<Route path="/authorization/spotify" element={<SpotifyAuth />} />
|
||||
<Route path="/authorization/google" element={<GoogleAuth />} />
|
||||
<Route path="/authorization/twitter" element={<TwitterAuth />} />
|
||||
<Route path="/authorization/discord" element={<DiscordAuth />} />
|
||||
<Route path="/authorization/anilist" element={<AnilistAuth />} />
|
||||
{possibleServices.map((elem, index) => {
|
||||
return (<Route path={`/authorization/${elem.uid}`} element={<ServiceAuth service={elem.uid} endpoint="" navigate_to="/pipelines" redirect_uri={`authorization/${elem.uid}`}/>} />);
|
||||
})}
|
||||
{possibleServices.map((elem, index) => {
|
||||
return (<Route path={`/signin/${elem.uid}`} element={<ServiceSignIn service={elem.uid} endpoint="/signin" navigate_to="/pipelines" redirect_uri={`singin/${elem.uid}`}/>} />);
|
||||
})}
|
||||
{possibleServices.map((elem, index) => {
|
||||
return (<Route path={`/signup/${elem.uid}`} element={<ServiceSignUp service={elem.uid} endpoint="/signup" navigate_to="/pipelines" redirect_uri={`singup/${elem.uid}`}/>} />);
|
||||
})}
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</header>
|
||||
|
||||
@@ -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<Array<AppServiceType>>(AppServices);
|
||||
const [authData, setAuthData] = useState<AuthCompProps>({
|
||||
username: "",
|
||||
password: "",
|
||||
@@ -253,6 +255,27 @@ export default function AuthComponent() {
|
||||
{authData.authMode === "login" ? t('signUp') : t('connectToAeris')}
|
||||
</Button>
|
||||
</CardActions>
|
||||
<Divider variant="middle" sx={{ m: 1 }}/>
|
||||
<Typography variant="body2" sx={{ mb: 1}}>
|
||||
Or you can sign up with this services:
|
||||
</Typography>
|
||||
<CardActions>
|
||||
{servicesData.map((elem, index) => {
|
||||
if (elem.uid === "utils")
|
||||
return (<div/>);
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
authData.authMode === 'login' ? (window.location.href = elem.signinUrl) : (window.location.href = elem.signupUrl);
|
||||
}}
|
||||
variant="text"
|
||||
style={{ borderRadius: "20px", width: "20px", height: "20px" }}
|
||||
>
|
||||
<img loading="lazy" width="20px" height="20px" src={elem.logo.imageSrc} alt={elem.logo.altText} />
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</CardActions>
|
||||
</Card>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -42,8 +42,8 @@ export const AppServicesLogos: { [key: string]: ImageProps } = {
|
||||
},
|
||||
};
|
||||
|
||||
const getServiceUrl = (service: string) =>
|
||||
`${API_ROUTE}/auth/${service}/url?redirect_uri=${window.location.origin}/authorization/${service}`;
|
||||
const getServiceUrl = (service: string, method: string = "authorization") =>
|
||||
`${API_ROUTE}/auth/${service}/url?redirect_uri=${window.location.origin}/${method}/${service}`;
|
||||
|
||||
export const AppServices: Array<AppServiceType> = [
|
||||
{
|
||||
@@ -51,6 +51,8 @@ export const AppServices: Array<AppServiceType> = [
|
||||
uid: "google",
|
||||
logo: AppServicesLogos["youtube"],
|
||||
urlAuth: getServiceUrl("google"),
|
||||
signinUrl: getServiceUrl("google", "signin"),
|
||||
signupUrl: getServiceUrl("google", "signup"),
|
||||
linked: false,
|
||||
},
|
||||
{
|
||||
@@ -58,6 +60,8 @@ export const AppServices: Array<AppServiceType> = [
|
||||
uid: "spotify",
|
||||
logo: AppServicesLogos["spotify"],
|
||||
urlAuth: getServiceUrl("spotify"),
|
||||
signinUrl: getServiceUrl("spotify", "signin"),
|
||||
signupUrl: getServiceUrl("spotify", "signup"),
|
||||
linked: false,
|
||||
},
|
||||
{
|
||||
@@ -65,6 +69,8 @@ export const AppServices: Array<AppServiceType> = [
|
||||
uid: "github",
|
||||
logo: AppServicesLogos["github"],
|
||||
urlAuth: getServiceUrl("github"),
|
||||
signinUrl: getServiceUrl("github", "signin"),
|
||||
signupUrl: getServiceUrl("github", "signup"),
|
||||
linked: false,
|
||||
},
|
||||
{
|
||||
@@ -72,6 +78,8 @@ export const AppServices: Array<AppServiceType> = [
|
||||
uid: "twitter",
|
||||
logo: AppServicesLogos["twitter"],
|
||||
urlAuth: getServiceUrl("twitter"),
|
||||
signinUrl: getServiceUrl("twitter", "signin"),
|
||||
signupUrl: getServiceUrl("twitter", "signup"),
|
||||
linked: false,
|
||||
},
|
||||
{
|
||||
@@ -79,6 +87,8 @@ export const AppServices: Array<AppServiceType> = [
|
||||
uid: "discord",
|
||||
logo: AppServicesLogos["discord"],
|
||||
urlAuth: getServiceUrl("discord"),
|
||||
signinUrl: getServiceUrl("discord", "signin"),
|
||||
signupUrl: getServiceUrl("discord", "signup"),
|
||||
linked: false,
|
||||
},
|
||||
{
|
||||
@@ -86,6 +96,8 @@ export const AppServices: Array<AppServiceType> = [
|
||||
uid: "anilist",
|
||||
logo: AppServicesLogos["anilist"],
|
||||
urlAuth: getServiceUrl("anilist"),
|
||||
signinUrl: getServiceUrl("anilist", "signin"),
|
||||
signupUrl: getServiceUrl("anilist", "signup"),
|
||||
linked: false,
|
||||
},
|
||||
{
|
||||
@@ -93,7 +105,7 @@ export const AppServices: Array<AppServiceType> = [
|
||||
uid: "utils",
|
||||
logo: AppServicesLogos["utils"],
|
||||
urlAuth: "",
|
||||
linked: true,
|
||||
linked: true
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface AppServiceType {
|
||||
uid: string;
|
||||
logo: ImageProps;
|
||||
urlAuth: string;
|
||||
signinUrl: string;
|
||||
signupUrl: string;
|
||||
linked: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,25 @@ export const sendServiceAuthToken = async (
|
||||
return response.ok;
|
||||
};
|
||||
|
||||
export const signInService = async (
|
||||
authToken: string,
|
||||
serviceEndpoint: string,
|
||||
redirectUri: string
|
||||
): Promise<boolean> => {
|
||||
const response = await fetch(`${API_ROUTE}${serviceEndpoint}?code=${authToken}&redirect_uri=${redirectUri}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) return false;
|
||||
let json = await response.json();
|
||||
setCookie("aeris_jwt", json['jwt'], 365);
|
||||
return response.ok;
|
||||
};
|
||||
|
||||
export const PipelineParamsToApiParam = (pipelineParams: { [key: string]: ParamsType }) => {
|
||||
return Object.fromEntries(Object.entries(pipelineParams).map((el) => [el[0], el[1].value]));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user