@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,384 lines (1,170 loc) • 42.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.yesodTemplate = void 0;
exports.yesodTemplate = {
id: 'yesod',
name: 'yesod',
displayName: 'Yesod Full-Stack Web Framework',
description: 'A Haskell web framework focusing on type safety, high performance, and rapid development',
framework: 'yesod',
language: 'haskell',
version: '1.6',
author: 'Re-Shell Team',
featured: true,
recommended: true,
icon: '🏛️',
type: 'full-stack',
complexity: 'advanced',
keywords: ['haskell', 'yesod', 'full-stack', 'type-safe', 'web', 'framework'],
features: [
'Type-safe URLs and routing',
'Compile-time template checking',
'Built-in authentication and authorization',
'Form handling with CSRF protection',
'Database integration with Persistent',
'Automatic RESTful routes',
'WebSocket support',
'Internationalization (i18n)',
'Email sending',
'Background jobs',
'Admin scaffolding',
'Asset management',
'Testing framework'
],
structure: {
'app/Main.hs': `{-# LANGUAGE OverloadedStrings #-}
import Prelude
import Yesod.Default.Config2 (makeYesodRunner, loadYamlSettings, useEnv)
import Yesod.Default.Main (defaultMainLog)
import Application (makeFoundation, makeLogWare)
import Settings (configSettingsYmlValue)
-- | The main function for the application
main :: IO ()
main = do
-- Get the settings from all relevant sources
settings <- loadYamlSettings
["config/settings.yml"]
[] -- No values to override from args
useEnv
-- Generate the foundation from the settings
foundation <- makeFoundation settings
-- Generate a WAI Application from the foundation
app <- makeApplication foundation
-- Run the application with Warp
runSettings (warpSettings foundation) app`,
'src/Application.hs': `{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Application
( getApplicationDev
, appMain
, develMain
, makeFoundation
, makeLogWare
, getApplicationRepl
, handler
, db
) where
import Control.Monad.Logger (liftLoc, runLoggingT)
import Database.Persist.Postgresql (createPostgresqlPool, pgConnStr, pgPoolSize)
import Import
import Language.Haskell.TH.Syntax (qLocation)
import Network.HTTP.Client.TLS (getGlobalManager)
import Network.Wai (Middleware)
import Network.Wai.Handler.Warp (Settings, defaultSettings, defaultShouldDisplayException,
runSettings, setHost, setOnException, setPort)
import Network.Wai.Middleware.RequestLogger (Destination (Callback), IPAddrSource (..),
mkRequestLogger, outputFormat)
import System.Log.FastLogger (defaultBufSize, newStdoutLoggerSet, toLogStr)
-- Import all relevant handler modules here.
-- Don't forget to add new modules to your cabal file!
import Handler.Common
import Handler.Home
import Handler.User
import Handler.Todo
import Handler.Auth
-- This line actually creates our YesodDispatch instance. It is the second half
-- of the call to mkYesodData which occurs in Foundation.hs. Please see the
-- comments there for more details.
mkYesodDispatch "App" resourcesApp
-- | This function allocates resources (such as a database connection pool),
-- performs initialization and returns a foundation datatype value.
makeFoundation :: AppSettings -> IO App
makeFoundation appSettings = do
-- Some basic initializations: HTTP connection manager, logger, and static
-- subsite.
appHttpManager <- getGlobalManager
appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger
appStatic <-
(if appMutableStatic appSettings then staticDevel else static)
(appStaticDir appSettings)
-- Create the database connection pool
appConnPool <- createPoolConfig $ appDatabaseConf appSettings
-- Return the foundation
return App {..}
-- | Convert our foundation to a WAI Application by calling toWaiAppPlain and
-- applying some additional middlewares.
makeApplication :: App -> IO Application
makeApplication foundation = do
logWare <- makeLogWare foundation
-- Create the WAI application and apply middlewares
appPlain <- toWaiAppPlain foundation
return $ logWare $ defaultMiddlewaresNoLogging appPlain
makeLogWare :: App -> IO Middleware
makeLogWare foundation =
mkRequestLogger def
{ outputFormat =
if appDetailedRequestLogging $ appSettings foundation
then Detailed True
else Apache FromFallback
, destination = Callback $ \\str -> do
runLoggingT
(toLogStr str >>= loggerPutStr (appLogger foundation))
(messageLoggerSource foundation (appLogger foundation))
}
-- | Warp settings for the given foundation value.
warpSettings :: App -> Settings
warpSettings foundation =
setPort (appPort $ appSettings foundation)
$ setHost (appHost $ appSettings foundation)
$ setOnException (\\_ e ->
when (defaultShouldDisplayException e) $ runLoggingT
(messageLoggerSource foundation (appLogger foundation)
$(qLocation >>= liftLoc)
"yesod"
LevelError
(toLogStr $ "Exception from Warp: " ++ show e))
(messageLoggerSource foundation (appLogger foundation)))
defaultSettings
-- | For yesod devel, return the Warp settings and WAI Application.
getApplicationDev :: IO (Settings, Application)
getApplicationDev = do
settings <- getAppSettings
foundation <- makeFoundation settings
wsettings <- getDevSettings $ warpSettings foundation
app <- makeApplication foundation
return (wsettings, app)
getAppSettings :: IO AppSettings
getAppSettings = loadYamlSettings ["config/settings.yml"] [] useEnv
-- | main function for use by yesod devel
develMain :: IO ()
develMain = develMainHelper getApplicationDev
-- | The @main@ function for an executable running this site.
appMain :: IO ()
appMain = do
-- Get the settings from all relevant sources
settings <- loadYamlSettingsArgs
-- fall back to compile-time values, set to [] to require values at runtime
[]
-- allow environment variables to override
useEnv
-- Generate the foundation from the settings
foundation <- makeFoundation settings
-- Generate a WAI Application from the foundation
app <- makeApplication foundation
-- Run the application with Warp
runSettings (warpSettings foundation) app
-- | Used for yesod devel and testing
getApplicationRepl :: IO (Int, App, Application)
getApplicationRepl = do
settings <- getAppSettings
foundation <- makeFoundation settings
wsettings <- getDevSettings $ warpSettings foundation
app1 <- makeApplication foundation
return (getPort wsettings, foundation, app1)
-- | Run DB queries
handler :: Handler a -> IO a
handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h
-- | Run DB queries
db :: ReaderT SqlBackend Handler a -> IO a
db = handler . runDB`,
'src/Foundation.hs': `{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}
module Foundation where
import Import.NoFoundation
import Database.Persist.Sql (ConnectionPool, runSqlPool)
import Text.Hamlet (hamletFile)
import Text.Jasmine (minifym)
import Control.Monad.Logger (LogSource)
import Yesod.Auth.Email
import Yesod.Auth.Message (AuthMessage (InvalidLogin))
import Yesod.Default.Util (addStaticContentExternal)
import Yesod.Core.Types (Logger)
import qualified Yesod.Core.Unsafe as Unsafe
import qualified Data.Text.Encoding as TE
-- | 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.
data App = App
{ appSettings :: AppSettings
, appStatic :: Static -- ^ Settings for static file serving.
, appConnPool :: ConnectionPool -- ^ Database connection pool.
, appHttpManager :: Manager
, appLogger :: Logger
}
-- This is where we define all of the routes in our application. For a full
-- explanation of the syntax, please see:
-- https://www.yesodweb.com/book/routing-and-handlers
mkYesodData "App" $(parseRoutesFile "config/routes")
-- | A convenient synonym for creating forms.
type Form x = Html -> MForm (HandlerFor App) (FormResult x, Widget)
-- | A convenient synonym for database access functions.
type DB a = forall (m :: * -> *).
(MonadUnliftIO m) => ReaderT SqlBackend m a
-- Please see the documentation for the Yesod typeclass. There are a number
-- of settings which can be configured by overriding methods here.
instance Yesod App where
-- Controls the base of generated URLs. For more information on modifying,
-- see: https://github.com/yesodweb/yesod/wiki/Overriding-approot
approot :: Approot App
approot = ApprootRequest $ \\app req ->
case appRoot $ appSettings app of
Nothing -> getApprootText guessApproot app req
Just root -> root
-- Store session data on the client in encrypted cookies
makeSessionBackend :: App -> IO (Maybe SessionBackend)
makeSessionBackend _ = Just <$> defaultClientSessionBackend
120 -- timeout in minutes
"config/client_session_key.aes"
-- Yesod Middleware allows you to run code before and after each handler function.
yesodMiddleware :: Handler res -> Handler res
yesodMiddleware = defaultYesodMiddleware
defaultLayout :: Widget -> Handler Html
defaultLayout widget = do
master <- getYesod
mmsg <- getMessage
-- We break up the default layout into two components:
-- default-layout is the contents of the body tag, and
-- default-layout-wrapper is the entire page. Since the final
-- value passed to hamletToRepHtml cannot be a widget, this allows
-- you to use normal widget features in default-layout.
pc <- widgetToPageContent $ do
addStylesheet $ StaticR css_bootstrap_css
$(widgetFile "default-layout")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
-- Authentication
authRoute :: App -> Maybe (Route App)
authRoute _ = Just $ AuthR LoginR
isAuthorized :: Route App -> Bool -> Handler AuthResult
isAuthorized (AuthR _) _ = return Authorized
isAuthorized HomeR _ = return Authorized
isAuthorized FaviconR _ = return Authorized
isAuthorized RobotsR _ = return Authorized
isAuthorized (StaticR _) _ = return Authorized
-- Routes requiring authentication
isAuthorized TodoListR _ = isAuthenticated
isAuthorized (TodoR _) _ = isAuthenticated
isAuthorized ProfileR _ = isAuthenticated
-- Admin routes
isAuthorized AdminR _ = isAdmin
isAuthorized (UserR _) _ = isAdmin
-- This function creates static content files in the static folder
-- and names them based on a hash of their content. This allows
-- expiration dates to be set far in the future without worry of
-- users receiving stale content.
addStaticContent ::
Text -- ^ The file extension
-> Text -- ^ The MIME content type
-> LByteString -- ^ The contents of the file
-> Handler (Maybe (Either Text (Route App, [(Text, Text)])))
addStaticContent ext mime content = do
master <- getYesod
let settings = appSettings master
staticDir = appStaticDir settings
addStaticContentExternal
(if appMinifyResources settings then minifym else id)
genFileName
staticDir
(StaticR . flip StaticRoute [])
ext
mime
content
where
-- Generate a unique filename based on the content itself
genFileName lbs = "autogen-" ++ base64md5 lbs
-- What messages should be logged.
shouldLogIO :: App -> LogSource -> LogLevel -> IO Bool
shouldLogIO app _source level =
return $ appShouldLogAll (appSettings app)
|| level == LevelWarn
|| level == LevelError
makeLogger :: App -> IO Logger
makeLogger = return . appLogger
-- | Require authentication
isAuthenticated :: Handler AuthResult
isAuthenticated = do
muid <- maybeAuthId
return $ case muid of
Nothing -> Unauthorized "You must login to access this page"
Just _ -> Authorized
-- | Require admin privileges
isAdmin :: Handler AuthResult
isAdmin = do
muser <- maybeAuth
return $ case muser of
Nothing -> Unauthorized "You must login to access this page"
Just (Entity _ user) ->
if userAdmin user
then Authorized
else Unauthorized "You must be an admin to access this page"
-- How to run database actions.
instance YesodPersist App where
type YesodPersistBackend App = SqlBackend
runDB :: DB a -> Handler a
runDB action = do
master <- getYesod
runSqlPool action $ appConnPool master
instance YesodPersistRunner App where
getDBRunner :: Handler (DBRunner App, Handler ())
getDBRunner = defaultGetDBRunner appConnPool
-- Authentication
instance YesodAuth App where
type AuthId App = UserId
-- Where to send a user after successful login
loginDest :: App -> Route App
loginDest _ = HomeR
-- Where to send a user after logout
logoutDest :: App -> Route App
logoutDest _ = HomeR
-- Override the above destinations when a Referer: header is present
redirectToReferer :: App -> Bool
redirectToReferer _ = True
authenticate :: (MonadHandler m, HandlerSite m ~ App)
=> Creds App -> m (AuthenticationResult App)
authenticate creds = liftHandler $ runDB $ do
x <- insertBy $ User
(credsIdent creds)
Nothing
Nothing
False
case x of
Left (Entity uid _) -> return $ Authenticated uid
Right uid -> return $ Authenticated uid
-- You can add other plugins like Google Email, email or OAuth here
authPlugins :: App -> [AuthPlugin App]
authPlugins app = [authEmail]
-- Email authentication
instance YesodAuthEmail App where
type AuthEmailId App = UserId
afterPasswordRoute _ = HomeR
addUnverified email verkey = liftHandler $ runDB $ do
insert $ User email Nothing (Just verkey) False
sendVerifyEmail email _ verurl = do
liftIO $ putStrLn $ "Verification email for " ++ show email ++ ": " ++ show verurl
-- In production, actually send email here
getVerifyKey = liftHandler . runDB . fmap (join . fmap userVerkey) . get
setVerifyKey uid key = liftHandler $ runDB $ update uid [UserVerkey =. Just key]
verifyAccount uid = liftHandler $ runDB $ do
mu <- get uid
case mu of
Nothing -> return Nothing
Just _ -> do
update uid [UserVerified =. True]
return $ Just uid
getPassword = liftHandler . runDB . fmap (join . fmap userPassword) . get
setPassword uid pass = liftHandler $ runDB $ update uid [UserPassword =. Just pass]
getEmailCreds email = liftHandler $ runDB $ do
mu <- getBy $ UniqueUser email
case mu of
Nothing -> return Nothing
Just (Entity uid u) -> return $ Just EmailCreds
{ emailCredsId = uid
, emailCredsAuthId = Just uid
, emailCredsStatus = isJust $ userPassword u
, emailCredsVerkey = userVerkey u
, emailCredsEmail = email
}
getEmail = liftHandler . runDB . fmap (fmap userEmail) . get
-- | Access function to determine if a user is logged in.
isAuthenticated :: Handler AuthResult
isAuthenticated = do
muid <- maybeAuthId
return $ case muid of
Nothing -> Unauthorized "You must login to access this page"
Just _ -> Authorized
instance YesodAuthPersist App
-- This instance is required to use forms. You can modify renderMessage to
-- achieve customized and internationalized form validation messages.
instance RenderMessage App FormMessage where
renderMessage :: App -> [Lang] -> FormMessage -> Text
renderMessage _ _ = defaultFormMessage
-- Useful when writing code that is re-usable outside of the Handler context.
instance HasHttpManager App where
getHttpManager :: App -> Manager
getHttpManager = appHttpManager
unsafeHandler :: App -> Handler a -> IO a
unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger`,
'src/Import.hs': `{-# LANGUAGE NoImplicitPrelude #-}
module Import
( module Import
) where
import Foundation as Import
import Import.NoFoundation as Import`,
'src/Import/NoFoundation.hs': `{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module Import.NoFoundation
( module Import
) where
import ClassyPrelude.Yesod as Import
import Model as Import
import Settings as Import
import Settings.StaticFiles as Import
import SharedTypes as Import
import Yesod.Auth as Import
import Yesod.Core.Types as Import (loggerSet)
import Yesod.Default.Config2 as Import`,
'src/Settings.hs': `{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
module Settings where
import ClassyPrelude.Yesod
import qualified Control.Exception as Exception
import Data.Aeson (Result (..), fromJSON, withObject, (.!=), (.:?))
import Data.FileEmbed (embedFile)
import Data.Yaml (decodeEither')
import Database.Persist.Postgresql (PostgresConf)
import Language.Haskell.TH.Syntax (Exp, Name, Q)
import Network.Wai.Handler.Warp (HostPreference)
import Yesod.Default.Config2 (applyEnvValue, configSettingsYml)
import Yesod.Default.Util (WidgetFileSettings, widgetFileNoReload, widgetFileReload)
-- | Runtime settings to configure this application.
data AppSettings = AppSettings
{ appStaticDir :: String
, appDatabaseConf :: PostgresConf
, appRoot :: Maybe Text
, appHost :: HostPreference
, appPort :: Int
, appIpFromHeader :: Bool
, appDetailedRequestLogging :: Bool
, appShouldLogAll :: Bool
, appReloadTemplates :: Bool
, appMutableStatic :: Bool
, appSkipCombining :: Bool
, appAnalytics :: Maybe Text
, appAuthDummyLogin :: Bool
, appMinifyResources :: Bool
}
instance FromJSON AppSettings where
parseJSON = withObject "AppSettings" $ \\o -> do
let defaultEnv = False
appStaticDir <- o .: "static-dir"
appDatabaseConf <- o .: "database"
appRoot <- o .:? "approot"
appHost <- fromString <$> o .: "host"
appPort <- o .: "port"
appIpFromHeader <- o .: "ip-from-header"
appDetailedRequestLogging <- o .:? "detailed-logging" .!= defaultEnv
appShouldLogAll <- o .:? "should-log-all" .!= defaultEnv
appReloadTemplates <- o .:? "reload-templates" .!= defaultEnv
appMutableStatic <- o .:? "mutable-static" .!= defaultEnv
appSkipCombining <- o .:? "skip-combining" .!= defaultEnv
appAnalytics <- o .:? "analytics"
appAuthDummyLogin <- o .:? "auth-dummy-login" .!= defaultEnv
appMinifyResources <- o .:? "minify-resources" .!= not defaultEnv
return AppSettings {..}
-- | Settings for 'widgetFile', such as which template languages to support and
-- default Hamlet settings.
widgetFileSettings :: WidgetFileSettings
widgetFileSettings = def
-- | How static files should be combined.
combineSettings :: CombineSettings
combineSettings = def
-- The rest of this file contains settings which rarely need changing by a
-- user.
widgetFile :: String -> Q Exp
widgetFile = (if appReloadTemplates compileTimeAppSettings
then widgetFileReload
else widgetFileNoReload)
widgetFileSettings
-- | Raw bytes at compile time of @config/settings.yml@
configSettingsYmlBS :: ByteString
configSettingsYmlBS = $(embedFile configSettingsYml)
-- | @config/settings.yml@, parsed to a @Value@.
configSettingsYmlValue :: Value
configSettingsYmlValue = either Exception.throw id
$ decodeEither' configSettingsYmlBS
-- | A version of @AppSettings@ parsed at compile time from @config/settings.yml@.
compileTimeAppSettings :: AppSettings
compileTimeAppSettings =
case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of
Error e -> error e
Success settings -> settings
-- The following two functions can be used to combine multiple CSS or JS files
-- at compile time to decrease the number of http requests.
-- Sample usage (inside a Widget):
--
-- > $(combineStylesheets 'StaticR [style1_css, style2_css])
combineStylesheets :: Name -> [Route Static] -> Q Exp
combineStylesheets = combineStylesheets'
(appSkipCombining compileTimeAppSettings)
combineSettings
combineScripts :: Name -> [Route Static] -> Q Exp
combineScripts = combineScripts'
(appSkipCombining compileTimeAppSettings)
combineSettings`,
'src/Model.hs': `{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
module Model where
import ClassyPrelude.Yesod
import Database.Persist.Quasi
-- You can define all of your database entities in the entities file.
-- You can find more information on persistent and how to declare entities
-- at:
-- https://www.yesodweb.com/book/persistent/
share [mkPersist sqlSettings, mkMigrate "migrateAll"]
$(persistFileWith lowerCaseSettings "config/models.persistentmodels")`,
'src/Handler/Home.hs': `{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE QuasiQuotes #-}
module Handler.Home where
import Import
import Text.Julius (RawJS (..))
-- | Homepage handler
getHomeR :: Handler Html
getHomeR = do
mauth <- maybeAuth
defaultLayout $ do
setTitle "Welcome to Yesod!"
$(widgetFile "homepage")
-- | About page handler
getAboutR :: Handler Html
getAboutR = defaultLayout $ do
setTitle "About"
$(widgetFile "about")`,
'src/Handler/User.hs': `{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
module Handler.User where
import Import
-- | User profile page
getProfileR :: Handler Html
getProfileR = do
userId <- requireAuthId
user <- runDB $ get404 userId
defaultLayout $ do
setTitle "Profile"
$(widgetFile "profile")
-- | Update user profile
postProfileR :: Handler Html
postProfileR = do
userId <- requireAuthId
user <- runDB $ get404 userId
((result, widget), enctype) <- runFormPost $ profileForm user
case result of
FormSuccess ProfileData{..} -> do
runDB $ update userId
[ UserEmail =. profileEmail
]
setMessage "Profile updated successfully"
redirect ProfileR
_ -> defaultLayout $ do
setTitle "Profile"
$(widgetFile "profile-edit")
-- | User list (admin only)
getUserListR :: Handler Html
getUserListR = do
users <- runDB $ selectList [] [Desc UserId]
defaultLayout $ do
setTitle "Users"
$(widgetFile "users")
-- | Individual user page (admin only)
getUserR :: UserId -> Handler Html
getUserR userId = do
user <- runDB $ get404 userId
todos <- runDB $ selectList [TodoUserId ==. userId] [Desc TodoCreated]
defaultLayout $ do
setTitle $ "User: " <> userEmail user
$(widgetFile "user")
-- | Profile form data
data ProfileData = ProfileData
{ profileEmail :: Text
}
-- | Profile form
profileForm :: User -> Form ProfileData
profileForm user = renderDivs $ ProfileData
<$> areq emailField (fieldSettingsLabel "Email") (Just $ userEmail user)`,
'src/Handler/Todo.hs': `{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
module Handler.Todo where
import Import
-- | List all todos for the current user
getTodoListR :: Handler Html
getTodoListR = do
userId <- requireAuthId
todos <- runDB $ selectList [TodoUserId ==. userId] [Desc TodoCreated]
(widget, enctype) <- generateFormPost todoForm
defaultLayout $ do
setTitle "My Todos"
$(widgetFile "todos")
-- | Create a new todo
postTodoListR :: Handler Html
postTodoListR = do
userId <- requireAuthId
((result, widget), enctype) <- runFormPost todoForm
case result of
FormSuccess TodoData{..} -> do
now <- liftIO getCurrentTime
_ <- runDB $ insert $ Todo
{ todoUserId = userId
, todoTitle = todoTitle
, todoDescription = todoDescription
, todoCompleted = False
, todoCreated = now
, todoUpdated = now
}
setMessage "Todo created successfully"
redirect TodoListR
_ -> do
todos <- runDB $ selectList [TodoUserId ==. userId] [Desc TodoCreated]
defaultLayout $ do
setTitle "My Todos"
$(widgetFile "todos")
-- | Get a specific todo
getTodoR :: TodoId -> Handler Html
getTodoR todoId = do
userId <- requireAuthId
todo <- runDB $ get404 todoId
-- Ensure the todo belongs to the current user
when (todoUserId todo /= userId) $
permissionDenied "You don't have permission to view this todo"
defaultLayout $ do
setTitle $ todoTitle todo
$(widgetFile "todo")
-- | Update a todo
postTodoR :: TodoId -> Handler Html
postTodoR todoId = do
userId <- requireAuthId
todo <- runDB $ get404 todoId
-- Ensure the todo belongs to the current user
when (todoUserId todo /= userId) $
permissionDenied "You don't have permission to update this todo"
((result, widget), enctype) <- runFormPost $ todoUpdateForm todo
case result of
FormSuccess TodoData{..} -> do
now <- liftIO getCurrentTime
runDB $ update todoId
[ TodoTitle =. todoTitle
, TodoDescription =. todoDescription
, TodoCompleted =. todoCompleted
, TodoUpdated =. now
]
setMessage "Todo updated successfully"
redirect $ TodoR todoId
_ -> defaultLayout $ do
setTitle "Edit Todo"
$(widgetFile "todo-edit")
-- | Delete a todo
deleteTodoR :: TodoId -> Handler ()
deleteTodoR todoId = do
userId <- requireAuthId
todo <- runDB $ get404 todoId
-- Ensure the todo belongs to the current user
when (todoUserId todo /= userId) $
permissionDenied "You don't have permission to delete this todo"
runDB $ delete todoId
setMessage "Todo deleted successfully"
redirect TodoListR
-- | Todo form data
data TodoData = TodoData
{ todoTitle :: Text
, todoDescription :: Text
, todoCompleted :: Bool
}
-- | Form for creating a new todo
todoForm :: Form TodoData
todoForm = renderDivs $ TodoData
<$> areq textField (fieldSettingsLabel "Title") Nothing
<*> areq textareaField (fieldSettingsLabel "Description") Nothing
<*> pure False
-- | Form for updating an existing todo
todoUpdateForm :: Todo -> Form TodoData
todoUpdateForm todo = renderDivs $ TodoData
<$> areq textField (fieldSettingsLabel "Title") (Just $ todoTitle todo)
<*> areq textareaField (fieldSettingsLabel "Description") (Just $ todoDescription todo)
<*> areq checkBoxField (fieldSettingsLabel "Completed") (Just $ todoCompleted todo)`,
'src/Handler/Auth.hs': `{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE QuasiQuotes #-}
module Handler.Auth where
import Import
import Yesod.Auth.Email
-- | Custom registration handler
getRegisterR :: Handler Html
getRegisterR = do
(widget, enctype) <- generateFormPost registrationForm
defaultLayout $ do
setTitle "Register"
$(widgetFile "register")
-- | Process registration
postRegisterR :: Handler Html
postRegisterR = do
((result, widget), enctype) <- runFormPost registrationForm
case result of
FormSuccess (email, password) -> do
-- Check if user already exists
muser <- runDB $ getBy $ UniqueUser email
case muser of
Just _ -> do
setMessage "Email already registered"
redirect RegisterR
Nothing -> do
-- Create user with unverified status
verkey <- liftIO generateVerificationKey
userId <- runDB $ insert $ User email (Just password) (Just verkey) False
-- Send verification email
sendVerifyEmail email verkey $ \\verurl -> do
-- In production, send actual email
liftIO $ putStrLn $ "Verification URL: " ++ show verurl
setMessage "Registration successful! Please check your email to verify your account."
redirect HomeR
_ -> defaultLayout $ do
setTitle "Register"
$(widgetFile "register")
-- | Registration form
registrationForm :: Form (Text, Text)
registrationForm = renderDivs $ (,)
<$> areq emailField (fieldSettingsLabel "Email") Nothing
<*> areq passwordField (fieldSettingsLabel "Password") Nothing
-- | Generate a random verification key
generateVerificationKey :: IO Text
generateVerificationKey = do
-- In production, use a proper random generator
return "verification-key-placeholder"`,
'src/Handler/Common.hs': `{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
module Handler.Common where
import Data.FileEmbed (embedFile)
import Import
-- These handlers embed files in the executable at compile time to avoid a
-- runtime dependency, and for efficiency.
getFaviconR :: Handler TypedContent
getFaviconR = do
cacheSeconds $ 60 * 60 * 24 * 30 -- cache for a month
return $ TypedContent "image/x-icon"
$ toContent $(embedFile "config/favicon.ico")
getRobotsR :: Handler TypedContent
getRobotsR = return $ TypedContent typePlain
$ toContent $(embedFile "config/robots.txt")`,
'src/SharedTypes.hs': `{-# LANGUAGE NoImplicitPrelude #-}
module SharedTypes where
import ClassyPrelude.Yesod
import Data.Kind (Type)`,
'config/routes': `-- Routes for the application
/static StaticR Static appStatic
/auth AuthR Auth getAuth
/favicon.ico FaviconR GET
/robots.txt RobotsR GET
/ HomeR GET
/about AboutR GET
/profile ProfileR GET POST
/register RegisterR GET POST
/todos TodoListR GET POST
/todos/#TodoId TodoR GET POST DELETE
/admin AdminR GET
/admin/users UserListR GET
/admin/users/#UserId UserR GET`,
'config/models.persistentmodels': `-- Persistent entity definitions
-- https://www.yesodweb.com/book/persistent/
User
email Text
password Text Maybe
verkey Text Maybe
verified Bool
admin Bool default=False
UniqueUser email
deriving Typeable
Todo
userId UserId
title Text
description Text
completed Bool default=False
created UTCTime
updated UTCTime
deriving Show
Email
email Text
userId UserId Maybe
verkey Text Maybe
UniqueEmail email`,
'config/settings.yml': `# Values formatted like "_env:YESOD_ENV_VAR_NAME:default_value" can be overridden by the specified environment variable.
# See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables
static-dir: "_env:YESOD_STATIC_DIR:static"
host: "_env:YESOD_HOST:*4" # any IPv4 host
port: "_env:YESOD_PORT:3000"
ip-from-header: "_env:YESOD_IP_FROM_HEADER:false"
# Default behavior: determine the application root from the request headers.
# Uncomment to set an explicit approot
#approot: "_env:YESOD_APPROOT:http://localhost:3000"
# By default, \`yesod devel\` runs in development, and built executables use
# production settings (see below). To override this, use the following:
#
# development: false
# Optional values with the following production defaults.
# In development, they default to the inverse.
#
# detailed-logging: false
# should-log-all: false
# reload-templates: false
# mutable-static: false
# skip-combining: false
# auth-dummy-login : false
# NB: If you need a numeric value (e.g. 123) to parse as a String, wrap it in single quotes (e.g. "\\\'123\\\'")
# See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings
database:
user: "_env:YESOD_POSTGRES_USER:postgres"
password: "_env:YESOD_POSTGRES_PASSWORD:postgres"
host: "_env:YESOD_POSTGRES_HOST:localhost"
port: "_env:YESOD_POSTGRES_PORT:5432"
database: "_env:YESOD_POSTGRES_DATABASE:yesod_dev"
poolsize: "_env:YESOD_POSTGRES_POOLSIZE:10"
# Google Analytics
# analytics: "_env:YESOD_ANALYTICS:your-google-analytics-id"
# Authentication
auth-dummy-login: "_env:YESOD_AUTH_DUMMY:false"
# Minify resources
minify-resources: "_env:YESOD_MINIFY_RESOURCES:true"`,
'yesod-app.cabal': `cabal-version: 1.12
name: yesod-app
version: 0.1.0.0
description: A Yesod web application with full-stack capabilities
homepage: https://github.com/yourusername/yesod-app#readme
bug-reports: https://github.com/yourusername/yesod-app/issues
author: Your Name
maintainer: your.email@example.com
copyright: 2024 Your Name
license: BSD3
build-type: Simple
extra-source-files:
README.md
CHANGELOG.md
static/css/bootstrap.css
static/fonts/glyphicons-halflings-regular.eot
static/fonts/glyphicons-halflings-regular.svg
static/fonts/glyphicons-halflings-regular.ttf
static/fonts/glyphicons-halflings-regular.woff
config/favicon.ico
config/robots.txt
config/routes
config/models.persistentmodels
templates/*.hamlet
templates/*.julius
templates/*.lucius
templates/*.cassius
source-repository head
type: git
location: https://github.com/yourusername/yesod-app
flag dev
description: Turn on development settings, like auto-reload templates.
manual: False
default: False
flag library-only
description: Build for use with "yesod devel"
manual: False
default: False
library
exposed-modules:
Application
Foundation
Handler.Auth
Handler.Common
Handler.Home
Handler.Todo
Handler.User
Import
Import.NoFoundation
Model
Settings
Settings.StaticFiles
SharedTypes
other-modules:
Paths_yesod_app
hs-source-dirs:
src
build-depends:
aeson >=1.4
, base >=4.9.1.0 && <5
, bytestring >=0.9 && <0.11
, case-insensitive
, classy-prelude >=1.5 && <1.6
, classy-prelude-conduit >=1.5 && <1.6
, classy-prelude-yesod >=1.5 && <1.6
, conduit >=1.0 && <2.0
, containers
, data-default
, directory >=1.1 && <1.4
, fast-logger >=2.2 && <3.1
, file-embed
, foreign-store
, hjsmin >=0.1 && <0.3
, http-client-tls >=0.3 && <0.4
, http-conduit >=2.3 && <2.4
, monad-control >=0.3 && <1.1
, monad-logger >=0.3 && <0.4
, persistent >=2.9 && <2.14
, persistent-postgresql >=2.9 && <2.14
, persistent-template >=2.5 && <2.13
, safe
, shakespeare >=2.0 && <2.1
, template-haskell
, text >=0.11 && <2.0
, time
, unordered-containers
, vector
, wai
, wai-extra >=3.0 && <3.1
, wai-logger >=2.2 && <2.4
, warp >=3.0 && <3.4
, yaml >=0.11 && <0.12
, yesod >=1.6 && <1.7
, yesod-auth >=1.6 && <1.7
, yesod-core >=1.6 && <1.7
, yesod-form >=1.6 && <1.7
, yesod-static >=1.6 && <1.7
if flag(dev) || flag(library-only)
cpp-options: -DDEVELOPMENT
ghc-options: -Wall -fwarn-tabs -O0
else
ghc-options: -Wall -fwarn-tabs -O2
executable yesod-app
main-is: main.hs
other-modules:
DevelMain
Paths_yesod_app
hs-source-dirs:
app
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends:
base
, yesod-app
if flag(library-only)
buildable: False
default-language: Haskell2010
test-suite yesod-app-test
type: exitcode-stdio-1.0
main-is: Spec.hs
other-modules:
Handler.CommonSpec
Handler.HomeSpec
TestImport
Paths_yesod_app
hs-source-dirs:
test
ghc-options: -Wall
build-depends:
base
, classy-prelude >=1.5 && <1.6
, classy-prelude-yesod >=1.5 && <1.6
, hspec >=2.0.0
, microlens
, monad-logger
, persistent
, persistent-postgresql
, resourcet
, shakespeare
, transformers
, wai-extra
, yesod >=1.6 && <1.7
, yesod-app
, yesod-auth >=1.6 && <1.7
, yesod-core
, yesod-test >=1.6 && <1.7
default-language: Haskell2010`,
'stack.yaml': `resolver: lts-21.25
packages:
- .
extra-deps: []
flags: {}
extra-package-dbs: []`,
'.gitignore': `dist*
static/tmp/
static/combined/
config/client_session_key.aes
*.hi
*.o
*.sqlite3
*.sqlite3-shm
*.sqlite3-wal
.hsenv*
cabal-dev/
.stack-work/
.stack-work-devel/
yesod-devel/
.cabal-sandbox
cabal.sandbox.config
.DS_Store
*.swp
*.keter
*~
\\#*`,
'README.md': `# Yesod Full-Stack Web Application
A full-stack web application built with Yesod, featuring type-safe URLs, authentication, and database integration.
## Features
- ✅ Type-safe URLs and routing
- ✅ Compile-time template checking
- ✅ Built-in authentication and authorization
- ✅ Form handling with CSRF protection
- ✅ Database integration with Persistent
- ✅ Email authentication
- ✅ Admin panel
- ✅ WebSocket support
- ✅ Internationalization ready
- ✅ Asset management
- ✅ Testing framework
## Getting Started
### Prerequisites
- Haskell Stack
- PostgreSQL
- Redis (optional, for sessions)
### Development Setup
1. Clone the repository:
\`\`\`bash
git clone <your-repo>
cd yesod-app
\`\`\`
2. Install dependencies:
\`\`\`bash
stack setup
stack build
\`\`\`
3. Set up the database:
\`\`\`bash
createdb yesod_dev
\`\`\`
4. Configure environment:
\`\`\`bash
export YESOD_POSTGRES_USER=postgres
export YESOD_POSTGRES_PASSWORD=postgres
export YESOD_POSTGRES_HOST=localhost
export YESOD_POSTGRES_PORT=5432
export YESOD_POSTGRES_DATABASE=yesod_dev
\`\`\`
5. Run the development server:
\`\`\`bash
stack exec -- yesod devel
\`\`\`
The server will start on http://localhost:3000 with auto-reload enabled.
## Project Structure
\`\`\`
.
├── app/ # Application entry points
├── config/ # Configuration files
│ ├── models.persistentmodels # Database models
│ ├── routes # URL routes
│ └── settings.yml # Application settings
├── src/
│ ├── Application.hs # Application initialization
│ ├── Foundation.hs # Core application type
│ ├── Handler/ # Request handlers
│ ├── Model.hs # Database models
│ └── Settings.hs # Settings management
├── static/ # Static files (CSS, JS, images)
├── templates/ # HTML templates
└── test/ # Test suite
\`\`\`
## Key Concepts
### Type-Safe URLs
Routes are defined in \`config/routes\` and are type-checked at compile time:
\`\`\`
/todos TodoListR GET POST
/todos/#TodoId TodoR GET POST DELETE
\`\`\`
### Database Models
Models are defined in \`config/models.persistentmodels\`:
\`\`\`
User
email Text
password Text Maybe
verified Bool
UniqueUser email
Todo
userId UserId
title Text
completed Bool
\`\`\`
### Templates
Yesod uses Shakespeare templates with compile-time checking:
- \`.hamlet\` - HTML templates
- \`.julius\` - JavaScript templates
- \`.lucius\` - CSS templates
- \`.cassius\` - CSS templates (indentation-based)
### Authentication
Built-in authentication with multiple backends:
- Email/password
- OAuth providers
- Custom authentication
## Testing
Run the test suite:
\`\`\`bash
stack test
\`\`\`
## Production Deployment
### Build for production:
\`\`\`bash
stack build --flag yesod-app:-dev
\`\`\`
### Keter deployment:
\`\`\`bash
yesod keter
\`\`\`
### Docker deployment:
\`\`\`bash
docker build -t yesod-app .
docker run -p 3000:3000 yesod-app
\`\`\`
## Configuration
Configuration is managed through:
- \`config/settings.yml\` - Default settings
- Environment variables - Override defaults
- Runtime configuration - Dynamic settings
## Contributing
1. Fork the repository
2. Create your feature branch
3. Commit your changes
4. Push to the branch
5. Create a Pull Request
## License
BSD3 License - see LICENSE file for details.`
}
};