Squashed 'applications/users/' content from commit 1269ef3
git-subtree-dir: applications/users git-subtree-split: 1269ef321098783e8699978b9c4dea9592d8f3e3
This commit is contained in:
commit
63990d006a
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.lua
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2018 Paul Liverman III
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
51
ReadMe.md
Normal file
51
ReadMe.md
Normal file
@ -0,0 +1,51 @@
|
||||
## Installation
|
||||
|
||||
(Note: These instructions need to be rewritten for use with the new service
|
||||
locator and utility modules I've made recently.)
|
||||
|
||||
1. From application root: `git submodule add https://github.com/lazuscripts/users users`
|
||||
|
||||
2. Inside your application: `@include "users/users"`
|
||||
|
||||
3. In migrations, do the following:
|
||||
```moonscript
|
||||
import create_table, types from require "lapis.db.schema"
|
||||
create_table "users", {
|
||||
{"id", types.serial primary_key: true}
|
||||
{"name", types.varchar unique: true}
|
||||
{"digest", types.text}
|
||||
{"admin", types.boolean default: false}
|
||||
}
|
||||
```
|
||||
|
||||
4. `bcrypt` Lua Rock must be installed!
|
||||
|
||||
5. If `@session.redirect` is defined when going to the login route, a user will be
|
||||
redirected there after logging in.
|
||||
|
||||
Note: Assumes you have a route named "index" to redirect to when things go
|
||||
wrong.
|
||||
|
||||
**WARNING**: Now expects you to display informational messages stored in
|
||||
`@session.info`.
|
||||
|
||||
## Config
|
||||
|
||||
`digest_rounds 9` must be set. Use a higher or lower number depending on how
|
||||
long it takes to calculate a digest or how paranoid you want to be. See this bit
|
||||
about tuning: https://github.com/mikejsavage/lua-bcrypt#tuning
|
||||
|
||||
## Access
|
||||
|
||||
To get the Users model for use outside of this sub-application:<br>
|
||||
`Users = require "users.models.Users"`
|
||||
|
||||
Named routes:
|
||||
|
||||
- user_new
|
||||
- user_me
|
||||
- user_admin
|
||||
- user_edit
|
||||
- user_login
|
||||
- user_logout
|
||||
- user_list
|
6
locator_config.moon
Normal file
6
locator_config.moon
Normal file
@ -0,0 +1,6 @@
|
||||
import Sessions from require "models"
|
||||
|
||||
{
|
||||
before_filter: =>
|
||||
@user = Sessions\get(@session)
|
||||
}
|
66
migrations.moon
Normal file
66
migrations.moon
Normal file
@ -0,0 +1,66 @@
|
||||
db = require "lapis.db"
|
||||
import create_table, types, add_column, rename_column, create_index, drop_index from require "lapis.db.schema"
|
||||
|
||||
{
|
||||
[1]: =>
|
||||
create_table "users", {
|
||||
{"id", types.serial primary_key: true}
|
||||
{"name", types.varchar unique: true}
|
||||
{"email", types.text unique: true}
|
||||
{"digest", types.text}
|
||||
{"admin", types.boolean default: false}
|
||||
|
||||
{"created_at", types.time}
|
||||
{"updated_at", types.time}
|
||||
}
|
||||
create_table "sessions", {
|
||||
{"user_id", types.foreign_key}
|
||||
|
||||
{"created_at", types.time}
|
||||
{"updated_at", types.time}
|
||||
}
|
||||
|
||||
[1518430372]: =>
|
||||
add_column "sessions", "id", types.serial primary_key: true
|
||||
rename_column "sessions", "created_at", "opened_at"
|
||||
rename_column "sessions", "updated_at", "closed_at"
|
||||
|
||||
create_index "users", "id", unique: true
|
||||
create_index "users", "name", unique: true
|
||||
create_index "users", "email", unique: true
|
||||
|
||||
create_index "sessions", "id", unique: true
|
||||
|
||||
[1518968812]: =>
|
||||
import autoload from require "locator"
|
||||
import settings from autoload "utility"
|
||||
|
||||
settings["users.allow-sign-up"] = true
|
||||
settings["users.allow-name-change"] = true
|
||||
settings["users.admin-only-mode"] = false
|
||||
settings["users.require-email"] = true
|
||||
settings["users.require-unique-email"] = true
|
||||
settings["users.allow-email-change"] = true
|
||||
settings["users.session-timeout"] = 60 * 60 * 24 -- default is one day
|
||||
|
||||
settings["users.minimum-password-length"] = 12
|
||||
settings["users.maximum-character-repetition"] = 6
|
||||
settings["users.bcrypt-digest-rounds"] = 12
|
||||
-- settings["users.password-check-fn"] = nil -- should return true if passes, falsy and error message if fails
|
||||
settings.save!
|
||||
|
||||
drop_index "users", "email" -- replacing because it was a unique index
|
||||
db.query "ALTER TABLE users DROP CONSTRAINT users_email_key"
|
||||
create_index "users", "email"
|
||||
|
||||
[1519416945]: =>
|
||||
import autoload from require "locator"
|
||||
import settings from autoload "utility"
|
||||
|
||||
settings["users.require-recaptcha"] = false -- protect against bots for sign-up (default off because it requires set-up)
|
||||
-- settings["users.recaptcha-sitekey"] = nil -- provided by admin panel
|
||||
-- settings["users.recaptcha-secret"] = nil -- provided by admin panel
|
||||
settings.save!
|
||||
|
||||
-- NOTE may need to run a migration to allow null emails ?
|
||||
}
|
32
models/Sessions.moon
Normal file
32
models/Sessions.moon
Normal file
@ -0,0 +1,32 @@
|
||||
import Model from require "lapis.db.model"
|
||||
import Users from require "models"
|
||||
|
||||
import locate, autoload from require "locator"
|
||||
import to_seconds, for_db from locate "datetime"
|
||||
import settings from autoload "utility"
|
||||
import time from os
|
||||
|
||||
class Sessions extends Model
|
||||
@create: (values, opts) =>
|
||||
now = time!
|
||||
values.opened_at = for_db now
|
||||
values.closed_at = for_db now + settings["users.session-timeout"]
|
||||
return super values, opts
|
||||
|
||||
get: (cookie) =>
|
||||
if cookie.session_id
|
||||
if session = @find id: cookie.session_id
|
||||
now = time!
|
||||
if now - to_seconds(session.closed_at) <= 0
|
||||
if user = Users\find id: session.user_id
|
||||
session\update closed_at: for_db now + settings["users.session-timeout"]
|
||||
return user
|
||||
else
|
||||
session\update closed_at: for_db now
|
||||
cookie.session_id = nil
|
||||
|
||||
close: (cookie) =>
|
||||
if cookie.session_id
|
||||
if session = @find id: cookie.session_id
|
||||
session\update closed_at: for_db time!
|
||||
cookie.session_id = nil
|
35
models/Users.moon
Normal file
35
models/Users.moon
Normal file
@ -0,0 +1,35 @@
|
||||
import Model from require "lapis.db.model"
|
||||
|
||||
import autoload from require "locator"
|
||||
import settings from autoload "utility"
|
||||
|
||||
class Users extends Model
|
||||
@timestamp: true
|
||||
|
||||
@constraints: {
|
||||
name: (value) =>
|
||||
if not value
|
||||
return "User names must exist."
|
||||
|
||||
-- TODO make this customizable ?
|
||||
if value\find "%W"
|
||||
return "User names can only contain alphanumeric characters."
|
||||
|
||||
if Users\find name: value
|
||||
return "User names must be unique."
|
||||
|
||||
-- TODO make this extendable? / make this show up BEFORE it gets to erroring at the model
|
||||
lower = value\lower!
|
||||
if (lower == "admin") or (lower == "administrator") or (lower == "new") or (lower == "edit") or (lower == "create") or (lower == "login") or (lower == "logout") or (lower == "me")
|
||||
return "User names must not be 'admin', 'administrator', 'new', 'edit', 'create', 'login', 'logout', or 'me'."
|
||||
|
||||
email: (value) =>
|
||||
if settings["users.require-email"] and (not value)
|
||||
return "Email addresses must exist."
|
||||
|
||||
-- TODO figure out how to check for valid email address
|
||||
|
||||
if settings["users.require-unique-email"]
|
||||
if Users\find email: value
|
||||
return "Email addresses must be unique."
|
||||
}
|
335
users.moon
Normal file
335
users.moon
Normal file
@ -0,0 +1,335 @@
|
||||
lapis = require "lapis"
|
||||
http = require "lapis.nginx.http"
|
||||
csrf = require "lapis.csrf"
|
||||
config = require("lapis.config").get!
|
||||
|
||||
bcrypt = require "bcrypt"
|
||||
|
||||
import decode from require "cjson"
|
||||
import respond_to, capture_errors, assert_error, yield_error from require "lapis.application"
|
||||
import assert_valid, validate_functions from require "lapis.validate"
|
||||
import trim from require "lapis.util"
|
||||
import locate, autoload from require "locator"
|
||||
import settings from autoload "utility"
|
||||
|
||||
import Users, Sessions from require "models"
|
||||
|
||||
validate_functions.not_equals = (...) ->
|
||||
return not validate_functions.equals(...)
|
||||
validate_functions.unique_user = (input) ->
|
||||
return not Users\find name: input
|
||||
validate_functions.unique_email = (input) ->
|
||||
return not Users\find email: input
|
||||
validate_functions.max_repetitions = (input, max) ->
|
||||
tab = {}
|
||||
for i=1,#input
|
||||
char = input\sub i, i
|
||||
if tab[char]
|
||||
tab[char] += 1
|
||||
else
|
||||
tab[char] = 1
|
||||
for k,v in pairs tab
|
||||
if v >= settings["users.maximum-character-repetition"]
|
||||
return false
|
||||
return true
|
||||
|
||||
class extends lapis.Application
|
||||
@path: "/users"
|
||||
@name: "user_"
|
||||
|
||||
[new: "/new"]: respond_to {
|
||||
before: =>
|
||||
unless settings["users.allow-sign-up"]
|
||||
@session.info = "Sign ups are disabled."
|
||||
return redirect_to: @params.redirect or @url_for "index"
|
||||
|
||||
GET: =>
|
||||
if @user
|
||||
@session.info = "You are logged into an account already."
|
||||
return redirect_to: @params.redirect or @url_for "user_me"
|
||||
|
||||
@csrf_token = csrf.generate_token(@)
|
||||
|
||||
return render: locate "views.user_new"
|
||||
|
||||
POST: capture_errors {
|
||||
on_error: =>
|
||||
@session.info = "The following errors occurred:"
|
||||
for err in *@errors
|
||||
@session.info ..= " #{err}"
|
||||
return redirect_to: @url_for "user_new", nil, redirect: @params.redirect
|
||||
|
||||
=>
|
||||
csrf.assert_token(@)
|
||||
|
||||
if settings["users.require-recaptcha"]
|
||||
body = http.simple "https://www.google.com/recaptcha/api/siteverify", {
|
||||
secret: settings["users.recaptcha-secret"]
|
||||
response: @params["g-recaptcha-response"]
|
||||
}
|
||||
unless decode(body).success
|
||||
yield_error "You failed to complete the reCAPTCHA challenge."
|
||||
|
||||
assert_valid @params, {
|
||||
{"name", exists: true, "You must have a username."}
|
||||
{"name", unique_user: true, "That username is taken."}
|
||||
{"password", exists: true, "You must enter a password."}
|
||||
{"password", min_length: settings["users.minimum-password-length"], "Your password must be at least #{settings["users.minimum-password-length"]} characters long."}
|
||||
{"password", max_repetitions: settings["users.maximum-character-repetition"], "Your password must not have more than #{settings["users.maximum-character-repetition"]} repetitions of the same character."}
|
||||
}
|
||||
|
||||
if settings["users.password-check-fn"]
|
||||
fn = locate settings["users.password-check-fn"]
|
||||
assert_error fn(@params.password)
|
||||
|
||||
if settings["users.require-email"]
|
||||
assert_valid @params, {
|
||||
{"email", exists: true, "You must enter an email address."}
|
||||
}
|
||||
if settings["users.require-unique-email"]
|
||||
assert_valid @params, {
|
||||
{"email", unique_email: true, "That email address is already tied to another account."}
|
||||
}
|
||||
|
||||
digest = bcrypt.digest @params.password, settings["users.bcrypt-digest-rounds"]
|
||||
|
||||
user = assert_error Users\create {
|
||||
name: trim @params.name -- NOTE this might allow an empty username by using spaces to fool validation functions
|
||||
email: trim @params.email
|
||||
digest: digest
|
||||
}
|
||||
|
||||
-- if there are no admins, they become one
|
||||
unless Users\find admin: true
|
||||
user\update admin: true
|
||||
|
||||
if settings["users.admin-only-mode"] and (not user.admin)
|
||||
yield_error "Your account was created. However, this site is in admin-only mode right now, so you are not logged in."
|
||||
|
||||
session = assert_error Sessions\create user_id: user.id
|
||||
@session.session_id = session.id
|
||||
@session.info = "Account created, you are logged in."
|
||||
return redirect_to: @params.redirect or @url_for "user_me"
|
||||
}
|
||||
}
|
||||
|
||||
[me: "/me"]: =>
|
||||
unless @user
|
||||
@session.info = "You are not logged in."
|
||||
return redirect_to: @url_for "user_login", nil, redirect: @url_for "user_me"
|
||||
|
||||
return render: locate "views.user_me"
|
||||
|
||||
[edit: "/edit"]: respond_to {
|
||||
before: =>
|
||||
unless @user
|
||||
@session.info = "You are not logged in."
|
||||
return redirect_to: @url_for "user_login", nil, redirect: @url_for "user_edit"
|
||||
|
||||
GET: =>
|
||||
@csrf_token = csrf.generate_token(@)
|
||||
return render: locate "views.user_edit"
|
||||
|
||||
POST: capture_errors {
|
||||
on_error: =>
|
||||
@session.info = "The following errors occurred:"
|
||||
for err in *@errors
|
||||
@session.info ..= " #{err}"
|
||||
return redirect_to: @url_for "user_edit"
|
||||
|
||||
=>
|
||||
csrf.assert_token(@)
|
||||
|
||||
if @params.name
|
||||
unless settings["users.allow-name-change"]
|
||||
yield_error "You cannot change your username."
|
||||
assert_valid @params, {
|
||||
{"name", exists: true, "You must have a username."}
|
||||
{"name", not_equals: @user.name, "You must enter a different username to change it."}
|
||||
{"name", unique_user: true, "That username is taken."}
|
||||
}
|
||||
assert_error @user\update name: trim @params.name
|
||||
@session.info = "Username updated."
|
||||
return redirect_to: @url_for "user_edit"
|
||||
|
||||
if @params.email
|
||||
unless settings["users.allow-email-change"]
|
||||
yield_error "You cannot change your email address."
|
||||
if settings["users.require-email"]
|
||||
assert_valid @params, {
|
||||
{"email", exists: true, "You must enter an email address."}
|
||||
}
|
||||
if settings["users.require-unique-email"]
|
||||
assert_valid @params, {
|
||||
{"email", unique_email: true, "That email address is already tied to another account."}
|
||||
}
|
||||
assert_error @user\update email: trim @params.email
|
||||
@session.info = "Email address updated."
|
||||
return redirect_to: @url_for "user_edit"
|
||||
|
||||
if @params.password
|
||||
assert_valid @params, {
|
||||
{"password", exists: true, "You must enter a password."}
|
||||
{"password", min_length: settings["users.minimum-password-length"], "Your password must be at least #{settings["users.minimum-password-length"]} characters long."}
|
||||
{"password", max_repetitions: settings["users.maximum-character-repetition"], "Your password must not have more than #{settings["users.maximum-character-repetition"]} repetitions of the same character."}
|
||||
}
|
||||
if settings["users.password-check-fn"]
|
||||
fn = locate settings["users.password-check-fn"]
|
||||
assert_error fn(@params.password)
|
||||
|
||||
unless bcrypt.verify @params.oldpassword, @user.digest
|
||||
yield_error "Incorrect password."
|
||||
|
||||
assert_error @user\update digest: bcrypt.digest @params.password, settings["users.bcrypt-digest-rounds"]
|
||||
@session.info = "Password updated."
|
||||
return redirect_to: @url_for "user_edit"
|
||||
|
||||
if @params.delete
|
||||
Sessions\close(@session)
|
||||
assert_error @user\delete!
|
||||
@session.info = "Account deleted."
|
||||
return redirect_to: @url_for "index"
|
||||
|
||||
@csrf_token = csrf.generate_token(@)
|
||||
return render: locate "views.user_edit"
|
||||
}
|
||||
}
|
||||
|
||||
[admin: "/admin"]: respond_to {
|
||||
before: =>
|
||||
unless @user
|
||||
@session.info = "You are not logged in."
|
||||
return redirect_to: @url_for "user_login", nil, redirect: @url_for "user_admin"
|
||||
|
||||
unless @user.admin
|
||||
@session.info = "You are not an administrator."
|
||||
return redirect_to: @url_for "index"
|
||||
|
||||
GET: =>
|
||||
@csrf_token = csrf.generate_token(@)
|
||||
if @params.id
|
||||
@user_editing = Users\find id: @params.id -- query params option
|
||||
unless @user_editing
|
||||
@user_editing = @user
|
||||
return render: locate "views.user_admin"
|
||||
|
||||
POST: capture_errors {
|
||||
on_error: =>
|
||||
@session.info = "The following errors occurred:"
|
||||
for err in *@errors
|
||||
@session.info ..= " #{err}"
|
||||
return redirect_to: @url_for "user_admin"
|
||||
|
||||
=>
|
||||
csrf.assert_token(@)
|
||||
|
||||
-- figure out who we're editing
|
||||
if @params.change_via_name and @params.change_via_name\len! > 0
|
||||
@user_editing = Users\find name: @params.change_via_name
|
||||
elseif @params.change_via_id
|
||||
@user_editing = Users\find id: @params.change_via_id
|
||||
|
||||
unless @user_editing
|
||||
yield_error "No user selected. You have been automatically selected."
|
||||
|
||||
-- figure out what we're editing and do it
|
||||
if @params.name
|
||||
assert_valid @params, {
|
||||
{"name", exists: true, "They must have a username."}
|
||||
{"name", not_equals: @user_editing.name, "You must enter a different username to change it."}
|
||||
{"name", unique_user: true, "That username is taken."}
|
||||
}
|
||||
assert_error @user_editing\update name: trim @params.name
|
||||
@session.info = "Username updated."
|
||||
return redirect_to: @url_for "user_admin"
|
||||
|
||||
if @params.email
|
||||
if settings["users.require-email"]
|
||||
assert_valid @params, {
|
||||
{"email", exists: true, "They must have an email address."}
|
||||
}
|
||||
if settings["users.require-unique-email"]
|
||||
assert_valid @params, {
|
||||
{"email", unique_email: true, "That email address is already tied to another account."}
|
||||
}
|
||||
assert_error @user_editing\update email: trim @params.email
|
||||
@session.info = "Email address updated."
|
||||
return redirect_to: @url_for "user_admin"
|
||||
|
||||
if @params.password
|
||||
assert_error @user_editing\update digest: bcrypt.digest @params.password, settings["users.bcrypt-digest-rounds"]
|
||||
@session.info = "Password updated. (Warning: Admins editing passwords are not restricted to secure passwords, as these are intended to be TEMPORARY only!)"
|
||||
-- TODO require a password edited by an admin to be changed upon login
|
||||
return redirect_to: @url_for "user_admin"
|
||||
|
||||
if @params.admin ~= nil
|
||||
assert_error @user_editing\update admin: @params.admin
|
||||
if @params.admin
|
||||
@session.info = "#{@user_editing.name} is now an admin!"
|
||||
else
|
||||
@session.info = "#{@user_editing.name} is no longer an admin."
|
||||
return redirect_to: @url_for "user_admin"
|
||||
|
||||
if @params.delete
|
||||
assert_error @user_editing\delete!
|
||||
@session.info = "Account deleted. You are now viewing your own account."
|
||||
@user_editing = @user
|
||||
|
||||
@csrf_token = csrf.generate_token(@)
|
||||
return render: locate "views.user_admin"
|
||||
}
|
||||
}
|
||||
|
||||
[list: "/list"]: =>
|
||||
unless @user
|
||||
@session.info = "You are not logged in."
|
||||
return redirect_to: @url_for "user_login", nil, redirect: @url_for "user_list"
|
||||
|
||||
unless @user.admin
|
||||
@session.info = "You are not an administrator."
|
||||
return redirect_to: @url_for "index"
|
||||
|
||||
@users = Users\select "WHERE true ORDER BY name ASC"
|
||||
return render: locate "views.user_list"
|
||||
|
||||
[login: "/login"]: respond_to {
|
||||
before: =>
|
||||
if @user
|
||||
@session.info = "You are already logged in."
|
||||
return redirect_to: @params.redirect or @url_for "user_me"
|
||||
|
||||
GET: =>
|
||||
@csrf_token = csrf.generate_token(@)
|
||||
return render: locate "views.user_login"
|
||||
|
||||
POST: capture_errors {
|
||||
on_error: =>
|
||||
@session.info = "The following errors occurred:"
|
||||
for err in *@errors
|
||||
@session.info ..= " #{err}"
|
||||
return redirect_to: @url_for "user_login"
|
||||
|
||||
=>
|
||||
csrf.assert_token(@)
|
||||
|
||||
user = assert_error Users\find name: trim @params.name
|
||||
if settings["users.admin-only-mode"] and (not user.admin)
|
||||
yield_error "This site is in admin-only mode. You cannot log in."
|
||||
|
||||
if bcrypt.verify @params.password, user.digest
|
||||
session = assert_error Sessions\create user_id: user.id
|
||||
@session.session_id = session.id
|
||||
@session.info = "Logged in."
|
||||
return redirect_to: @params.redirect or @session.redirect or @url_for "index"
|
||||
|
||||
@session.info = "Invalid username or password."
|
||||
return redirect_to: @url_for "user_login", nil, redirect: @params.redirect or @session.redirect
|
||||
}
|
||||
}
|
||||
|
||||
[logout: "/logout"]: =>
|
||||
@session.id = nil
|
||||
@session.redirect = nil
|
||||
Sessions\close(@session)
|
||||
@session.info = "You have been logged out."
|
||||
return redirect_to: @params.redirect or @url_for "index"
|
85
views/user_admin.moon
Normal file
85
views/user_admin.moon
Normal file
@ -0,0 +1,85 @@
|
||||
import Widget from require "lapis.html"
|
||||
|
||||
class extends Widget
|
||||
content: =>
|
||||
form {
|
||||
action: @url_for "user_admin"
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
}, ->
|
||||
p "Selected User: #{@user_editing.name} (#{@user_editing.id})"
|
||||
element "table", ->
|
||||
tr ->
|
||||
td "Switch (by name)?"
|
||||
td -> input type: "text", name: "change_via_name", placeholder: @user_editing.name
|
||||
tr ->
|
||||
td "Switch (by ID)?"
|
||||
td -> input type: "number", name: "change_via_id", value: @user_editing.id
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
||||
hr!
|
||||
|
||||
form {
|
||||
action: @url_for "user_admin"
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
}, ->
|
||||
text "Change username? "
|
||||
input type: "text", name: "name", value: @user_editing.name, autocomplete: "username"
|
||||
br!
|
||||
input type: "hidden", name: "id", value: @user_editing.id
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
||||
hr!
|
||||
|
||||
form {
|
||||
action: @url_for "user_admin"
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
}, ->
|
||||
text "Change email? "
|
||||
input type: "text", name: "email", value: @user_editing.email
|
||||
br!
|
||||
input type: "hidden", name: "id", value: @user_editing.id
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
||||
hr!
|
||||
|
||||
form {
|
||||
action: @url_for "user_admin"
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
}, ->
|
||||
text "Change password? "
|
||||
input type: "password", name: "password"
|
||||
br!
|
||||
input type: "hidden", name: "id", value: @user_editing.id
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
||||
hr!
|
||||
|
||||
form {
|
||||
action: @url_for "user_admin"
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
onsubmit: "return confirm('Are you sure you want to do this?');"
|
||||
}, ->
|
||||
text "Administrator? "
|
||||
input type: "checkbox", name: "admin", checked: @user_editing.admin
|
||||
br!
|
||||
input type: "hidden", name: "id", value: @user_editing.id
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
||||
|
||||
form {
|
||||
action: @url_for "user_admin"
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
onsubmit: "return confirm('Are you sure you want to do this?');"
|
||||
}, ->
|
||||
text "Delete user? "
|
||||
input type: "checkbox", name: "delete"
|
||||
br!
|
||||
input type: "hidden", name: "id", value: @user_editing.id
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
56
views/user_edit.moon
Normal file
56
views/user_edit.moon
Normal file
@ -0,0 +1,56 @@
|
||||
import Widget from require "lapis.html"
|
||||
|
||||
class extends Widget
|
||||
content: =>
|
||||
form {
|
||||
action: @url_for "user_edit"
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
}, ->
|
||||
text "Change username? "
|
||||
input type: "text", name: "name", value: @user.name, autocomplete: "username"
|
||||
br!
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
||||
hr!
|
||||
|
||||
form {
|
||||
action: @url_for "user_edit"
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
}, ->
|
||||
text "Change email address? "
|
||||
input type: "email", name: "email", value: @user.email
|
||||
br!
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
||||
hr!
|
||||
|
||||
form {
|
||||
action: @url_for "user_edit"
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
}, ->
|
||||
p "Change password?"
|
||||
element "table", ->
|
||||
tr ->
|
||||
td "Old password:"
|
||||
td -> input type: "password", name: "oldpassword"
|
||||
tr ->
|
||||
td "New password:"
|
||||
td -> input type: "password", name: "password"
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
||||
hr!
|
||||
|
||||
form {
|
||||
action: @url_for "user_edit"
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
onsubmit: "return confirm('Are you sure you want to do this?');"
|
||||
}, ->
|
||||
text "Delete user? "
|
||||
input type: "checkbox", name: "delete"
|
||||
br!
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
28
views/user_list.moon
Normal file
28
views/user_list.moon
Normal file
@ -0,0 +1,28 @@
|
||||
import Widget from require "lapis.html"
|
||||
|
||||
class extends Widget
|
||||
content: =>
|
||||
p "#{#@users} users."
|
||||
element "table", ->
|
||||
thead ->
|
||||
tr ->
|
||||
th "ID"
|
||||
th "Admin"
|
||||
th "Username"
|
||||
th "Email Address"
|
||||
tbody ->
|
||||
for user in *@users
|
||||
tr ->
|
||||
td user.id
|
||||
if user.admin
|
||||
td "✔"
|
||||
else
|
||||
td! -- "❌"
|
||||
td user.name
|
||||
td user.email
|
||||
tfoot ->
|
||||
tr ->
|
||||
th "ID"
|
||||
th "Admin"
|
||||
th "Username"
|
||||
th "Email Address"
|
17
views/user_login.moon
Normal file
17
views/user_login.moon
Normal file
@ -0,0 +1,17 @@
|
||||
import Widget from require "lapis.html"
|
||||
|
||||
class extends Widget
|
||||
content: =>
|
||||
form {
|
||||
action: @url_for "user_login", nil, redirect: @params.redirect
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
}, ->
|
||||
text "Username: "
|
||||
input type: "text", name: "name", autocomplete: "username"
|
||||
br!
|
||||
text "Password: "
|
||||
input type: "password", name: "password"
|
||||
br!
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
8
views/user_me.moon
Normal file
8
views/user_me.moon
Normal file
@ -0,0 +1,8 @@
|
||||
import Widget from require "lapis.html"
|
||||
|
||||
class extends Widget
|
||||
content: =>
|
||||
p "Username: #{@user.name} (#{@user.id})"
|
||||
p "Email Address: #{@user.email}"
|
||||
p "Is admin? #{@user.admin}"
|
||||
p -> a href: @url_for("user_edit"), "Edit"
|
26
views/user_new.moon
Normal file
26
views/user_new.moon
Normal file
@ -0,0 +1,26 @@
|
||||
import Widget from require "lapis.html"
|
||||
|
||||
import autoload from require "locator"
|
||||
import settings from autoload "utility"
|
||||
|
||||
class extends Widget
|
||||
content: =>
|
||||
form {
|
||||
action: @url_for "user_new", nil, redirect: @params.redirect
|
||||
method: "POST"
|
||||
enctype: "multipart/form-data"
|
||||
}, ->
|
||||
p "Username: "
|
||||
input type: "text", name: "name", autocomplete: "username"
|
||||
p "Email: "
|
||||
input type: "email", name: "email"
|
||||
p "Password: "
|
||||
input type: "password", name: "password"
|
||||
br!
|
||||
if settings["users.require-recaptcha"]
|
||||
div class: "g-recaptcha", "data-sitekey": settings["users.recaptcha-sitekey"]
|
||||
input type: "hidden", name: "csrf_token", value: @csrf_token
|
||||
input type: "submit"
|
||||
|
||||
if settings["users.require-recaptcha"]
|
||||
script src: "https://www.google.com/recaptcha/api.js"
|
Loading…
Reference in New Issue
Block a user