Merge commit '63990d006a779d1001a37569492f5fb3df233fed' as 'applications/users'
This commit is contained in:
commit
63a968c661
1
applications/users/.gitignore
vendored
Normal file
1
applications/users/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.lua
|
21
applications/users/LICENSE
Normal file
21
applications/users/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
applications/users/ReadMe.md
Normal file
51
applications/users/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
applications/users/locator_config.moon
Normal file
6
applications/users/locator_config.moon
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Sessions from require "models"
|
||||||
|
|
||||||
|
{
|
||||||
|
before_filter: =>
|
||||||
|
@user = Sessions\get(@session)
|
||||||
|
}
|
66
applications/users/migrations.moon
Normal file
66
applications/users/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
applications/users/models/Sessions.moon
Normal file
32
applications/users/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
applications/users/models/Users.moon
Normal file
35
applications/users/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
applications/users/users.moon
Normal file
335
applications/users/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
applications/users/views/user_admin.moon
Normal file
85
applications/users/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
applications/users/views/user_edit.moon
Normal file
56
applications/users/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
applications/users/views/user_list.moon
Normal file
28
applications/users/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
applications/users/views/user_login.moon
Normal file
17
applications/users/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
applications/users/views/user_me.moon
Normal file
8
applications/users/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
applications/users/views/user_new.moon
Normal file
26
applications/users/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