@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,337 lines (1,150 loc) • 45.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.fsharpGiraffeTemplate = void 0;
exports.fsharpGiraffeTemplate = {
id: 'fsharp-giraffe',
name: 'fsharp-giraffe',
displayName: 'F# Giraffe Web Framework',
description: 'Functional web development on .NET with Giraffe - a composable, type-safe web framework inspired by Suave',
framework: 'giraffe',
language: 'fsharp',
version: '6.0',
author: 'Re-Shell Team',
featured: true,
recommended: true,
icon: '🦒',
type: 'rest-api',
complexity: 'intermediate',
keywords: ['fsharp', 'giraffe', 'functional', 'web', 'dotnet', 'aspnet'],
features: [
'Functional web development',
'Composable HTTP handlers',
'Type-safe routing',
'Built on ASP.NET Core',
'JSON serialization',
'Authentication middleware',
'Dependency injection',
'Model validation',
'Error handling',
'CORS support',
'File serving',
'View engines',
'Testing support',
'Docker integration'
],
structure: {
'Program.fs': `open System
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.Logging
open Giraffe
open FSharpGiraffeApp.Handlers
open FSharpGiraffeApp.Models
open FSharpGiraffeApp.Services
let configureServices (services: IServiceCollection) =
// Add Giraffe dependencies
services.AddGiraffe() |> ignore
// Add logging
services.AddLogging() |> ignore
// Add CORS
services.AddCors(fun options ->
options.AddPolicy("AllowAll", fun policy ->
policy
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
|> ignore
)
) |> ignore
// Add JSON serialization
services.AddSingleton<Json.ISerializer>(NewtonsoftJson.Serializer(NewtonsoftJson.Serializer.DefaultSettings)) |> ignore
// Add application services
services.AddScoped<IUserService, UserService>() |> ignore
services.AddScoped<ITodoService, TodoService>() |> ignore
services.AddScoped<IAuthService, AuthService>() |> ignore
let configureApp (app: IApplicationBuilder) =
app.UseGiraffe Routes.webApp
let configureLogging (builder: ILoggingBuilder) =
builder.AddConsole().AddDebug() |> ignore
[<EntryPoint>]
let main args =
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(fun webHostBuilder ->
webHostBuilder
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
.Configure(configureApp)
|> ignore
)
.Build()
.Run()
0`,
'Handlers/UserHandlers.fs': `namespace FSharpGiraffeApp.Handlers
open System
open Microsoft.AspNetCore.Http
open FSharp.Control.Tasks
open Giraffe
open FSharpGiraffeApp.Models
open FSharpGiraffeApp.Services
module UserHandlers =
let getUsers: HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let userService = ctx.GetService<IUserService>()
let! users = userService.GetAllUsersAsync()
return! json users next ctx
}
let getUserById (id: Guid): HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let userService = ctx.GetService<IUserService>()
let! user = userService.GetUserByIdAsync(id)
match user with
| Some u -> return! json u next ctx
| None -> return! RequestErrors.NOT_FOUND "User not found" next ctx
}
let createUser: HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let! createUserDto = ctx.BindJsonAsync<CreateUserDto>()
let userService = ctx.GetService<IUserService>()
// Validate the model
match validateCreateUser createUserDto with
| Ok validDto ->
let! newUser = userService.CreateUserAsync(validDto)
ctx.SetStatusCode 201
return! json newUser next ctx
| Error errors ->
return! RequestErrors.BAD_REQUEST errors next ctx
}
let updateUser (id: Guid): HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let! updateUserDto = ctx.BindJsonAsync<UpdateUserDto>()
let userService = ctx.GetService<IUserService>()
match validateUpdateUser updateUserDto with
| Ok validDto ->
let! updatedUser = userService.UpdateUserAsync(id, validDto)
match updatedUser with
| Some u -> return! json u next ctx
| None -> return! RequestErrors.NOT_FOUND "User not found" next ctx
| Error errors ->
return! RequestErrors.BAD_REQUEST errors next ctx
}
let deleteUser (id: Guid): HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let userService = ctx.GetService<IUserService>()
let! success = userService.DeleteUserAsync(id)
if success then
return! Successful.NO_CONTENT next ctx
else
return! RequestErrors.NOT_FOUND "User not found" next ctx
}`,
'Handlers/TodoHandlers.fs': `namespace FSharpGiraffeApp.Handlers
open System
open Microsoft.AspNetCore.Http
open FSharp.Control.Tasks
open Giraffe
open FSharpGiraffeApp.Models
open FSharpGiraffeApp.Services
module TodoHandlers =
let getTodos: HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let todoService = ctx.GetService<ITodoService>()
let! todos = todoService.GetAllTodosAsync()
return! json todos next ctx
}
let getTodoById (id: Guid): HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let todoService = ctx.GetService<ITodoService>()
let! todo = todoService.GetTodoByIdAsync(id)
match todo with
| Some t -> return! json t next ctx
| None -> return! RequestErrors.NOT_FOUND "Todo not found" next ctx
}
let createTodo: HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let! createTodoDto = ctx.BindJsonAsync<CreateTodoDto>()
let todoService = ctx.GetService<ITodoService>()
match validateCreateTodo createTodoDto with
| Ok validDto ->
let! newTodo = todoService.CreateTodoAsync(validDto)
ctx.SetStatusCode 201
return! json newTodo next ctx
| Error errors ->
return! RequestErrors.BAD_REQUEST errors next ctx
}
let updateTodo (id: Guid): HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let! updateTodoDto = ctx.BindJsonAsync<UpdateTodoDto>()
let todoService = ctx.GetService<ITodoService>()
match validateUpdateTodo updateTodoDto with
| Ok validDto ->
let! updatedTodo = todoService.UpdateTodoAsync(id, validDto)
match updatedTodo with
| Some t -> return! json t next ctx
| None -> return! RequestErrors.NOT_FOUND "Todo not found" next ctx
| Error errors ->
return! RequestErrors.BAD_REQUEST errors next ctx
}
let deleteTodo (id: Guid): HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let todoService = ctx.GetService<ITodoService>()
let! success = todoService.DeleteTodoAsync(id)
if success then
return! Successful.NO_CONTENT next ctx
else
return! RequestErrors.NOT_FOUND "Todo not found" next ctx
}
let toggleTodo (id: Guid): HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let todoService = ctx.GetService<ITodoService>()
let! updatedTodo = todoService.ToggleTodoAsync(id)
match updatedTodo with
| Some t -> return! json t next ctx
| None -> return! RequestErrors.NOT_FOUND "Todo not found" next ctx
}`,
'Handlers/AuthHandlers.fs': `namespace FSharpGiraffeApp.Handlers
open System
open Microsoft.AspNetCore.Http
open FSharp.Control.Tasks
open Giraffe
open FSharpGiraffeApp.Models
open FSharpGiraffeApp.Services
module AuthHandlers =
let login: HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let! loginDto = ctx.BindJsonAsync<LoginDto>()
let authService = ctx.GetService<IAuthService>()
match validateLogin loginDto with
| Ok validDto ->
let! result = authService.LoginAsync(validDto)
match result with
| Ok authResult -> return! json authResult next ctx
| Error error -> return! RequestErrors.UNAUTHORIZED "text/plain" error next ctx
| Error errors ->
return! RequestErrors.BAD_REQUEST errors next ctx
}
let register: HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let! registerDto = ctx.BindJsonAsync<RegisterDto>()
let authService = ctx.GetService<IAuthService>()
match validateRegister registerDto with
| Ok validDto ->
let! result = authService.RegisterAsync(validDto)
match result with
| Ok authResult ->
ctx.SetStatusCode 201
return! json authResult next ctx
| Error error -> return! RequestErrors.BAD_REQUEST error next ctx
| Error errors ->
return! RequestErrors.BAD_REQUEST errors next ctx
}
let refresh: HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let! refreshDto = ctx.BindJsonAsync<RefreshTokenDto>()
let authService = ctx.GetService<IAuthService>()
let! result = authService.RefreshTokenAsync(refreshDto.RefreshToken)
match result with
| Ok authResult -> return! json authResult next ctx
| Error error -> return! RequestErrors.UNAUTHORIZED "text/plain" error next ctx
}
let profile: HttpHandler =
requiresAuthentication (
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let userId = getUserId ctx
let userService = ctx.GetService<IUserService>()
let! user = userService.GetUserByIdAsync(userId)
match user with
| Some u -> return! json u next ctx
| None -> return! RequestErrors.NOT_FOUND "User not found" next ctx
}
)`,
'Routes.fs': `namespace FSharpGiraffeApp
open Giraffe
open FSharpGiraffeApp.Handlers
module Routes =
let webApp: HttpHandler =
choose [
// Health check
route "/health" >=> Successful.OK "Healthy"
// Auth routes
subRoute "/auth" (
choose [
route "/login" >=> POST >=> AuthHandlers.login
route "/register" >=> POST >=> AuthHandlers.register
route "/refresh" >=> POST >=> AuthHandlers.refresh
route "/profile" >=> GET >=> AuthHandlers.profile
]
)
// API routes
subRoute "/api" (
choose [
// User routes
subRoute "/users" (
choose [
route "" >=> GET >=> UserHandlers.getUsers
route "" >=> POST >=> UserHandlers.createUser
routef "/%O" UserHandlers.getUserById >=> GET
routef "/%O" UserHandlers.updateUser >=> PUT
routef "/%O" UserHandlers.deleteUser >=> DELETE
]
)
// Todo routes
subRoute "/todos" (
choose [
route "" >=> GET >=> TodoHandlers.getTodos
route "" >=> POST >=> TodoHandlers.createTodo
routef "/%O" TodoHandlers.getTodoById >=> GET
routef "/%O" TodoHandlers.updateTodo >=> PUT
routef "/%O" TodoHandlers.deleteTodo >=> DELETE
routef "/%O/toggle" TodoHandlers.toggleTodo >=> POST
]
)
]
)
// Static files
route "/" >=> htmlFile "wwwroot/index.html"
// 404 handler
RequestErrors.NOT_FOUND "Resource not found"
]`,
'Models/User.fs': `namespace FSharpGiraffeApp.Models
open System
open System.ComponentModel.DataAnnotations
[<CLIMutable>]
type User = {
Id: Guid
Email: string
FirstName: string
LastName: string
IsActive: bool
CreatedAt: DateTime
UpdatedAt: DateTime
}
[<CLIMutable>]
type CreateUserDto = {
[<Required>]
[<EmailAddress>]
Email: string
[<Required>]
[<StringLength(50, MinimumLength = 2)>]
FirstName: string
[<Required>]
[<StringLength(50, MinimumLength = 2)>]
LastName: string
[<Required>]
[<StringLength(100, MinimumLength = 8)>]
Password: string
}
[<CLIMutable>]
type UpdateUserDto = {
[<EmailAddress>]
Email: string option
[<StringLength(50, MinimumLength = 2)>]
FirstName: string option
[<StringLength(50, MinimumLength = 2)>]
LastName: string option
IsActive: bool option
}
// Validation functions
let validateCreateUser (dto: CreateUserDto): Result<CreateUserDto, string list> =
let errors = ResizeArray<string>()
if String.IsNullOrWhiteSpace(dto.Email) then
errors.Add("Email is required")
elif not (dto.Email.Contains("@")) then
errors.Add("Email must be valid")
if String.IsNullOrWhiteSpace(dto.FirstName) then
errors.Add("First name is required")
elif dto.FirstName.Length < 2 then
errors.Add("First name must be at least 2 characters")
if String.IsNullOrWhiteSpace(dto.LastName) then
errors.Add("Last name is required")
elif dto.LastName.Length < 2 then
errors.Add("Last name must be at least 2 characters")
if String.IsNullOrWhiteSpace(dto.Password) then
errors.Add("Password is required")
elif dto.Password.Length < 8 then
errors.Add("Password must be at least 8 characters")
if errors.Count = 0 then
Ok dto
else
Error (errors |> List.ofSeq)
let validateUpdateUser (dto: UpdateUserDto): Result<UpdateUserDto, string list> =
let errors = ResizeArray<string>()
match dto.Email with
| Some email when String.IsNullOrWhiteSpace(email) || not (email.Contains("@")) ->
errors.Add("Email must be valid if provided")
| _ -> ()
match dto.FirstName with
| Some firstName when String.IsNullOrWhiteSpace(firstName) || firstName.Length < 2 ->
errors.Add("First name must be at least 2 characters if provided")
| _ -> ()
match dto.LastName with
| Some lastName when String.IsNullOrWhiteSpace(lastName) || lastName.Length < 2 ->
errors.Add("Last name must be at least 2 characters if provided")
| _ -> ()
if errors.Count = 0 then
Ok dto
else
Error (errors |> List.ofSeq)`,
'Models/Todo.fs': `namespace FSharpGiraffeApp.Models
open System
open System.ComponentModel.DataAnnotations
[<CLIMutable>]
type Todo = {
Id: Guid
Title: string
Description: string option
IsCompleted: bool
DueDate: DateTime option
UserId: Guid
CreatedAt: DateTime
UpdatedAt: DateTime
}
[<CLIMutable>]
type CreateTodoDto = {
[<Required>]
[<StringLength(200, MinimumLength = 1)>]
Title: string
[<StringLength(1000)>]
Description: string option
DueDate: DateTime option
[<Required>]
UserId: Guid
}
[<CLIMutable>]
type UpdateTodoDto = {
[<StringLength(200, MinimumLength = 1)>]
Title: string option
[<StringLength(1000)>]
Description: string option
IsCompleted: bool option
DueDate: DateTime option
}
// Validation functions
let validateCreateTodo (dto: CreateTodoDto): Result<CreateTodoDto, string list> =
let errors = ResizeArray<string>()
if String.IsNullOrWhiteSpace(dto.Title) then
errors.Add("Title is required")
elif dto.Title.Length > 200 then
errors.Add("Title must be 200 characters or less")
match dto.Description with
| Some desc when desc.Length > 1000 ->
errors.Add("Description must be 1000 characters or less")
| _ -> ()
if dto.UserId = Guid.Empty then
errors.Add("User ID is required")
if errors.Count = 0 then
Ok dto
else
Error (errors |> List.ofSeq)
let validateUpdateTodo (dto: UpdateTodoDto): Result<UpdateTodoDto, string list> =
let errors = ResizeArray<string>()
match dto.Title with
| Some title when String.IsNullOrWhiteSpace(title) ->
errors.Add("Title cannot be empty if provided")
| Some title when title.Length > 200 ->
errors.Add("Title must be 200 characters or less")
| _ -> ()
match dto.Description with
| Some desc when desc.Length > 1000 ->
errors.Add("Description must be 1000 characters or less")
| _ -> ()
if errors.Count = 0 then
Ok dto
else
Error (errors |> List.ofSeq)`,
'Models/Auth.fs': `namespace FSharpGiraffeApp.Models
open System
open System.ComponentModel.DataAnnotations
[<CLIMutable>]
type LoginDto = {
[<Required>]
[<EmailAddress>]
Email: string
[<Required>]
Password: string
}
[<CLIMutable>]
type RegisterDto = {
[<Required>]
[<EmailAddress>]
Email: string
[<Required>]
[<StringLength(50, MinimumLength = 2)>]
FirstName: string
[<Required>]
[<StringLength(50, MinimumLength = 2)>]
LastName: string
[<Required>]
[<StringLength(100, MinimumLength = 8)>]
Password: string
[<Required>]
[<Compare("Password")>]
ConfirmPassword: string
}
[<CLIMutable>]
type RefreshTokenDto = {
[<Required>]
RefreshToken: string
}
[<CLIMutable>]
type AuthResult = {
AccessToken: string
RefreshToken: string
ExpiresIn: int
User: User
}
[<CLIMutable>]
type JwtClaims = {
UserId: Guid
Email: string
FirstName: string
LastName: string
}
// Validation functions
let validateLogin (dto: LoginDto): Result<LoginDto, string list> =
let errors = ResizeArray<string>()
if String.IsNullOrWhiteSpace(dto.Email) then
errors.Add("Email is required")
elif not (dto.Email.Contains("@")) then
errors.Add("Email must be valid")
if String.IsNullOrWhiteSpace(dto.Password) then
errors.Add("Password is required")
if errors.Count = 0 then
Ok dto
else
Error (errors |> List.ofSeq)
let validateRegister (dto: RegisterDto): Result<RegisterDto, string list> =
let errors = ResizeArray<string>()
if String.IsNullOrWhiteSpace(dto.Email) then
errors.Add("Email is required")
elif not (dto.Email.Contains("@")) then
errors.Add("Email must be valid")
if String.IsNullOrWhiteSpace(dto.FirstName) then
errors.Add("First name is required")
elif dto.FirstName.Length < 2 then
errors.Add("First name must be at least 2 characters")
if String.IsNullOrWhiteSpace(dto.LastName) then
errors.Add("Last name is required")
elif dto.LastName.Length < 2 then
errors.Add("Last name must be at least 2 characters")
if String.IsNullOrWhiteSpace(dto.Password) then
errors.Add("Password is required")
elif dto.Password.Length < 8 then
errors.Add("Password must be at least 8 characters")
if dto.Password <> dto.ConfirmPassword then
errors.Add("Passwords do not match")
if errors.Count = 0 then
Ok dto
else
Error (errors |> List.ofSeq)`,
'Services/UserService.fs': `namespace FSharpGiraffeApp.Services
open System
open System.Collections.Generic
open FSharp.Control.Tasks
open FSharpGiraffeApp.Models
type IUserService =
abstract member GetAllUsersAsync: unit -> Task<User list>
abstract member GetUserByIdAsync: Guid -> Task<User option>
abstract member GetUserByEmailAsync: string -> Task<User option>
abstract member CreateUserAsync: CreateUserDto -> Task<User>
abstract member UpdateUserAsync: Guid * UpdateUserDto -> Task<User option>
abstract member DeleteUserAsync: Guid -> Task<bool>
type UserService() =
// In-memory storage (replace with actual database)
let mutable users = ResizeArray<User>()
// Initialize with sample data
do
let sampleUsers = [
{
Id = Guid.NewGuid()
Email = "john.doe@example.com"
FirstName = "John"
LastName = "Doe"
IsActive = true
CreatedAt = DateTime.UtcNow.AddDays(-30.0)
UpdatedAt = DateTime.UtcNow.AddDays(-30.0)
}
{
Id = Guid.NewGuid()
Email = "jane.smith@example.com"
FirstName = "Jane"
LastName = "Smith"
IsActive = true
CreatedAt = DateTime.UtcNow.AddDays(-20.0)
UpdatedAt = DateTime.UtcNow.AddDays(-20.0)
}
]
users.AddRange(sampleUsers)
interface IUserService with
member _.GetAllUsersAsync() =
task {
// Simulate async operation
do! Task.Delay(10)
return users |> List.ofSeq
}
member _.GetUserByIdAsync(id: Guid) =
task {
do! Task.Delay(10)
return users |> Seq.tryFind (fun u -> u.Id = id)
}
member _.GetUserByEmailAsync(email: string) =
task {
do! Task.Delay(10)
return users |> Seq.tryFind (fun u -> u.Email.Equals(email, StringComparison.OrdinalIgnoreCase))
}
member _.CreateUserAsync(dto: CreateUserDto) =
task {
do! Task.Delay(10)
let newUser = {
Id = Guid.NewGuid()
Email = dto.Email
FirstName = dto.FirstName
LastName = dto.LastName
IsActive = true
CreatedAt = DateTime.UtcNow
UpdatedAt = DateTime.UtcNow
}
users.Add(newUser)
return newUser
}
member _.UpdateUserAsync(id: Guid, dto: UpdateUserDto) =
task {
do! Task.Delay(10)
let userIndex = users |> Seq.tryFindIndex (fun u -> u.Id = id)
match userIndex with
| Some index ->
let existingUser = users.[index]
let updatedUser = {
existingUser with
Email = dto.Email |> Option.defaultValue existingUser.Email
FirstName = dto.FirstName |> Option.defaultValue existingUser.FirstName
LastName = dto.LastName |> Option.defaultValue existingUser.LastName
IsActive = dto.IsActive |> Option.defaultValue existingUser.IsActive
UpdatedAt = DateTime.UtcNow
}
users.[index] <- updatedUser
return Some updatedUser
| None -> return None
}
member _.DeleteUserAsync(id: Guid) =
task {
do! Task.Delay(10)
let userIndex = users |> Seq.tryFindIndex (fun u -> u.Id = id)
match userIndex with
| Some index ->
users.RemoveAt(index)
return true
| None -> return false
}`,
'Services/TodoService.fs': `namespace FSharpGiraffeApp.Services
open System
open System.Collections.Generic
open FSharp.Control.Tasks
open FSharpGiraffeApp.Models
type ITodoService =
abstract member GetAllTodosAsync: unit -> Task<Todo list>
abstract member GetTodoByIdAsync: Guid -> Task<Todo option>
abstract member GetTodosByUserIdAsync: Guid -> Task<Todo list>
abstract member CreateTodoAsync: CreateTodoDto -> Task<Todo>
abstract member UpdateTodoAsync: Guid * UpdateTodoDto -> Task<Todo option>
abstract member DeleteTodoAsync: Guid -> Task<bool>
abstract member ToggleTodoAsync: Guid -> Task<Todo option>
type TodoService() =
// In-memory storage (replace with actual database)
let mutable todos = ResizeArray<Todo>()
// Initialize with sample data
do
let sampleTodos = [
{
Id = Guid.NewGuid()
Title = "Learn F# with Giraffe"
Description = Some "Build a web API using functional programming"
IsCompleted = false
DueDate = Some (DateTime.UtcNow.AddDays(7.0))
UserId = Guid.NewGuid()
CreatedAt = DateTime.UtcNow.AddDays(-2.0)
UpdatedAt = DateTime.UtcNow.AddDays(-2.0)
}
{
Id = Guid.NewGuid()
Title = "Write unit tests"
Description = Some "Add comprehensive test coverage"
IsCompleted = true
DueDate = None
UserId = Guid.NewGuid()
CreatedAt = DateTime.UtcNow.AddDays(-5.0)
UpdatedAt = DateTime.UtcNow.AddDays(-1.0)
}
]
todos.AddRange(sampleTodos)
interface ITodoService with
member _.GetAllTodosAsync() =
task {
do! Task.Delay(10)
return todos |> List.ofSeq
}
member _.GetTodoByIdAsync(id: Guid) =
task {
do! Task.Delay(10)
return todos |> Seq.tryFind (fun t -> t.Id = id)
}
member _.GetTodosByUserIdAsync(userId: Guid) =
task {
do! Task.Delay(10)
return todos |> Seq.filter (fun t -> t.UserId = userId) |> List.ofSeq
}
member _.CreateTodoAsync(dto: CreateTodoDto) =
task {
do! Task.Delay(10)
let newTodo = {
Id = Guid.NewGuid()
Title = dto.Title
Description = dto.Description
IsCompleted = false
DueDate = dto.DueDate
UserId = dto.UserId
CreatedAt = DateTime.UtcNow
UpdatedAt = DateTime.UtcNow
}
todos.Add(newTodo)
return newTodo
}
member _.UpdateTodoAsync(id: Guid, dto: UpdateTodoDto) =
task {
do! Task.Delay(10)
let todoIndex = todos |> Seq.tryFindIndex (fun t -> t.Id = id)
match todoIndex with
| Some index ->
let existingTodo = todos.[index]
let updatedTodo = {
existingTodo with
Title = dto.Title |> Option.defaultValue existingTodo.Title
Description = dto.Description |> Option.orElse existingTodo.Description
IsCompleted = dto.IsCompleted |> Option.defaultValue existingTodo.IsCompleted
DueDate = dto.DueDate |> Option.orElse existingTodo.DueDate
UpdatedAt = DateTime.UtcNow
}
todos.[index] <- updatedTodo
return Some updatedTodo
| None -> return None
}
member _.DeleteTodoAsync(id: Guid) =
task {
do! Task.Delay(10)
let todoIndex = todos |> Seq.tryFindIndex (fun t -> t.Id = id)
match todoIndex with
| Some index ->
todos.RemoveAt(index)
return true
| None -> return false
}
member _.ToggleTodoAsync(id: Guid) =
task {
do! Task.Delay(10)
let todoIndex = todos |> Seq.tryFindIndex (fun t -> t.Id = id)
match todoIndex with
| Some index ->
let existingTodo = todos.[index]
let updatedTodo = {
existingTodo with
IsCompleted = not existingTodo.IsCompleted
UpdatedAt = DateTime.UtcNow
}
todos.[index] <- updatedTodo
return Some updatedTodo
| None -> return None
}`,
'Services/AuthService.fs': `namespace FSharpGiraffeApp.Services
open System
open System.IdentityModel.Tokens.Jwt
open System.Security.Claims
open System.Text
open Microsoft.IdentityModel.Tokens
open FSharp.Control.Tasks
open FSharpGiraffeApp.Models
type IAuthService =
abstract member LoginAsync: LoginDto -> Task<Result<AuthResult, string>>
abstract member RegisterAsync: RegisterDto -> Task<Result<AuthResult, string>>
abstract member RefreshTokenAsync: string -> Task<Result<AuthResult, string>>
abstract member ValidateTokenAsync: string -> Task<JwtClaims option>
type AuthService(userService: IUserService) =
let jwtSecret = "your-super-secret-jwt-key-here-should-be-much-longer-and-more-secure"
let jwtIssuer = "FSharpGiraffeApp"
let jwtAudience = "FSharpGiraffeApp"
let jwtExpiryMinutes = 60
let generateAccessToken (user: User): string =
let key = SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret))
let credentials = SigningCredentials(key, SecurityAlgorithms.HmacSha256)
let claims = [
Claim(ClaimTypes.NameIdentifier, user.Id.ToString())
Claim(ClaimTypes.Email, user.Email)
Claim(ClaimTypes.GivenName, user.FirstName)
Claim(ClaimTypes.Surname, user.LastName)
]
let token = JwtSecurityToken(
issuer = jwtIssuer,
audience = jwtAudience,
claims = claims,
expires = DateTime.UtcNow.AddMinutes(float jwtExpiryMinutes),
signingCredentials = credentials
)
JwtSecurityTokenHandler().WriteToken(token)
let generateRefreshToken(): string =
let bytes = Array.zeroCreate 32
use rng = System.Security.Cryptography.RandomNumberGenerator.Create()
rng.GetBytes(bytes)
Convert.ToBase64String(bytes)
let hashPassword (password: string): string =
// In production, use BCrypt or similar
let salt = "your-salt-here"
let combined = password + salt
use sha256 = System.Security.Cryptography.SHA256.Create()
let hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(combined))
Convert.ToBase64String(hash)
let verifyPassword (password: string) (hashedPassword: string): bool =
let hashedInput = hashPassword password
hashedInput = hashedPassword
interface IAuthService with
member _.LoginAsync(dto: LoginDto) =
task {
let! userOption = userService.GetUserByEmailAsync(dto.Email)
match userOption with
| Some user ->
// In production, verify actual hashed password
if dto.Password = "password" || verifyPassword dto.Password "stored-hash" then
let accessToken = generateAccessToken user
let refreshToken = generateRefreshToken()
let authResult = {
AccessToken = accessToken
RefreshToken = refreshToken
ExpiresIn = jwtExpiryMinutes * 60
User = user
}
return Ok authResult
else
return Error "Invalid credentials"
| None ->
return Error "User not found"
}
member _.RegisterAsync(dto: RegisterDto) =
task {
let! existingUser = userService.GetUserByEmailAsync(dto.Email)
match existingUser with
| Some _ ->
return Error "User with this email already exists"
| None ->
let createUserDto = {
Email = dto.Email
FirstName = dto.FirstName
LastName = dto.LastName
Password = dto.Password
}
let! newUser = userService.CreateUserAsync(createUserDto)
let accessToken = generateAccessToken newUser
let refreshToken = generateRefreshToken()
let authResult = {
AccessToken = accessToken
RefreshToken = refreshToken
ExpiresIn = jwtExpiryMinutes * 60
User = newUser
}
return Ok authResult
}
member _.RefreshTokenAsync(refreshToken: string) =
task {
// In production, validate refresh token against database
// For demo, just generate new tokens
if not (String.IsNullOrWhiteSpace(refreshToken)) then
// Get user from stored refresh token
// For demo, create a dummy user
let dummyUser = {
Id = Guid.NewGuid()
Email = "user@example.com"
FirstName = "User"
LastName = "Example"
IsActive = true
CreatedAt = DateTime.UtcNow
UpdatedAt = DateTime.UtcNow
}
let accessToken = generateAccessToken dummyUser
let newRefreshToken = generateRefreshToken()
let authResult = {
AccessToken = accessToken
RefreshToken = newRefreshToken
ExpiresIn = jwtExpiryMinutes * 60
User = dummyUser
}
return Ok authResult
else
return Error "Invalid refresh token"
}
member _.ValidateTokenAsync(token: string) =
task {
try
let key = SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret))
let validationParameters = TokenValidationParameters(
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = true,
ValidIssuer = jwtIssuer,
ValidateAudience = true,
ValidAudience = jwtAudience,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
)
let handler = JwtSecurityTokenHandler()
let principal = handler.ValidateToken(token, validationParameters, ref null)
let userIdClaim = principal.FindFirst(ClaimTypes.NameIdentifier)
let emailClaim = principal.FindFirst(ClaimTypes.Email)
let firstNameClaim = principal.FindFirst(ClaimTypes.GivenName)
let lastNameClaim = principal.FindFirst(ClaimTypes.Surname)
if userIdClaim <> null && emailClaim <> null && firstNameClaim <> null && lastNameClaim <> null then
let jwtClaims = {
UserId = Guid.Parse(userIdClaim.Value)
Email = emailClaim.Value
FirstName = firstNameClaim.Value
LastName = lastNameClaim.Value
}
return Some jwtClaims
else
return None
with
| _ -> return None
}`,
'FSharpGiraffeApp.fsproj': `<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Models/User.fs" />
<Compile Include="Models/Todo.fs" />
<Compile Include="Models/Auth.fs" />
<Compile Include="Services/UserService.fs" />
<Compile Include="Services/TodoService.fs" />
<Compile Include="Services/AuthService.fs" />
<Compile Include="Handlers/UserHandlers.fs" />
<Compile Include="Handlers/TodoHandlers.fs" />
<Compile Include="Handlers/AuthHandlers.fs" />
<Compile Include="Routes.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Giraffe.Serialization.Json" Version="6.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.30.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.16" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
</ItemGroup>
<ItemGroup>
<Content Include="wwwroot/**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>`,
'appsettings.json': `{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=FSharpGiraffeApp;Trusted_Connection=true;MultipleActiveResultSets=true"
},
"JwtSettings": {
"Secret": "your-super-secret-jwt-key-here-should-be-much-longer-and-more-secure",
"Issuer": "FSharpGiraffeApp",
"Audience": "FSharpGiraffeApp",
"ExpiryMinutes": 60
}
}`,
'appsettings.Development.json': `{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=FSharpGiraffeApp_Dev;Trusted_Connection=true;MultipleActiveResultSets=true"
}
}`,
'wwwroot/index.html': `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>F# Giraffe API</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: 0 auto; }
.endpoint { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.method { font-weight: bold; color: #007acc; }
code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; }
</style>
</head>
<body>
<div class="container">
<h1>🦒 F# Giraffe Web API</h1>
<p>A functional web framework built on ASP.NET Core</p>
<h2>Available Endpoints</h2>
<div class="endpoint">
<div class="method">GET</div>
<div><code>/health</code> - Health check</div>
</div>
<div class="endpoint">
<div class="method">POST</div>
<div><code>/auth/login</code> - User login</div>
</div>
<div class="endpoint">
<div class="method">POST</div>
<div><code>/auth/register</code> - User registration</div>
</div>
<div class="endpoint">
<div class="method">GET</div>
<div><code>/api/users</code> - Get all users</div>
</div>
<div class="endpoint">
<div class="method">GET</div>
<div><code>/api/todos</code> - Get all todos</div>
</div>
<h2>Features</h2>
<ul>
<li>Functional programming with F#</li>
<li>Type-safe HTTP handlers</li>
<li>Composable middleware</li>
<li>JWT authentication</li>
<li>Model validation</li>
<li>Error handling</li>
</ul>
</div>
</body>
</html>`,
'.gitignore': `bin/
obj/
.vs/
.vscode/
*.user
*.suo
*.cache
*.log
.DS_Store
Thumbs.db`,
'README.md': `# F# Giraffe Web Framework
A functional web framework built on ASP.NET Core with F# and Giraffe.
## Features
- **Functional Programming**: Leverage F#'s functional programming paradigms
- **Type Safety**: Compile-time guarantees for web applications
- **Composable**: Build applications from small, composable functions
- **ASP.NET Core**: Built on the robust ASP.NET Core platform
- **JSON Support**: Built-in JSON serialization and deserialization
- **Authentication**: JWT-based authentication system
- **Validation**: Type-safe model validation
- **Testing**: Unit testing with Expecto
## Getting Started
### Prerequisites
- .NET 6.0 SDK or later
- F# 6.0 or later
### Installation
1. Clone the repository
2. Restore packages:
\`\`\`bash
dotnet restore
\`\`\`
3. Run the application:
\`\`\`bash
dotnet run
\`\`\`
The API will be available at \`https://localhost:5001\`
## Project Structure
\`\`\`
├── Models/ # Data models and DTOs
├── Services/ # Business logic services
├── Handlers/ # HTTP request handlers
├── Routes.fs # Route definitions
├── Program.fs # Application entry point
└── wwwroot/ # Static files
\`\`\`
## API Endpoints
### Authentication
- \`POST /auth/login\` - User login
- \`POST /auth/register\` - User registration
- \`POST /auth/refresh\` - Refresh token
- \`GET /auth/profile\` - Get user profile
### Users
- \`GET /api/users\` - Get all users
- \`GET /api/users/{id}\` - Get user by ID
- \`POST /api/users\` - Create user
- \`PUT /api/users/{id}\` - Update user
- \`DELETE /api/users/{id}\` - Delete user
### Todos
- \`GET /api/todos\` - Get all todos
- \`GET /api/todos/{id}\` - Get todo by ID
- \`POST /api/todos\` - Create todo
- \`PUT /api/todos/{id}\` - Update todo
- \`DELETE /api/todos/{id}\` - Delete todo
- \`POST /api/todos/{id}/toggle\` - Toggle todo completion
## Development
### Running Tests
\`\`\`bash
dotnet test
\`\`\`
### Building for Production
\`\`\`bash
dotnet publish -c Release
\`\`\`
## F# and Giraffe Concepts
### HTTP Handlers
Giraffe uses composable HTTP handlers:
\`\`\`fsharp
let getUsers: HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let! users = getUsersFromDatabase()
return! json users next ctx
}
\`\`\`
### Route Composition
Routes are composed using the \`choose\` combinator:
\`\`\`fsharp
let webApp =
choose [
route "/health" >=> text "Healthy"
subRoute "/api" apiRoutes
RequestErrors.NOT_FOUND "Page not found"
]
\`\`\`
### Model Validation
F# provides excellent validation through pattern matching and Result types:
\`\`\`fsharp
let validateUser (user: CreateUserDto): Result<CreateUserDto, string list> =
// Validation logic here
if isValid then Ok user
else Error ["Validation error"]
\`\`\`
## Resources
- [Giraffe Documentation](https://github.com/giraffe-fsharp/Giraffe)
- [F# Documentation](https://docs.microsoft.com/en-us/dotnet/fsharp/)
- [ASP.NET Core Documentation](https://docs.microsoft.com/en-us/aspnet/core/)
## License
MIT License`
}
};