First cut of user admin page (#19)
This commit is contained in:
parent
1e987fdf72
commit
41ae1d8dad
|
@ -327,8 +327,8 @@ type Upload =
|
||||||
module Upload =
|
module Upload =
|
||||||
|
|
||||||
/// An empty upload
|
/// An empty upload
|
||||||
let empty = {
|
let empty =
|
||||||
Id = UploadId.empty
|
{ Id = UploadId.empty
|
||||||
WebLogId = WebLogId.empty
|
WebLogId = WebLogId.empty
|
||||||
Path = Permalink.empty
|
Path = Permalink.empty
|
||||||
UpdatedOn = DateTime.MinValue
|
UpdatedOn = DateTime.MinValue
|
||||||
|
|
|
@ -195,8 +195,8 @@ type Episode =
|
||||||
module Episode =
|
module Episode =
|
||||||
|
|
||||||
/// An empty episode
|
/// An empty episode
|
||||||
let empty = {
|
let empty =
|
||||||
Media = ""
|
{ Media = ""
|
||||||
Length = 0L
|
Length = 0L
|
||||||
Duration = None
|
Duration = None
|
||||||
MediaType = None
|
MediaType = None
|
||||||
|
|
|
@ -207,6 +207,51 @@ type DisplayUpload =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// View model to display a user's information
|
||||||
|
[<NoComparison; NoEquality>]
|
||||||
|
type DisplayUser =
|
||||||
|
{ /// The ID of the user
|
||||||
|
Id : string
|
||||||
|
|
||||||
|
/// The user name (e-mail address)
|
||||||
|
Email : string
|
||||||
|
|
||||||
|
/// The user's first name
|
||||||
|
FirstName : string
|
||||||
|
|
||||||
|
/// The user's last name
|
||||||
|
LastName : string
|
||||||
|
|
||||||
|
/// The user's preferred name
|
||||||
|
PreferredName : string
|
||||||
|
|
||||||
|
/// The URL of the user's personal site
|
||||||
|
Url : string
|
||||||
|
|
||||||
|
/// The user's access level
|
||||||
|
AccessLevel : string
|
||||||
|
|
||||||
|
/// When the user was created
|
||||||
|
CreatedOn : DateTime
|
||||||
|
|
||||||
|
/// When the user last logged on
|
||||||
|
LastSeenOn : Nullable<DateTime>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a displayed user from a web log user
|
||||||
|
static member fromUser webLog (user : WebLogUser) =
|
||||||
|
{ Id = WebLogUserId.toString user.Id
|
||||||
|
Email = user.Email
|
||||||
|
FirstName = user.FirstName
|
||||||
|
LastName = user.LastName
|
||||||
|
PreferredName = user.PreferredName
|
||||||
|
Url = defaultArg user.Url ""
|
||||||
|
AccessLevel = AccessLevel.toString user.AccessLevel
|
||||||
|
CreatedOn = WebLog.localTime webLog user.CreatedOn
|
||||||
|
LastSeenOn = user.LastSeenOn |> Option.map (WebLog.localTime webLog) |> Option.toNullable
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// View model for editing categories
|
/// View model for editing categories
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type EditCategoryModel =
|
type EditCategoryModel =
|
||||||
|
|
|
@ -228,10 +228,11 @@ let register () =
|
||||||
typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
|
typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
|
||||||
// View models
|
// View models
|
||||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
||||||
typeof<DisplayRevision>; typeof<DisplayUpload>; typeof<EditCategoryModel>; typeof<EditCustomFeedModel>
|
typeof<DisplayRevision>; typeof<DisplayUpload>; typeof<DisplayUser>; typeof<EditCategoryModel>
|
||||||
typeof<EditMyInfoModel>; typeof<EditPageModel>; typeof<EditPostModel>; typeof<EditRssModel>
|
typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>; typeof<EditPageModel>; typeof<EditPostModel>
|
||||||
typeof<EditTagMapModel>; typeof<LogOnModel>; typeof<ManagePermalinksModel>; typeof<ManageRevisionsModel>
|
typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<LogOnModel>; typeof<ManagePermalinksModel>
|
||||||
typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>; typeof<UserMessage>
|
typeof<ManageRevisionsModel>; typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>
|
||||||
|
typeof<UserMessage>
|
||||||
// Framework types
|
// Framework types
|
||||||
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
|
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
|
||||||
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>
|
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>
|
||||||
|
|
|
@ -149,6 +149,7 @@ let router : HttpHandler = choose [
|
||||||
route "/new" >=> Upload.showNew
|
route "/new" >=> Upload.showNew
|
||||||
])
|
])
|
||||||
subRoute "/user" (choose [
|
subRoute "/user" (choose [
|
||||||
|
route "s" >=> User.all
|
||||||
route "/my-info" >=> User.myInfo
|
route "/my-info" >=> User.myInfo
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,6 +5,8 @@ open System
|
||||||
open System.Security.Cryptography
|
open System.Security.Cryptography
|
||||||
open System.Text
|
open System.Text
|
||||||
|
|
||||||
|
// ~~ LOG ON / LOG OFF ~~
|
||||||
|
|
||||||
/// Hash a password for a given user
|
/// Hash a password for a given user
|
||||||
let hashedPassword (plainText : string) (email : string) (salt : Guid) =
|
let hashedPassword (plainText : string) (email : string) (salt : Guid) =
|
||||||
let allSalt = Array.concat [ salt.ToByteArray (); Encoding.UTF8.GetBytes email ]
|
let allSalt = Array.concat [ salt.ToByteArray (); Encoding.UTF8.GetBytes email ]
|
||||||
|
@ -69,6 +71,24 @@ let logOff : HttpHandler = fun next ctx -> task {
|
||||||
return! redirectToGet "" next ctx
|
return! redirectToGet "" next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ~~ ADMINISTRATION ~~
|
||||||
|
|
||||||
|
// GET /admin/users
|
||||||
|
let all : HttpHandler = fun next ctx -> task {
|
||||||
|
let data = ctx.Data
|
||||||
|
let! tmpl = TemplateCache.get "admin" "user-list-body" data
|
||||||
|
let! users = data.WebLogUser.FindByWebLog ctx.WebLog.Id
|
||||||
|
let hash = Hash.FromAnonymousObject {|
|
||||||
|
page_title = "User Administration"
|
||||||
|
csrf = ctx.CsrfTokenSet
|
||||||
|
web_log = ctx.WebLog
|
||||||
|
users = users |> List.map (DisplayUser.fromUser ctx.WebLog) |> Array.ofList
|
||||||
|
|}
|
||||||
|
return!
|
||||||
|
addToHash "user_list" (tmpl.Render hash) hash
|
||||||
|
|> adminView "user-list" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
/// Display the user "my info" page, with information possibly filled in
|
/// Display the user "my info" page, with information possibly filled in
|
||||||
let private showMyInfo (user : WebLogUser) (hash : Hash) : HttpHandler = fun next ctx ->
|
let private showMyInfo (user : WebLogUser) (hash : Hash) : HttpHandler = fun next ctx ->
|
||||||
addToHash "page_title" "Edit Your Information" hash
|
addToHash "page_title" "Edit Your Information" hash
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_web_log_admin %}
|
{% if is_web_log_admin %}
|
||||||
{{ "admin/categories" | nav_link: "Categories" }}
|
{{ "admin/categories" | nav_link: "Categories" }}
|
||||||
|
{{ "admin/users" | nav_link: "Users" }}
|
||||||
{{ "admin/settings" | nav_link: "Settings" }}
|
{{ "admin/settings" | nav_link: "Settings" }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
56
src/admin-theme/user-list-body.liquid
Normal file
56
src/admin-theme/user-list-body.liquid
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<form method="post" id="userList" class="container" hx-target="this" hx-swap="outerHTML show:window:top">
|
||||||
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
|
<div class="row mwl-table-detail" id="user_new"></div>
|
||||||
|
{%- assign user_col = "col-12 col-md-4 col-xl-3" -%}
|
||||||
|
{%- assign email_col = "col-12 col-md-4 col-xl-4" -%}
|
||||||
|
{%- assign cre8_col = "d-none d-xl-block col-xl-2" -%}
|
||||||
|
{%- assign last_col = "col-12 col-md-4 col-xl-3" -%}
|
||||||
|
{%- assign badge = "ms-2 badge bg" -%}
|
||||||
|
{% for user in users -%}
|
||||||
|
<div class="row mwl-table-detail" id="user_{{ user.id }}">
|
||||||
|
<div class="{{ user_col }} no-wrap">
|
||||||
|
{{ user.preferred_name }}
|
||||||
|
{%- if user.access_level == "Administrator" %}
|
||||||
|
<span class="{{ badge }}-success">ADMINISTRATOR</span>
|
||||||
|
{%- elsif user.access_level == "WebLogAdmin" %}
|
||||||
|
<span class="{{ badge }}-primary">WEB LOG ADMIN</span>
|
||||||
|
{%- elsif user.access_level == "Editor" %}
|
||||||
|
<span class="{{ badge }}-secondary">EDITOR</span>
|
||||||
|
{%- elsif user.access_level == "Author" %}
|
||||||
|
<span class="{{ badge }}-dark">AUTHOR</span>
|
||||||
|
{%- endif %}<br>
|
||||||
|
<small>
|
||||||
|
{%- assign user_url_base = "admin/user/" | append: user.id -%}
|
||||||
|
<a href="{{ user_url_base | append: "/edit" | relative_link }}" hx-target="#user_{{ user.id }}"
|
||||||
|
hx-swap="innerHTML show:#user_{{ user.id }}:top">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<span class="text-muted"> • </span>
|
||||||
|
{%- assign user_del_link = user_url_base | append: "/delete" | relative_link -%}
|
||||||
|
<a href="{{ user_del_link }}" hx-post="{{ user_del_link }}" class="text-danger"
|
||||||
|
hx-confirm="Are you sure you want to delete the category “{{ cat.name }}”? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="{{ email_col }}">
|
||||||
|
{{ user.first_name }} {{ user.last_name }}<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ user.email }}
|
||||||
|
{%- unless user.url == "" %}<br>{{ user.url }}{% endunless %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="{{ cre8_col }}">
|
||||||
|
{{ user.created_on | date: "MMMM d, yyyy" }}
|
||||||
|
</div>
|
||||||
|
<div class="{{ last_col }}">
|
||||||
|
{% if user.last_seen_on %}
|
||||||
|
{{ user.last_seen_on | date: "MMMM d, yyyy" }} at
|
||||||
|
{{ user.last_seen_on | date: "h:mmtt" | downcase }}
|
||||||
|
{% else %}
|
||||||
|
--
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{%- endfor %}
|
||||||
|
</form>
|
20
src/admin-theme/user-list.liquid
Normal file
20
src/admin-theme/user-list.liquid
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<h2 class="my-3">{{ page_title }}</h2>
|
||||||
|
<article>
|
||||||
|
<a href="{{ "admin/user/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
||||||
|
hx-target="#user_new">
|
||||||
|
Add a New User
|
||||||
|
</a>
|
||||||
|
<div class="container">
|
||||||
|
{%- assign user_col = "col-12 col-md-4 col-xl-3" -%}
|
||||||
|
{%- assign email_col = "col-12 col-md-4 col-xl-4" -%}
|
||||||
|
{%- assign cre8_col = "d-none d-xl-block col-xl-2" -%}
|
||||||
|
{%- assign last_col = "col-12 col-md-4 col-xl-3" -%}
|
||||||
|
<div class="row mwl-table-heading">
|
||||||
|
<div class="{{ user_col }}">User<span class="d-md-none">; Details; Last Log On</span></div>
|
||||||
|
<div class="{{ email_col }} d-none d-md-inline-block">Details</div>
|
||||||
|
<div class="{{ cre8_col }}">Created</div>
|
||||||
|
<div class="{{ last_col }} d-none d-md-block">Last Log On</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ user_list }}
|
||||||
|
</article>
|
|
@ -334,13 +334,10 @@ htmx.on("htmx:afterOnLoad", function (evt) {
|
||||||
})
|
})
|
||||||
|
|
||||||
htmx.on("htmx:responseError", function (evt) {
|
htmx.on("htmx:responseError", function (evt) {
|
||||||
/** @type {XMLHttpRequest} */
|
|
||||||
const xhr = evt.detail.xhr
|
const xhr = evt.detail.xhr
|
||||||
const hdrs = xhr.getAllResponseHeaders()
|
const hdrs = xhr.getAllResponseHeaders()
|
||||||
// Show messages if there were any in the response
|
// Show an error message if there were none in the response
|
||||||
if (hdrs.indexOf("x-message") >= 0) {
|
if (hdrs.indexOf("x-message") < 0) {
|
||||||
Admin.showMessage(evt.detail.xhr.getResponseHeader("x-message"))
|
|
||||||
} else {
|
|
||||||
Admin.showMessage(`danger|||${xhr.status}: ${xhr.statusText}`)
|
Admin.showMessage(`danger|||${xhr.status}: ${xhr.statusText}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue
Block a user