mirror of
https://github.com/Start9Labs/registry.git
synced 2026-03-26 10:21:51 +00:00
* handle root url * add handler file * fix compilation bug * fix compilation error * attempt redirect * add query param of hostname * add name to root query params * cleanup
348 lines
11 KiB
Haskell
348 lines
11 KiB
Haskell
{-# LANGUAGE AllowAmbiguousTypes #-}
|
|
{-# LANGUAGE BangPatterns #-}
|
|
{-# LANGUAGE InstanceSigs #-}
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
{-# LANGUAGE RankNTypes #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE TemplateHaskell #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
{-# LANGUAGE UndecidableInstances #-}
|
|
{-# LANGUAGE ViewPatterns #-}
|
|
{-# OPTIONS_GHC -Wno-orphans #-}
|
|
|
|
module Foundation where
|
|
|
|
import Startlude (
|
|
Applicative (pure),
|
|
Bool (False),
|
|
Eq ((==)),
|
|
IO,
|
|
MVar,
|
|
Maybe (..),
|
|
Monad (return),
|
|
Monoid (mempty),
|
|
Semigroup ((<>)),
|
|
String,
|
|
Text,
|
|
ThreadId,
|
|
Word64,
|
|
decodeUtf8,
|
|
drop,
|
|
encodeUtf8,
|
|
flip,
|
|
fst,
|
|
isJust,
|
|
otherwise,
|
|
putMVar,
|
|
show,
|
|
when,
|
|
($),
|
|
(.),
|
|
(<$>),
|
|
(<&>),
|
|
(<**>),
|
|
(=<<),
|
|
(||),
|
|
)
|
|
|
|
import Control.Monad.Logger (
|
|
Loc,
|
|
LogSource,
|
|
LogStr,
|
|
ToLogStr (toLogStr),
|
|
fromLogStr,
|
|
)
|
|
import Database.Persist.Sql (
|
|
ConnectionPool,
|
|
LogFunc,
|
|
PersistStoreRead (get),
|
|
SqlBackend,
|
|
SqlPersistT,
|
|
runSqlPool,
|
|
)
|
|
import Yesod.Core (
|
|
AuthResult (Authorized, Unauthorized),
|
|
LogLevel (..),
|
|
MonadHandler (liftHandler),
|
|
RenderMessage (..),
|
|
RenderRoute (Route, renderRoute),
|
|
RouteAttrs (routeAttrs),
|
|
SessionBackend,
|
|
ToTypedContent,
|
|
Yesod (
|
|
isAuthorized,
|
|
makeLogger,
|
|
makeSessionBackend,
|
|
maximumContentLengthIO,
|
|
messageLoggerSource,
|
|
shouldLogIO,
|
|
yesodMiddleware
|
|
),
|
|
defaultYesodMiddleware,
|
|
getYesod,
|
|
getsYesod,
|
|
mkYesodData,
|
|
parseRoutesFile,
|
|
)
|
|
import Yesod.Core.Types (
|
|
HandlerData (handlerEnv),
|
|
Logger (loggerDate),
|
|
RunHandlerEnv (rheChild, rheSite),
|
|
loggerPutStr,
|
|
)
|
|
import Yesod.Core.Unsafe qualified as Unsafe
|
|
|
|
import Control.Monad.Logger.Extras (wrapSGRCode)
|
|
import Control.Monad.Reader.Has (Has (extract, update))
|
|
import Crypto.Hash (
|
|
SHA256 (SHA256),
|
|
hashWith,
|
|
)
|
|
import Data.Set (member)
|
|
import Data.String.Interpolate.IsString (
|
|
i,
|
|
)
|
|
import Data.Text qualified as T
|
|
import Handler.Types.Api (ApiVersion (..))
|
|
import Language.Haskell.TH (Loc (..))
|
|
import Lib.PkgRepository (
|
|
EosRepo,
|
|
PkgRepo,
|
|
)
|
|
import Lib.Types.Core (PkgId, S9PK)
|
|
import Model (
|
|
Admin (..),
|
|
Key (AdminKey), EntityField (AdminId, AdminDeletedAt),
|
|
)
|
|
import Settings (AppSettings (appShouldLogAll))
|
|
import System.Console.ANSI.Codes (
|
|
Color (..),
|
|
ColorIntensity (..),
|
|
ConsoleLayer (Foreground),
|
|
SGR (SetColor),
|
|
)
|
|
import Yesod (
|
|
FormMessage,
|
|
defaultFormMessage,
|
|
)
|
|
import Yesod.Auth (
|
|
AuthEntity,
|
|
Creds (credsIdent),
|
|
YesodAuth (
|
|
AuthId,
|
|
authPlugins,
|
|
getAuthId,
|
|
loginDest,
|
|
logoutDest,
|
|
maybeAuthId
|
|
),
|
|
YesodAuthPersist (getAuthEntity),
|
|
)
|
|
import Yesod.Auth.Http.Basic (
|
|
defaultAuthSettings,
|
|
defaultMaybeBasicAuthId,
|
|
)
|
|
import Yesod.Persist.Core (
|
|
DBRunner,
|
|
YesodPersist (..),
|
|
YesodPersistRunner (..),
|
|
defaultGetDBRunner,
|
|
)
|
|
import Database.Persist ((==.))
|
|
import Database.Persist (selectFirst)
|
|
import Database.Persist (entityVal)
|
|
|
|
-- | The foundation datatype for your application. This can be a good place to
|
|
-- keep settings and values requiring initialization before your application
|
|
-- starts running, such as database connections. Every handler will have
|
|
-- access to the data present here.
|
|
data RegistryCtx = RegistryCtx
|
|
{ appSettings :: AppSettings
|
|
, appLogger :: Logger
|
|
, appWebServerThreadId :: MVar (ThreadId, ThreadId)
|
|
, appShouldRestartWeb :: MVar Bool
|
|
, appConnPool :: ConnectionPool
|
|
}
|
|
|
|
|
|
instance Has PkgRepo RegistryCtx where
|
|
extract = transitiveExtract @AppSettings
|
|
update = transitiveUpdate @AppSettings
|
|
instance Has a r => Has a (HandlerData r r) where
|
|
extract = extract . rheSite . handlerEnv
|
|
update f r =
|
|
let ctx = update f (rheSite $ handlerEnv r)
|
|
rhe = (handlerEnv r){rheSite = ctx, rheChild = ctx}
|
|
in r{handlerEnv = rhe}
|
|
instance Has AppSettings RegistryCtx where
|
|
extract = appSettings
|
|
update f ctx = ctx{appSettings = f (appSettings ctx)}
|
|
instance Has EosRepo RegistryCtx where
|
|
extract = transitiveExtract @AppSettings
|
|
update = transitiveUpdate @AppSettings
|
|
|
|
|
|
{-# INLINE transitiveExtract #-}
|
|
transitiveExtract :: forall b a c. (Has a b, Has b c) => c -> a
|
|
transitiveExtract = extract @a . extract @b
|
|
|
|
|
|
{-# INLINE transitiveUpdate #-}
|
|
transitiveUpdate :: forall b a c. (Has a b, Has b c) => (a -> a) -> (c -> c)
|
|
transitiveUpdate f = update (update @a @b f)
|
|
|
|
|
|
setWebProcessThreadId :: (ThreadId, ThreadId) -> RegistryCtx -> IO ()
|
|
setWebProcessThreadId tid@(!_, !_) a = putMVar (appWebServerThreadId a) tid
|
|
|
|
|
|
-- This is where we define all of the routes in our application. For a full
|
|
-- explanation of the syntax, please see:
|
|
-- http://www.yesodweb.com/book/routing-and-handlers
|
|
--
|
|
|
|
-- Note that this is really half the story; in Application.hs, mkYesodDispatch
|
|
-- generates the rest of the code. Please see the following documentation
|
|
-- for an explanation for this split:
|
|
-- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules
|
|
--
|
|
-- This function also generates the following type synonyms:
|
|
-- type Handler = HandlerT RegistryCtx IO
|
|
mkYesodData "RegistryCtx" $(parseRoutesFile "config/routes")
|
|
|
|
|
|
-- Please see the documentation for the Yesod typeclass. There are a number
|
|
-- of settings which can be configured by overriding methods here.
|
|
instance Yesod RegistryCtx where
|
|
-- Store session data on the client in encrypted cookies,
|
|
-- default session idle timeout is 120 minutes
|
|
makeSessionBackend :: RegistryCtx -> IO (Maybe SessionBackend)
|
|
makeSessionBackend _ = pure Nothing
|
|
|
|
|
|
-- Yesod Middleware allows you to run code before and after each handler function.
|
|
-- The defaultYesodMiddleware adds the response header "Vary: Accept, Accept-Language" and performs authorization checks.
|
|
-- Some users may also want to add the defaultCsrfMiddleware, which:
|
|
-- a) Sets a cookie with a CSRF token in it.
|
|
-- b) Validates that incoming write requests include that token in either a header or POST parameter.
|
|
-- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware
|
|
-- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package.
|
|
yesodMiddleware :: ToTypedContent res => Handler res -> Handler res
|
|
yesodMiddleware = defaultYesodMiddleware
|
|
|
|
|
|
-- What messages should be logged. The following includes all messages when
|
|
-- in development, and warnings and errors in production.
|
|
shouldLogIO :: RegistryCtx -> LogSource -> LogLevel -> IO Bool
|
|
shouldLogIO app _source level =
|
|
return $ appShouldLogAll (appSettings app) || level == LevelInfo || level == LevelWarn || level == LevelError
|
|
|
|
|
|
makeLogger :: RegistryCtx -> IO Logger
|
|
makeLogger = return . appLogger
|
|
|
|
|
|
messageLoggerSource :: RegistryCtx -> Logger -> Loc -> LogSource -> LogLevel -> LogStr -> IO ()
|
|
messageLoggerSource ctx logger = \loc src lvl str -> do
|
|
shouldLog <- shouldLogIO ctx src lvl
|
|
when shouldLog $ do
|
|
date <- loggerDate logger
|
|
let formatted =
|
|
toLogStr date
|
|
<> ( toLogStr
|
|
. wrapSGRCode [SetColor Foreground Vivid (colorFor lvl)]
|
|
$ fromLogStr
|
|
( " ["
|
|
<> renderLvl lvl
|
|
<> (if T.null src then mempty else "#" <> toLogStr src)
|
|
<> "] "
|
|
<> str
|
|
)
|
|
)
|
|
<> toLogStr
|
|
( wrapSGRCode
|
|
[SetColor Foreground Dull White]
|
|
[i| @ #{loc_filename loc}:#{fst $ loc_start loc}\n|]
|
|
)
|
|
loggerPutStr logger formatted
|
|
where
|
|
renderLvl lvl = case lvl of
|
|
LevelOther t -> toLogStr t
|
|
_ -> toLogStr @String $ drop 5 $ show lvl
|
|
colorFor = \case
|
|
LevelDebug -> Green
|
|
LevelInfo -> Blue
|
|
LevelWarn -> Yellow
|
|
LevelError -> Red
|
|
LevelOther _ -> White
|
|
|
|
|
|
isAuthorized :: Route RegistryCtx -> Bool -> Handler AuthResult
|
|
isAuthorized route _
|
|
| "admin" `member` routeAttrs route = do
|
|
hasAuthId <- isJust <$> maybeAuthId
|
|
pure $ if hasAuthId then Authorized else Unauthorized "This feature is for admins only"
|
|
| otherwise = pure Authorized
|
|
|
|
|
|
maximumContentLengthIO :: RegistryCtx -> Maybe (Route RegistryCtx) -> IO (Maybe Word64)
|
|
maximumContentLengthIO _ (Just PkgUploadR) = pure Nothing
|
|
maximumContentLengthIO _ (Just EosUploadR) = pure Nothing
|
|
maximumContentLengthIO _ _ = pure $ Just 2097152 -- the original default
|
|
|
|
|
|
-- How to run database actions.
|
|
instance YesodPersist RegistryCtx where
|
|
type YesodPersistBackend RegistryCtx = SqlBackend
|
|
runDB :: SqlPersistT Handler a -> Handler a
|
|
runDB action = runSqlPool action . appConnPool =<< getYesod
|
|
|
|
|
|
instance YesodPersistRunner RegistryCtx where
|
|
getDBRunner :: Handler (DBRunner RegistryCtx, Handler ())
|
|
getDBRunner = defaultGetDBRunner appConnPool
|
|
|
|
|
|
instance RenderMessage RegistryCtx FormMessage where
|
|
renderMessage _ _ = defaultFormMessage
|
|
instance YesodAuth RegistryCtx where
|
|
type AuthId RegistryCtx = Text
|
|
getAuthId = pure . Just . credsIdent
|
|
maybeAuthId = do
|
|
pool <- getsYesod appConnPool
|
|
let checkCreds k s = flip runSqlPool pool $ do
|
|
let passHash = hashWith SHA256 . encodeUtf8 . ("start9_admin:" <>) $ decodeUtf8 s
|
|
selectFirst [AdminDeletedAt ==. Nothing, AdminId ==. (AdminKey $ decodeUtf8 k)] [] <&> \case
|
|
Nothing -> False
|
|
Just adminEntity -> do
|
|
let Admin{adminPassHash} = entityVal adminEntity
|
|
adminPassHash == passHash
|
|
|
|
defaultMaybeBasicAuthId checkCreds defaultAuthSettings
|
|
loginDest _ = PackageIndexR V1
|
|
logoutDest _ = PackageIndexR V1
|
|
authPlugins _ = []
|
|
|
|
|
|
instance YesodAuthPersist RegistryCtx where
|
|
type AuthEntity RegistryCtx = Admin
|
|
getAuthEntity = liftHandler . runDB . get . AdminKey
|
|
|
|
|
|
unsafeHandler :: RegistryCtx -> Handler a -> IO a
|
|
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
|
|
|
|
|
|
-- Note: Some functionality previously present in the scaffolding has been
|
|
-- moved to documentation in the Wiki. Following are some hopefully helpful
|
|
-- links:
|
|
--
|
|
-- https://github.com/yesodweb/yesod/wiki/Sending-email
|
|
-- https://github.com/yesodweb/yesod/wiki/Serve-static-files-from-a-separate-domain
|
|
-- https://github.com/yesodweb/yesod/wiki/i18n-messages-in-the-scaffolding
|
|
|
|
appLogFunc :: RegistryCtx -> LogFunc
|
|
appLogFunc = appLogger <**> messageLoggerSource
|