-
Notifications
You must be signed in to change notification settings - Fork 0
/
Handlers.fs
356 lines (329 loc) · 14.6 KB
/
Handlers.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
module Handlers
open System
open System.Security.Claims
open Microsoft.Extensions.Logging
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Authentication
open Microsoft.AspNetCore.Authentication.Cookies
open FSharp.Control.Tasks.ContextInsensitive
open Giraffe
open Data
open System.Text.RegularExpressions
type HttpContext with
member __.IsAuthor = __.User.Identity.IsAuthenticated
let accessDenied : HttpHandler =
setStatusCode 401 >=> text "Access Denied"
let pageNotFound : HttpHandler =
setStatusCode 404 >=> text "Page Not Found"
let badRequest : HttpHandler =
setStatusCode 400 >=> text "Bad Request"
let error (ex : Exception) (logger : ILogger) =
logger.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.")
clearResponse >=> setStatusCode 500 >=> text ex.Message
let latest page =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let data = ctx.GetService<BlogData> ()
// NOTE: disable the following line if you dont need it. Works best for sqlite
data.Database.EnsureCreated () |> ignore
let skipCount = page * 5
let posts = query {
for post in data.FullPosts () do
sortByDescending post.Date
skip skipCount
take 5
select post
}
return! htmlView (Views.latest ctx.IsAuthor posts page) next ctx
}
let single key commentError =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let data = ctx.GetService<BlogData> ()
let post = query {
for post in data.FullPosts () do
where (post.Key = key)
select post
}
return!
match Seq.tryHead post with
| Some p ->
let isAuthorsPost = ctx.IsAuthor && p.Author.Username = ctx.User.Identity.Name
let view = Views.single ctx.IsAuthor isAuthorsPost p commentError
htmlView view next ctx
| None -> pageNotFound next ctx
}
let login : HttpHandler = htmlView (Views.login false false)
let private monthNames =
[""; "january"; "february"; "march"; "april";
"may"; "june"; "july"; "august"; "september";
"october"; "november"; "december"]
let archives =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let data = ctx.GetService<BlogData> ()
let allByDate = query {
for post in data.FullPosts () do
sortBy post.Date
select (post.Date.Month, post.Date.Year)
}
let years =
allByDate
|> Seq.groupBy (fun (_,year) -> year)
|> Seq.map (fun (year,posts) ->
year, posts
|> Seq.groupBy (fun (month,_) -> month)
|> Seq.map (fun (month,posts) -> monthNames.[month],Seq.length posts))
return! htmlView (Views.archives ctx.IsAuthor years) next ctx
}
let month (monthName, year) =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let monthNumber = List.tryFindIndex (fun o -> o = monthName) monthNames
return!
match monthNumber with
| None -> pageNotFound next ctx
| Some m ->
let data = ctx.GetService<BlogData> ()
let posts = query {
for post in data.FullPosts () do
sortBy post.Date
where (post.Date.Year = year && post.Date.Month = m)
select post
}
let monthUrl targetMonth targetYear = sprintf "/month/%s/%i" targetMonth targetYear
let prevMonth = if m = 1 then monthUrl "december" (year - 1) else monthUrl monthNames.[m - 1] year
let nextMonth = if m = 12 then monthUrl "january" (year + 1) else monthUrl monthNames.[m + 1] year
htmlView (Views.month ctx.IsAuthor posts prevMonth nextMonth) next ctx
}
let private trimToSearchTerm (term:string) content =
let stripped = Regex.Replace(content, "<[^>]*>", "")
let index = stripped.ToLower().IndexOf(term.ToLower())
match index with
| -1 -> ""
| _ ->
let start,stop = max (index - 20) 0, min (index + term.Length + 20) stripped.Length
let section = stripped.Substring(start, stop - start)
"..." + section + "..."
let search =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
return!
match ctx.TryGetQueryStringValue "searchTerm" with
| None -> htmlView (Views.search ctx.IsAuthor None) next ctx
| Some term ->
let data = ctx.GetService<BlogData> ()
let posts = query {
for post in data.FullPosts () do
where (post.Title.Contains(term) || post.Content.Contains(term))
sortByDescending post.Date
take 50
select post
}
let results =
posts
|> Seq.map (fun p -> { p with Content = trimToSearchTerm term p.Content })
|> Seq.toList
htmlView (results |> Some |> Views.search ctx.IsAuthor) next ctx
}
let about : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
return! htmlView (Views.about ctx.IsAuthor) next ctx
}
[<CLIMutable>]
type LoginForm = {
username: string
password: string
}
let setUserAndRedirect (next : HttpFunc) (ctx : HttpContext) (author: Author) =
task {
let issuer = sprintf "%s://%s" ctx.Request.Scheme ctx.Request.Host.Value
let claims =
[
Claim(ClaimTypes.Name, author.Username, ClaimValueTypes.String, issuer)
]
let authScheme = CookieAuthenticationDefaults.AuthenticationScheme
let identity = ClaimsIdentity(claims, authScheme)
let user = ClaimsPrincipal(identity)
do! ctx.SignInAsync(authScheme, user)
return! redirectTo false "/" next ctx
}
let tryLogin =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! form = ctx.TryBindFormAsync<LoginForm> ()
let badLogin () = htmlView (Views.login ctx.IsAuthor true) next ctx
return!
match form with
| Error _ -> badLogin ()
| Ok form ->
let data = ctx.GetService<BlogData> ()
let authors = query {
for user in data.Authors do
where (user.Username = form.username)
select user
}
match Seq.tryHead authors with
| None -> badLogin ()
| Some a ->
if a.Validate form.password then
setUserAndRedirect next ctx a
else badLogin ()
}
[<CLIMutable>]
type NewComment = {
author: string
content: string
}
let createComment key =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! newComment = ctx.TryBindFormAsync<NewComment> ()
return!
match newComment with
| Error _ -> badRequest next ctx
| Ok c ->
let data = ctx.GetService<BlogData> ()
let post = query {
for post in data.FullPosts () do
where (post.Key = key)
select post
}
match Seq.tryHead post with
| None -> redirectTo false "/" next ctx
| Some p ->
if p.Comments.Count >= 20 then
badRequest next ctx
else if c.author = "" || c.content = "" then
single key Views.RequiredCommentFields next ctx
else if ["http:";"https:";"www."] |> List.exists (fun tk -> c.content.Contains(tk)) then
single key Views.InvalidCommentContent next ctx
else
data.Comments.Add
({
Author = c.author
Date = DateTime.Now
Content = c.content
Post_Key = key
Post = Unchecked.defaultof<Post>
Id = 0}) |> ignore
data.SaveChanges() |> ignore
redirectTo false (sprintf "/post/%s#comments" key) next ctx
}
let savedContentKey = "savedContent"
let editor key =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
return!
match key with
| None ->
let saved = ctx.Session.GetString(savedContentKey)
let model : Views.PostViewModel = {
title = ""
content = if saved = null then "" else saved }
let view = Views.editor model Views.AutoSaveEnabled Views.NoEditorErrors
htmlView view next ctx
| Some k ->
let data = ctx.GetService<BlogData> ()
let post = query {
for post in data.FullPosts () do
where (post.Key = k && post.Author.Username = ctx.User.Identity.Name)
select post
}
match Seq.tryHead post with
| None -> redirectTo false "/" next ctx
| Some p ->
let model : Views.PostViewModel = { title = p.Title; content = p.Content }
let view = Views.editor model Views.AutoSaveDisabled Views.NoEditorErrors
htmlView view next ctx
}
let getKey (title: string) =
let clean = title.ToLower().Replace (" ", "-")
Regex.Replace (clean, "[^A-Za-z0-9 -]+", "")
let createPost =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! newPost = ctx.TryBindFormAsync<Views.PostViewModel> ()
return!
match newPost with
| Error _ ->
badRequest next ctx
| Ok form when form.title = "" || form.content = "" ->
let view = Views.editor form Views.AutoSaveEnabled Views.RequiredEditorFields
htmlView view next ctx
| Ok form ->
let data = ctx.GetService<BlogData> ()
let key = getKey form.title
let existing = query {
for post in data.Posts do
where (post.Key = key)
select post
}
match Seq.tryHead existing with
| Some _ ->
let view = Views.editor form Views.AutoSaveEnabled Views.ExistingPostKey
htmlView view next ctx
| None ->
let postEntity = {
Author_Username = ctx.User.Identity.Name
Author = Unchecked.defaultof<Author>
Key = key
Title = form.title
Content = form.content
Date = DateTime.Now
Comments = new System.Collections.Generic.List<Comment>()
}
data.Posts.Add(postEntity) |> ignore
data.SaveChanges() |> ignore
ctx.Session.Remove(savedContentKey)
redirectTo false (sprintf "/post/%s" key) next ctx
}
let editPost key =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! newPost = ctx.TryBindFormAsync<Views.PostViewModel> ()
return!
match newPost with
| Error _ -> badRequest next ctx
| Ok form when form.title = "" || form.content = "" ->
let view = Views.editor form Views.AutoSaveDisabled Views.RequiredEditorFields
htmlView view next ctx
| Ok form ->
let data = ctx.GetService<BlogData> ()
let post = query {
for post in data.FullPosts () do
where (post.Key = key && post.Author.Username = ctx.User.Identity.Name)
select post
}
match Seq.tryHead post with
| None -> badRequest next ctx
| Some p ->
let key = getKey form.title
let existing = query {
for post in data.Posts do
where (post.Key = key && post.Key <> p.Key)
select post
}
match Seq.tryHead existing with
| Some _ ->
let view = Views.editor form Views.AutoSaveDisabled Views.ExistingPostKey
htmlView view next ctx
| None ->
let updated =
{ p with
Key = key
Title = form.title
Content = form.content }
data.Entry(p).CurrentValues.SetValues(updated) |> ignore
data.SaveChanges() |> ignore
ctx.Session.Remove(savedContentKey)
redirectTo false (sprintf "/post/%s" key) next ctx
}
let saveWork =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! body = ctx.BindJsonAsync<string>()
ctx.Request.HttpContext.Session.SetString(savedContentKey, body)
return! Successful.OK "" next ctx
}