@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,367 lines (1,162 loc) • 36.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.servantTemplate = void 0;
exports.servantTemplate = {
id: 'servant',
name: 'servant',
displayName: 'Servant Type-Safe REST API',
description: 'A Haskell framework for type-safe web APIs with automatic documentation generation',
framework: 'servant',
language: 'haskell',
version: '0.20',
author: 'Re-Shell Team',
featured: true,
recommended: true,
icon: '🔐',
type: 'rest-api',
complexity: 'intermediate',
keywords: ['haskell', 'servant', 'type-safe', 'rest', 'api', 'functional'],
features: [
'Type-safe routing and request handling',
'Automatic API documentation generation',
'Client library generation',
'Type-level API descriptions',
'JWT authentication',
'Database integration with Persistent',
'Request validation',
'Error handling',
'CORS support',
'Content negotiation',
'Swagger/OpenAPI generation',
'Testing with Hspec',
'Docker deployment'
],
structure: {
'app/Main.hs': `{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Network.Wai
import Network.Wai.Handler.Warp
import Network.Wai.Middleware.Cors
import Network.Wai.Middleware.RequestLogger
import Servant
import System.Environment (lookupEnv)
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import API (app)
import Config (loadConfig, Config(..))
import Database (runMigrations, createConnectionPool)
main :: IO ()
main = do
-- Load configuration
config <- loadConfig
-- Initialize database
pool <- createConnectionPool (dbConfig config)
runMigrations pool
-- Get port from environment or config
portEnv <- lookupEnv "PORT"
let port = fromMaybe (configPort config) (read <$> portEnv)
-- Create the application with middleware
let settings = setPort port $ setLogger logStdoutDev defaultSettings
application = corsMiddleware $ logStdoutDev $ app config pool
putStrLn $ "Starting Servant server on port " ++ show port
runSettings settings application
corsMiddleware :: Middleware
corsMiddleware = cors $ const $ Just simpleCorsResourcePolicy
{ corsOrigins = Nothing
, corsMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
, corsRequestHeaders = ["Content-Type", "Authorization"]
}`,
'src/API.hs': `{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module API
( API
, api
, app
, server
) where
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (ReaderT, runReaderT, asks)
import Data.Text (Text)
import Database.Persist.Postgresql (ConnectionPool)
import Network.Wai (Application)
import Servant
import Servant.Auth.Server
import Config (Config)
import Types
import API.User
import API.Todo
import API.Health
import Auth (authCheck, AuthUser)
-- | Main API type combining all endpoints
type API =
"api" :>
( HealthAPI
:<|> "v1" :>
( PublicAPI
:<|> ProtectedAPI
)
)
-- | Public API endpoints (no authentication required)
type PublicAPI =
"users" :> UserAPI
:<|> "auth" :> AuthAPI
-- | Protected API endpoints (authentication required)
type ProtectedAPI = Auth '[JWT] AuthUser :>
( "todos" :> TodoAPI
:<|> "profile" :> ProfileAPI
)
-- | Complete API including docs
type CompleteAPI =
API
:<|> "docs" :> Raw -- Serve Swagger UI
-- | API proxy
api :: Proxy API
api = Proxy
-- | Environment for handlers
data Env = Env
{ envConfig :: Config
, envPool :: ConnectionPool
}
type AppM = ReaderT Env Handler
-- | Main server implementation
server :: Env -> Server API
server env = hoistServer api (flip runReaderT env) $
healthServer
:<|> ( publicServer
:<|> protectedServer
)
-- | Public endpoints server
publicServer :: ServerT PublicAPI AppM
publicServer =
userServer
:<|> authServer
-- | Protected endpoints server
protectedServer :: AuthResult AuthUser -> ServerT ProtectedAPI AppM
protectedServer (Authenticated user) =
todoServer user
:<|> profileServer user
protectedServer _ = throwAll err401
-- | Create WAI application
app :: Config -> ConnectionPool -> Application
app config pool = serveWithContext completeApi ctx srv
where
env = Env config pool
ctx = authCheck (configJWTKey config) :. EmptyContext
srv = server env :<|> serveDirectoryWebApp "static/docs"
completeApi = Proxy :: Proxy CompleteAPI`,
'src/API/User.hs': `{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module API.User
( UserAPI
, userServer
, AuthAPI
, authServer
) where
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (asks)
import Crypto.BCrypt
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Time.Clock (getCurrentTime, addUTCTime)
import Database.Persist
import Database.Persist.Postgresql
import Servant
import Servant.Auth.Server
import Types
import Models
import Auth (AuthUser(..), generateJWT)
-- | User management API
type UserAPI =
ReqBody '[JSON] CreateUserRequest :> Post '[JSON] UserResponse
:<|> Capture "userId" UserId :> Get '[JSON] UserResponse
:<|> Get '[JSON] [UserResponse]
-- | Authentication API
type AuthAPI =
"login" :> ReqBody '[JSON] LoginRequest :> Post '[JSON] LoginResponse
:<|> "refresh" :> ReqBody '[JSON] RefreshTokenRequest :> Post '[JSON] LoginResponse
-- | User API server implementation
userServer :: ServerT UserAPI AppM
userServer = createUser :<|> getUser :<|> listUsers
-- | Authentication server implementation
authServer :: ServerT AuthAPI AppM
authServer = login :<|> refreshToken
-- | Create a new user
createUser :: CreateUserRequest -> AppM UserResponse
createUser CreateUserRequest{..} = do
pool <- asks envPool
-- Check if user already exists
existingUser <- liftIO $ runSqlPool (selectFirst [UserEmail ==. curEmail] []) pool
case existingUser of
Just _ -> throwError err409 { errBody = "User already exists" }
Nothing -> do
-- Hash password
mHashedPass <- liftIO $ hashPasswordUsingPolicy slowerBcryptHashingPolicy
(TE.encodeUtf8 curPassword)
case mHashedPass of
Nothing -> throwError err500 { errBody = "Failed to hash password" }
Just hashedPass -> do
-- Create user
now <- liftIO getCurrentTime
userId <- liftIO $ runSqlPool (insert User
{ userEmail = curEmail
, userName = curName
, userPasswordHash = TE.decodeUtf8 hashedPass
, userCreatedAt = now
, userUpdatedAt = now
, userActive = True
}) pool
return $ UserResponse userId curEmail curName now
-- | Get user by ID
getUser :: UserId -> AppM UserResponse
getUser userId = do
pool <- asks envPool
mUser <- liftIO $ runSqlPool (get userId) pool
case mUser of
Nothing -> throwError err404
Just user -> return $ UserResponse
userId
(userEmail user)
(userName user)
(userCreatedAt user)
-- | List all users
listUsers :: AppM [UserResponse]
listUsers = do
pool <- asks envPool
users <- liftIO $ runSqlPool (selectList [] [Desc UserCreatedAt]) pool
return $ map entityToResponse users
where
entityToResponse (Entity uid user) = UserResponse
uid
(userEmail user)
(userName user)
(userCreatedAt user)
-- | User login
login :: LoginRequest -> AppM LoginResponse
login LoginRequest{..} = do
pool <- asks envPool
config <- asks envConfig
-- Find user by email
mUser <- liftIO $ runSqlPool (selectFirst [UserEmail ==. lrEmail] []) pool
case mUser of
Nothing -> throwError err401 { errBody = "Invalid credentials" }
Just (Entity userId user) -> do
-- Verify password
let valid = validatePassword (TE.encodeUtf8 $ userPasswordHash user)
(TE.encodeUtf8 lrPassword)
if valid
then do
-- Generate tokens
let authUser = AuthUser userId (userEmail user) (userName user)
accessToken <- liftIO $ generateJWT (configJWTKey config) authUser 3600 -- 1 hour
refreshToken <- liftIO $ generateJWT (configJWTKey config) authUser 604800 -- 7 days
return $ LoginResponse accessToken refreshToken (UserResponse
userId
(userEmail user)
(userName user)
(userCreatedAt user))
else throwError err401 { errBody = "Invalid credentials" }
-- | Refresh access token
refreshToken :: RefreshTokenRequest -> AppM LoginResponse
refreshToken RefreshTokenRequest{..} = do
config <- asks envConfig
pool <- asks envPool
-- Verify refresh token
mAuthUser <- liftIO $ verifyJWT (configJWTKey config) rtrRefreshToken
case mAuthUser of
Nothing -> throwError err401 { errBody = "Invalid refresh token" }
Just authUser -> do
-- Generate new tokens
newAccessToken <- liftIO $ generateJWT (configJWTKey config) authUser 3600
newRefreshToken <- liftIO $ generateJWT (configJWTKey config) authUser 604800
-- Get user data
mUser <- liftIO $ runSqlPool (get $ authUserId authUser) pool
case mUser of
Nothing -> throwError err404
Just user -> return $ LoginResponse
newAccessToken
newRefreshToken
(UserResponse
(authUserId authUser)
(userEmail user)
(userName user)
(userCreatedAt user))`,
'src/API/Todo.hs': `{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module API.Todo
( TodoAPI
, todoServer
) where
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (asks)
import Data.Text (Text)
import Data.Time.Clock (getCurrentTime)
import Database.Persist
import Database.Persist.Postgresql
import Servant
import Types
import Models
import Auth (AuthUser(..))
-- | Todo management API
type TodoAPI =
Get '[JSON] [TodoResponse]
:<|> ReqBody '[JSON] CreateTodoRequest :> Post '[JSON] TodoResponse
:<|> Capture "todoId" TodoId :>
( Get '[JSON] TodoResponse
:<|> ReqBody '[JSON] UpdateTodoRequest :> Put '[JSON] TodoResponse
:<|> Delete '[JSON] NoContent
)
-- | Todo API server implementation
todoServer :: AuthUser -> ServerT TodoAPI AppM
todoServer user =
listTodos user
:<|> createTodo user
:<|> (\\tid -> getTodo user tid
:<|> updateTodo user tid
:<|> deleteTodo user tid)
-- | List todos for authenticated user
listTodos :: AuthUser -> AppM [TodoResponse]
listTodos AuthUser{..} = do
pool <- asks envPool
todos <- liftIO $ runSqlPool
(selectList [TodoUserId ==. authUserId] [Desc TodoCreatedAt]) pool
return $ map entityToResponse todos
where
entityToResponse (Entity tid todo) = TodoResponse
{ trId = tid
, trTitle = todoTitle todo
, trDescription = todoDescription todo
, trCompleted = todoCompleted todo
, trCreatedAt = todoCreatedAt todo
, trUpdatedAt = todoUpdatedAt todo
}
-- | Create a new todo
createTodo :: AuthUser -> CreateTodoRequest -> AppM TodoResponse
createTodo AuthUser{..} CreateTodoRequest{..} = do
pool <- asks envPool
now <- liftIO getCurrentTime
let todo = Todo
{ todoUserId = authUserId
, todoTitle = ctrTitle
, todoDescription = ctrDescription
, todoCompleted = False
, todoCreatedAt = now
, todoUpdatedAt = now
}
todoId <- liftIO $ runSqlPool (insert todo) pool
return $ TodoResponse todoId ctrTitle ctrDescription False now now
-- | Get a specific todo
getTodo :: AuthUser -> TodoId -> AppM TodoResponse
getTodo AuthUser{..} todoId = do
pool <- asks envPool
mTodo <- liftIO $ runSqlPool (get todoId) pool
case mTodo of
Nothing -> throwError err404
Just todo ->
if todoUserId todo == authUserId
then return $ TodoResponse
todoId
(todoTitle todo)
(todoDescription todo)
(todoCompleted todo)
(todoCreatedAt todo)
(todoUpdatedAt todo)
else throwError err403
-- | Update a todo
updateTodo :: AuthUser -> TodoId -> UpdateTodoRequest -> AppM TodoResponse
updateTodo AuthUser{..} todoId UpdateTodoRequest{..} = do
pool <- asks envPool
mTodo <- liftIO $ runSqlPool (get todoId) pool
case mTodo of
Nothing -> throwError err404
Just todo ->
if todoUserId todo == authUserId
then do
now <- liftIO getCurrentTime
liftIO $ runSqlPool (update todoId
[ TodoTitle =. utrTitle
, TodoDescription =. utrDescription
, TodoCompleted =. utrCompleted
, TodoUpdatedAt =. now
]) pool
return $ TodoResponse
todoId
utrTitle
utrDescription
utrCompleted
(todoCreatedAt todo)
now
else throwError err403
-- | Delete a todo
deleteTodo :: AuthUser -> TodoId -> AppM NoContent
deleteTodo AuthUser{..} todoId = do
pool <- asks envPool
mTodo <- liftIO $ runSqlPool (get todoId) pool
case mTodo of
Nothing -> throwError err404
Just todo ->
if todoUserId todo == authUserId
then do
liftIO $ runSqlPool (delete todoId) pool
return NoContent
else throwError err403`,
'src/API/Health.hs': `{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE OverloadedStrings #-}
module API.Health
( HealthAPI
, healthServer
) where
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (asks)
import Data.Text (Text)
import Data.Time.Clock (getCurrentTime)
import Database.Persist.Postgresql
import Servant
import Types
-- | Health check API
type HealthAPI =
"health" :> Get '[JSON] HealthResponse
:<|> "health" :> "db" :> Get '[JSON] HealthResponse
-- | Health check server implementation
healthServer :: ServerT HealthAPI AppM
healthServer = healthCheck :<|> dbHealthCheck
-- | Basic health check
healthCheck :: AppM HealthResponse
healthCheck = do
now <- liftIO getCurrentTime
return $ HealthResponse
{ hrStatus = "ok"
, hrVersion = "1.0.0"
, hrTimestamp = now
, hrServices = [("api", True)]
}
-- | Database health check
dbHealthCheck :: AppM HealthResponse
dbHealthCheck = do
pool <- asks envPool
now <- liftIO getCurrentTime
-- Try to execute a simple query
dbStatus <- liftIO $ runSqlPool (rawSql "SELECT 1" []) pool
let isHealthy = case dbStatus of
[Single (1 :: Int)] -> True
_ -> False
return $ HealthResponse
{ hrStatus = if isHealthy then "ok" else "error"
, hrVersion = "1.0.0"
, hrTimestamp = now
, hrServices = [("api", True), ("database", isHealthy)]
}`,
'src/Types.hs': `{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Types where
import Data.Aeson
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Database.Persist (Key)
import GHC.Generics
import Servant.Auth.Server (FromJWT, ToJWT)
import Models
-- | User creation request
data CreateUserRequest = CreateUserRequest
{ curEmail :: Text
, curPassword :: Text
, curName :: Text
} deriving (Generic, Show)
instance FromJSON CreateUserRequest
instance ToJSON CreateUserRequest
-- | User response
data UserResponse = UserResponse
{ urId :: Key User
, urEmail :: Text
, urName :: Text
, urCreatedAt :: UTCTime
} deriving (Generic, Show)
instance FromJSON UserResponse where
parseJSON = genericParseJSON $ defaultOptions { fieldLabelModifier = drop 2 }
instance ToJSON UserResponse where
toJSON = genericToJSON $ defaultOptions { fieldLabelModifier = drop 2 }
-- | Login request
data LoginRequest = LoginRequest
{ lrEmail :: Text
, lrPassword :: Text
} deriving (Generic, Show)
instance FromJSON LoginRequest
instance ToJSON LoginRequest
-- | Login response
data LoginResponse = LoginResponse
{ lrAccessToken :: Text
, lrRefreshToken :: Text
, lrUser :: UserResponse
} deriving (Generic, Show)
instance FromJSON LoginResponse where
parseJSON = genericParseJSON $ defaultOptions { fieldLabelModifier = drop 2 }
instance ToJSON LoginResponse where
toJSON = genericToJSON $ defaultOptions { fieldLabelModifier = drop 2 }
-- | Refresh token request
data RefreshTokenRequest = RefreshTokenRequest
{ rtrRefreshToken :: Text
} deriving (Generic, Show)
instance FromJSON RefreshTokenRequest
instance ToJSON RefreshTokenRequest
-- | Todo creation request
data CreateTodoRequest = CreateTodoRequest
{ ctrTitle :: Text
, ctrDescription :: Text
} deriving (Generic, Show)
instance FromJSON CreateTodoRequest
instance ToJSON CreateTodoRequest
-- | Todo update request
data UpdateTodoRequest = UpdateTodoRequest
{ utrTitle :: Text
, utrDescription :: Text
, utrCompleted :: Bool
} deriving (Generic, Show)
instance FromJSON UpdateTodoRequest
instance ToJSON UpdateTodoRequest
-- | Todo response
data TodoResponse = TodoResponse
{ trId :: Key Todo
, trTitle :: Text
, trDescription :: Text
, trCompleted :: Bool
, trCreatedAt :: UTCTime
, trUpdatedAt :: UTCTime
} deriving (Generic, Show)
instance FromJSON TodoResponse where
parseJSON = genericParseJSON $ defaultOptions { fieldLabelModifier = drop 2 }
instance ToJSON TodoResponse where
toJSON = genericToJSON $ defaultOptions { fieldLabelModifier = drop 2 }
-- | Health check response
data HealthResponse = HealthResponse
{ hrStatus :: Text
, hrVersion :: Text
, hrTimestamp :: UTCTime
, hrServices :: [(Text, Bool)]
} deriving (Generic, Show)
instance FromJSON HealthResponse where
parseJSON = genericParseJSON $ defaultOptions { fieldLabelModifier = drop 2 }
instance ToJSON HealthResponse where
toJSON = genericToJSON $ defaultOptions { fieldLabelModifier = drop 2 }
-- | Profile API
type ProfileAPI = Get '[JSON] UserResponse
-- | Profile server
profileServer :: AuthUser -> ServerT ProfileAPI AppM
profileServer AuthUser{..} = do
pool <- asks envPool
mUser <- liftIO $ runSqlPool (get authUserId) pool
case mUser of
Nothing -> throwError err404
Just user -> return $ UserResponse authUserId (userEmail user) (userName user) (userCreatedAt user)
-- | Type alias for readability
type AppM = ReaderT Env Handler
-- | Environment
data Env = Env
{ envConfig :: Config
, envPool :: ConnectionPool
}`,
'src/Models.hs': `{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
module Models where
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import Database.Persist.TH
-- | Database models definition using Persistent
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
email Text
name Text
passwordHash Text
createdAt UTCTime
updatedAt UTCTime
active Bool default=True
UniqueEmail email
deriving Show
Todo
userId UserId
title Text
description Text
completed Bool default=False
createdAt UTCTime
updatedAt UTCTime
deriving Show
RefreshToken
userId UserId
token Text
expiresAt UTCTime
createdAt UTCTime
UniqueToken token
deriving Show
|]`,
'src/Auth.hs': `{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Auth
( AuthUser(..)
, authCheck
, generateJWT
, verifyJWT
) where
import Control.Monad (when)
import Control.Monad.IO.Class (liftIO)
import Data.Aeson
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as BSL
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Time.Clock (UTCTime, getCurrentTime, addUTCTime)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds, posixSecondsToUTCTime)
import Database.Persist (Key)
import GHC.Generics
import Servant.Auth.Server
import Models
-- | Authenticated user data
data AuthUser = AuthUser
{ authUserId :: Key User
, authUserEmail :: Text
, authUserName :: Text
} deriving (Generic, Show)
instance FromJSON AuthUser
instance ToJSON AuthUser
instance FromJWT AuthUser
instance ToJWT AuthUser
-- | Create JWT settings for authentication
authCheck :: JWK -> JWTSettings
authCheck key = defaultJWTSettings key
-- | Generate a JWT token
generateJWT :: JWK -> AuthUser -> Integer -> IO Text
generateJWT key user expiresIn = do
now <- getCurrentTime
let expiry = addUTCTime (fromInteger expiresIn) now
eToken <- makeJWT user (defaultJWTSettings key) (Just expiry)
case eToken of
Left err -> error $ "Failed to generate JWT: " ++ show err
Right token -> return $ TE.decodeUtf8 $ BSL.toStrict token
-- | Verify a JWT token
verifyJWT :: JWK -> Text -> IO (Maybe AuthUser)
verifyJWT key token = do
let tokenBS = BSL.fromStrict $ TE.encodeUtf8 token
eUser <- verifyJWT' (defaultJWTSettings key) tokenBS
case eUser of
Nothing -> return Nothing
Just user -> return $ Just user`,
'src/Config.hs': `{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Config
( Config(..)
, DatabaseConfig(..)
, loadConfig
) where
import Control.Monad (when)
import Crypto.JOSE.JWK (JWK, genJWK, KeyMaterialGenParam(OctGenParam))
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS8
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import System.Environment
-- | Application configuration
data Config = Config
{ configPort :: Int
, configJWTKey :: JWK
, configEnvironment :: Text
, dbConfig :: DatabaseConfig
} deriving (Show)
-- | Database configuration
data DatabaseConfig = DatabaseConfig
{ dbHost :: String
, dbPort :: Int
, dbUser :: String
, dbPassword :: String
, dbName :: String
} deriving (Show)
-- | Load configuration from environment
loadConfig :: IO Config
loadConfig = do
port <- read . fromMaybe "3000" <$> lookupEnv "PORT"
env <- T.pack . fromMaybe "development" <$> lookupEnv "ENV"
-- Generate or load JWT key
jwtKey <- genJWK (OctGenParam 256)
-- Database configuration
dbHost <- fromMaybe "localhost" <$> lookupEnv "DB_HOST"
dbPort <- read . fromMaybe "5432" <$> lookupEnv "DB_PORT"
dbUser <- fromMaybe "postgres" <$> lookupEnv "DB_USER"
dbPass <- fromMaybe "postgres" <$> lookupEnv "DB_PASSWORD"
dbName <- fromMaybe "servant_dev" <$> lookupEnv "DB_NAME"
let dbConf = DatabaseConfig dbHost dbPort dbUser dbPass dbName
return $ Config port jwtKey env dbConf`,
'src/Database.hs': `{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
module Database
( createConnectionPool
, runMigrations
, ConnectionPool
) where
import Control.Monad.Logger (runStdoutLoggingT)
import Database.Persist.Postgresql
import Data.ByteString.Char8 (pack)
import Config (DatabaseConfig(..))
import Models (migrateAll)
-- | Create database connection pool
createConnectionPool :: DatabaseConfig -> IO ConnectionPool
createConnectionPool DatabaseConfig{..} = do
let connStr = pack $ concat
[ "host=", dbHost
, " port=", show dbPort
, " user=", dbUser
, " password=", dbPassword
, " dbname=", dbName
]
runStdoutLoggingT $ createPostgresqlPool connStr 10
-- | Run database migrations
runMigrations :: ConnectionPool -> IO ()
runMigrations pool = runStdoutLoggingT $ runSqlPool (runMigration migrateAll) pool`,
'test/Spec.hs': `{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.Wai.Test (SResponse)
import Network.HTTP.Types.Status
import Data.Aeson (encode)
import qualified Data.ByteString.Lazy as BSL
import API (app)
import Config (loadConfig)
import Database (createConnectionPool, runMigrations)
import Types
main :: IO ()
main = do
config <- loadConfig
pool <- createConnectionPool (dbConfig config)
runMigrations pool
let application = app config pool
hspec $ with (return application) $ do
describe "Health Check API" $ do
it "responds with 200 for health check" $ do
get "/api/health" \`shouldRespondWith\` 200
it "returns health status" $ do
get "/api/health" \`shouldRespondWith\`
[json|{status: "ok", version: "1.0.0"}|]
{matchStatus = 200}
describe "User API" $ do
it "creates a new user" $ do
let user = CreateUserRequest "test@example.com" "password123" "Test User"
post "/api/v1/users" (encode user) \`shouldRespondWith\` 200
it "prevents duplicate users" $ do
let user = CreateUserRequest "duplicate@example.com" "password123" "Test User"
post "/api/v1/users" (encode user) \`shouldRespondWith\` 200
post "/api/v1/users" (encode user) \`shouldRespondWith\` 409
it "lists all users" $ do
get "/api/v1/users" \`shouldRespondWith\` 200
describe "Authentication API" $ do
it "allows user login with correct credentials" $ do
-- First create a user
let user = CreateUserRequest "login@example.com" "password123" "Login User"
post "/api/v1/users" (encode user) \`shouldRespondWith\` 200
-- Then login
let loginReq = LoginRequest "login@example.com" "password123"
response <- post "/api/v1/auth/login" (encode loginReq)
liftIO $ statusCode (simpleStatus response) \`shouldBe\` 200
it "rejects login with incorrect credentials" $ do
let loginReq = LoginRequest "wrong@example.com" "wrongpass"
post "/api/v1/auth/login" (encode loginReq) \`shouldRespondWith\` 401
describe "Todo API (Protected)" $ do
it "requires authentication for todo endpoints" $ do
get "/api/v1/todos" \`shouldRespondWith\` 401
it "allows authenticated access to todos" $ do
-- This would require setting up auth tokens in tests
-- For now, just verify the endpoint exists
get "/api/v1/todos" \`shouldRespondWith\` 401`,
'servant-app.cabal': `cabal-version: 1.12
name: servant-app
version: 0.1.0.0
description: A Servant web application with type-safe APIs
homepage: https://github.com/yourusername/servant-app#readme
bug-reports: https://github.com/yourusername/servant-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
source-repository head
type: git
location: https://github.com/yourusername/servant-app
library
exposed-modules:
API
API.Health
API.Todo
API.User
Auth
Config
Database
Models
Types
other-modules:
Paths_servant_app
hs-source-dirs:
src
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
build-depends:
aeson >=1.4 && <2.2
, base >=4.7 && <5
, bcrypt >=0.0.11
, bytestring
, jose >=0.9
, monad-logger
, mtl
, persistent >=2.13
, persistent-postgresql >=2.13
, persistent-template >=2.12
, servant >=0.19
, servant-auth >=0.4
, servant-auth-server >=0.4
, servant-server >=0.19
, text
, time
, wai
, wai-cors
, wai-extra
, warp >=3.3
default-language: Haskell2010
executable servant-app
main-is: Main.hs
other-modules:
Paths_servant_app
hs-source-dirs:
app
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
build-depends:
base >=4.7 && <5
, servant-app
default-language: Haskell2010
test-suite servant-app-test
type: exitcode-stdio-1.0
main-is: Spec.hs
other-modules:
Paths_servant_app
hs-source-dirs:
test
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
build-depends:
aeson
, base >=4.7 && <5
, bytestring
, hspec >=2.6
, hspec-wai >=0.11
, hspec-wai-json >=0.11
, http-types
, servant-app
, wai-extra
default-language: Haskell2010`,
'stack.yaml': `resolver: lts-21.25
packages:
- .
extra-deps: []
flags: {}
extra-package-dbs: []`,
'package.yaml': `name: servant-app
version: 0.1.0.0
github: "yourusername/servant-app"
license: BSD3
author: "Your Name"
maintainer: "your.email@example.com"
copyright: "2024 Your Name"
extra-source-files:
- README.md
- CHANGELOG.md
description: A Servant web application with type-safe APIs
dependencies:
- base >= 4.7 && < 5
- aeson >= 1.4 && < 2.2
- bcrypt >= 0.0.11
- bytestring
- jose >= 0.9
- monad-logger
- mtl
- persistent >= 2.13
- persistent-postgresql >= 2.13
- persistent-template >= 2.12
- servant >= 0.19
- servant-auth >= 0.4
- servant-auth-server >= 0.4
- servant-server >= 0.19
- text
- time
- wai
- wai-cors
- wai-extra
- warp >= 3.3
ghc-options:
- -Wall
- -Wcompat
- -Widentities
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wmissing-export-lists
- -Wmissing-home-modules
- -Wpartial-fields
- -Wredundant-constraints
library:
source-dirs: src
executables:
servant-app:
main: Main.hs
source-dirs: app
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- servant-app
tests:
servant-app-test:
main: Spec.hs
source-dirs: test
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- servant-app
- hspec >= 2.6
- hspec-wai >= 0.11
- hspec-wai-json >= 0.11
- http-types
- wai-extra`,
'Setup.hs': `import Distribution.Simple
main = defaultMain`,
'.gitignore': `dist
dist-*
cabal-dev
*.o
*.hi
*.hie
*.chi
*.chs.h
*.dyn_o
*.dyn_hi
.hpc
.hsenv
.cabal-sandbox/
cabal.sandbox.config
*.prof
*.aux
*.hp
*.eventlog
.stack-work/
cabal.project.local
cabal.project.local~
.HTF/
.ghc.environment.*`,
'README.md': `# Servant Type-Safe REST API
A Haskell web application built with Servant, featuring type-safe APIs and automatic documentation generation.
## Features
- ✅ Type-safe routing and request handling
- ✅ Automatic API documentation generation
- ✅ JWT authentication
- ✅ PostgreSQL database with Persistent ORM
- ✅ Request validation
- ✅ Error handling
- ✅ CORS support
- ✅ Health checks
- ✅ Testing with Hspec
- ✅ Docker deployment
## Getting Started
### Prerequisites
- Haskell Stack or Cabal
- PostgreSQL
- Docker (optional)
### Development Setup
1. Clone the repository:
\`\`\`bash
git clone <your-repo>
cd servant-app
\`\`\`
2. Install dependencies:
\`\`\`bash
stack setup
stack build
\`\`\`
3. Set up the database:
\`\`\`bash
createdb servant_dev
\`\`\`
4. Configure environment variables:
\`\`\`bash
export DB_HOST=localhost
export DB_PORT=5432
export DB_USER=postgres
export DB_PASSWORD=postgres
export DB_NAME=servant_dev
\`\`\`
5. Run the application:
\`\`\`bash
stack run
\`\`\`
The server will start on http://localhost:3000
## API Endpoints
### Public Endpoints
- \`GET /api/health\` - Health check
- \`GET /api/health/db\` - Database health check
- \`POST /api/v1/users\` - Create new user
- \`GET /api/v1/users\` - List all users
- \`POST /api/v1/auth/login\` - User login
- \`POST /api/v1/auth/refresh\` - Refresh access token
### Protected Endpoints (requires authentication)
- \`GET /api/v1/todos\` - List user's todos
- \`POST /api/v1/todos\` - Create new todo
- \`GET /api/v1/todos/:id\` - Get specific todo
- \`PUT /api/v1/todos/:id\` - Update todo
- \`DELETE /api/v1/todos/:id\` - Delete todo
- \`GET /api/v1/profile\` - Get user profile
## Testing
Run the test suite:
\`\`\`bash
stack test
\`\`\`
## Docker Deployment
Build and run with Docker:
\`\`\`bash
docker build -t servant-app .
docker run -p 3000:3000 servant-app
\`\`\`
Or use Docker Compose:
\`\`\`bash
docker-compose up
\`\`\`
## Project Structure
\`\`\`
.
├── app/
│ └── Main.hs # Application entry point
├── src/
│ ├── API.hs # Main API definition
│ ├── API/
│ │ ├── Health.hs # Health check endpoints
│ │ ├── User.hs # User management endpoints
│ │ └── Todo.hs # Todo management endpoints
│ ├── Auth.hs # Authentication logic
│ ├── Config.hs # Configuration management
│ ├── Database.hs # Database connection
│ ├── Models.hs # Database models
│ └── Types.hs # Type definitions
├── test/
│ └── Spec.hs # Test suite
└── servant-app.cabal # Build configuration
\`\`\`
## Contributing
1. Fork the repository
2. Create your feature branch (\`git checkout -b feature/amazing-feature\`)
3. Commit your changes (\`git commit -m 'Add some amazing feature'\`)
4. Push to the branch (\`git push origin feature/amazing-feature\`)
5. Open a Pull Request
## License
This project is licensed under the BSD3 License - see the LICENSE file for details.`,
'Dockerfile': `# Build stage
FROM haskell:9.2.8 AS build
WORKDIR /opt/build
# Copy the package files
COPY servant-app.cabal stack.yaml stack.yaml.lock ./
RUN stack setup
# Build dependencies
RUN stack build --only-dependencies
# Copy application source
COPY . .
# Build application
RUN stack build --copy-bins
# Runtime stage
FROM debian:bullseye-slim
RUN apt-get update && apt-get install -y \\
ca-certificates \\
libpq5 \\
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/app
# Copy the built executable
COPY --from=build /root/.local/bin/servant-app .
# Expose port
EXPOSE 3000
# Run the application
CMD ["./servant-app"]`,
'docker-compose.yml': `version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- PORT=3000
- ENV=production
- DB_HOST=db
- DB_PORT=5432
- DB_USER=postgres
- DB_PASSWORD=postgres
- DB_NAME=servant_prod
depends_on:
- db
networks:
- servant-network
db:
image: postgres:15
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=servant_prod
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- servant-network
volumes:
postgres-data:
networks:
servant-network:
driver: bridge`,
'.env.example': `# Server Configuration
PORT=3000
ENV=development
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=servant_dev
# JWT Configuration
JWT_SECRET=your-secret-key-here`,
'CHANGELOG.md': `# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.0] - 2024-01-15
### Added
- Initial release
- Type-safe REST API with Servant
- JWT authentication
- PostgreSQL integration with Persistent
- User management endpoints
- Todo management endpoints
- Health check endpoints
- Docker deployment configuration
- Comprehensive test suite`
}
};