Merge branch 'master' into master2

This commit is contained in:
edshot99 2025-01-16 19:56:41 -06:00
commit 01d9e47d0a
88 changed files with 902 additions and 459 deletions

View File

@ -498,7 +498,7 @@ Layout/SpaceInsideBlockBraces:
# SupportedStylesForEmptyBraces: space, no_space
Layout/SpaceInsideHashLiteralBraces:
Exclude:
- 'app/controllers/admin/staff_notes_controller.rb'
- 'app/controllers/staff_notes_controller.rb'
- 'app/controllers/admin/users_controller.rb'
- 'app/controllers/application_controller.rb'
- 'app/controllers/comment_votes_controller.rb'
@ -2416,7 +2416,7 @@ Style/StringLiterals:
Exclude:
- 'Gemfile'
- 'Rakefile'
- 'app/controllers/admin/staff_notes_controller.rb'
- 'app/controllers/staff_notes_controller.rb'
- 'app/controllers/blips_controller.rb'
- 'app/controllers/comments_controller.rb'
- 'app/controllers/edit_histories_controller.rb'

View File

@ -26,6 +26,7 @@ gem 'marcel'
gem 'sidekiq-unique-jobs'
gem 'redis'
gem 'request_store'
gem "zxcvbn-ruby", require: "zxcvbn"
gem "diffy"
gem "rugged"

View File

@ -376,6 +376,7 @@ GEM
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.6.13)
zxcvbn-ruby (1.2.0)
PLATFORMS
ruby
@ -426,6 +427,7 @@ DEPENDENCIES
streamio-ffmpeg
webmock
webpacker (>= 4.0.x)
zxcvbn-ruby
BUNDLED WITH
2.4.10

View File

@ -1,41 +0,0 @@
# frozen_string_literal: true
module Admin
class StaffNotesController < ApplicationController
before_action :can_view_staff_notes_only
respond_to :html
def index
@user = User.find_by(id: params[:user_id])
@notes = StaffNote.search(search_params.merge({ user_id: params[:user_id] })).includes(:user, :creator).paginate(params[:page], limit: params[:limit])
respond_with(@notes)
end
def new
@user = User.find(params[:user_id])
@staff_note = StaffNote.new(note_params)
respond_with(@note)
end
def create
@user = User.find(params[:user_id])
@staff_note = StaffNote.create(note_params.merge({creator: CurrentUser.user, user_id: @user.id}))
flash[:notice] = @staff_note.valid? ? "Staff Note added" : @staff_note.errors.full_messages.join("; ")
respond_with(@staff_note) do |format|
format.html do
redirect_back fallback_location: admin_staff_notes_path
end
end
end
private
def search_params
permit_search_params(%i[creator_id creator_name user_id user_name resolved body_matches without_system_user])
end
def note_params
params.fetch(:staff_note, {}).permit(%i[body])
end
end
end

View File

@ -165,6 +165,7 @@ class ApplicationController < ActionController::Base
def set_current_user
SessionLoader.new(request).load
session.send(:load!) unless session.send(:loaded?)
end
def reset_current_user
@ -173,6 +174,14 @@ class ApplicationController < ActionController::Base
CurrentUser.safe_mode = Danbooru.config.safe_mode?
end
def requires_reauthentication
return redirect_to(new_session_path(url: request.fullpath)) if CurrentUser.user.is_anonymous?
last_authenticated_at = session[:last_authenticated_at]
if last_authenticated_at.blank? || Time.zone.parse(last_authenticated_at) < 1.hour.ago
redirect_to(confirm_password_session_path(url: request.fullpath))
end
end
def user_access_check(method)
if !CurrentUser.user.send(method) || CurrentUser.user.is_banned? || IpBan.is_banned?(CurrentUser.ip_addr)
access_denied

View File

@ -3,17 +3,14 @@
module Maintenance
module User
class ApiKeysController < ApplicationController
before_action :requires_reauthentication
before_action :member_only
before_action :authenticate!, except: [:show]
rescue_from ::SessionLoader::AuthenticationFailure, with: :authentication_failed
before_action :load_apikey
respond_to :html
def show
end
def view
end
def update
@api_key.regenerate!
redirect_to(user_api_key_path(CurrentUser.user), notice: "API key regenerated")
@ -24,19 +21,10 @@ module Maintenance
redirect_to(CurrentUser.user)
end
protected
private
def authenticate!
if ::User.authenticate(CurrentUser.user.name, params[:user][:password]) == CurrentUser.user
def load_apikey
@api_key = CurrentUser.user.api_key || ApiKey.generate!(CurrentUser.user)
@password = params[:user][:password]
else
raise ::SessionLoader::AuthenticationFailure
end
end
def authentication_failed
redirect_to(user_api_key_path(CurrentUser.user), notice: "Password was incorrect.")
end
end
end

View File

@ -5,19 +5,20 @@ class ModActionsController < ApplicationController
def index
@mod_actions = ModActionDecorator.decorate_collection(
ModAction.includes(:creator).search(search_params).paginate(params[:page], limit: params[:limit]),
ModAction.visible(CurrentUser.user).includes(:creator).search(search_params).paginate(params[:page], limit: params[:limit]),
)
respond_with(@mod_actions) do |format|
format.json do
render json: @mod_actions.to_json
end
end
respond_with(@mod_actions)
end
def show
@mod_action = ModAction.find(params[:id])
check_permission(@mod_action)
respond_with(@mod_action) do |fmt|
fmt.html { redirect_to mod_actions_path(search: { id: @mod_action.id }) }
end
end
def check_permission(mod_action)
raise(User::PrivilegeError) unless mod_action.can_view?(CurrentUser.user)
end
end

View File

@ -6,26 +6,31 @@ class SessionsController < ApplicationController
end
def create
sparams = params.fetch(:session, {}).slice(:url, :name, :password, :remember)
if RateLimiter.check_limit("login:#{request.remote_ip}", 15, 12.hours)
DanbooruLogger.add_attributes("user.login" => "rate_limited")
return redirect_to(new_session_path, :notice => "Username/Password was incorrect")
return redirect_to(new_session_path, notice: "Username/Password was incorrect")
end
session_creator = SessionCreator.new(session, cookies, params[:name], params[:password], request.remote_ip, params[:remember], request.ssl?)
session_creator = SessionCreator.new(request, session, cookies, sparams[:name], sparams[:password], sparams[:remember].to_s.truthy?)
if session_creator.authenticate
url = params[:url] if params[:url] && params[:url].start_with?("/") && !params[:url].start_with?("//")
url = sparams[:url] if sparams[:url] && sparams[:url].start_with?("/") && !sparams[:url].start_with?("//")
DanbooruLogger.add_attributes("user.login" => "success")
redirect_to(url || posts_path, :notice => "You are now logged in")
redirect_to(url || posts_path)
else
RateLimiter.hit("login:#{request.remote_ip}", 6.hours)
DanbooruLogger.add_attributes("user.login" => "fail")
redirect_to(new_session_path, :notice => "Username/Password was incorrect")
redirect_back(fallback_location: new_session_path, notice: "Username/Password was incorrect")
end
end
def destroy
session.delete(:user_id)
cookies.delete :remember
redirect_to(posts_path, :notice => "You are now logged out")
cookies.delete(:remember)
session.delete(:last_authenticated_at)
redirect_to(posts_path, notice: "You are now logged out")
end
def confirm_password
end
end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
class StaffNotesController < ApplicationController
before_action :can_view_staff_notes_only
before_action :load_staff_note, only: %i[show edit update delete undelete]
before_action :check_edit_privilege, only: %i[update]
before_action :check_delete_privilege, only: %i[delete undelete]
respond_to :html, :json
def index
@user = User.find_by(id: params[:user_id])
@notes = StaffNote.search(search_params.merge({ user_id: params[:user_id] })).includes(:user, :creator).paginate(params[:page], limit: params[:limit])
respond_with(@notes)
end
def show
respond_with(@staff_note)
end
def new
@user = User.find(params[:user_id])
@staff_note = StaffNote.new(staff_note_params)
respond_with(@note)
end
def edit
respond_with(@staff_note)
end
def create
@user = User.find(params[:user_id])
@staff_note = StaffNote.create(staff_note_params.merge({ user_id: @user.id }))
flash[:notice] = @staff_note.valid? ? "Staff Note added" : @staff_note.errors.full_messages.join("; ")
respond_with(@staff_note) do |format|
format.html do
redirect_back fallback_location: staff_notes_path
end
end
end
def update
@staff_note.update(staff_note_params)
redirect_back(fallback_location: staff_notes_path)
end
def delete
@staff_note.update(is_deleted: true)
redirect_back(fallback_location: staff_notes_path)
end
def undelete
@staff_note.update(is_deleted: false)
redirect_back(fallback_location: staff_notes_path)
end
private
def search_params
permit_search_params(%i[creator_id creator_name updater_id updater_name user_id user_name body_matches without_system_user include_deleted])
end
def staff_note_params
params.fetch(:staff_note, {}).permit(%i[body])
end
def load_staff_note
@staff_note = StaffNote.find(params[:id])
end
def check_edit_privilege
raise User::PrivilegeError unless @staff_note.can_edit?(CurrentUser.user)
end
def check_delete_privilege
raise User::PrivilegeError unless @staff_note.can_delete?(CurrentUser.user)
end
end

View File

@ -55,7 +55,7 @@ class ModActionDecorator < ApplicationDecorator
when "artist_delete"
"Deleted artist ##{vals['artist_id']} (#{vals['artist_name']})"
when "artist_page_rename"
"Renamed artist page (\"#{vals['old_name']}\":/artists/show_or_new?name=#{vals['old_name']} -> \"#{vals['new_name']}\":/artists/show_or_new?name=#{vals['new_name']})"
"Renamed artist page (\"#{vals['old_name']}\":/artists/show_or_new?name=#{vals['old_name']} \"#{vals['new_name']}\":/artists/show_or_new?name=#{vals['new_name']})"
when "artist_page_lock"
"Locked artist page artist ##{vals['artist_page']}"
when "artist_page_unlock"
@ -77,6 +77,16 @@ class ModActionDecorator < ApplicationDecorator
when "avoid_posting_undelete"
"Undeleted \"avoid posting ##{vals['id']}\":/avoid_postings/#{vals['id']} for [[#{vals['artist_name']}]]"
### Staff Note ###
when "staff_note_create"
"Created \"staff note ##{vals['id']}\":/staff_notes/#{vals['id']} for #{user}\n#{vals['body']}"
when "staff_note_update"
"Updated \"staff note ##{vals['id']}\":/staff_notes/#{vals['id']} for #{user}\n#{vals['body']}"
when "staff_note_delete"
"Deleted \"staff note ##{vals['id']}\":/staff_notes/#{vals['id']} for #{user}"
when "staff_note_undelete"
"Undeleted \"staff note ##{vals['id']}\":/staff_notes/#{vals['id']} for #{user}"
### User ###
when "user_delete"
@ -276,7 +286,7 @@ class ModActionDecorator < ApplicationDecorator
### BURs ###
when "mass_update"
"Mass updated [[#{vals['antecedent']}]] -> [[#{vals['consequent']}]]"
"Mass updated [[#{vals['antecedent']}]] [[#{vals['consequent']}]]"
when "nuke_tag"
"Nuked tag [[#{vals['tag_name']}]]"
@ -319,7 +329,7 @@ class ModActionDecorator < ApplicationDecorator
"Edited whitelist entry"
else
if vals['old_pattern'] && vals['old_pattern'] != vals['pattern'] && CurrentUser.is_admin?
"Edited whitelist entry '#{vals['old_pattern']}' -> '#{vals['pattern']}'"
"Edited whitelist entry '#{vals['old_pattern']}' '#{vals['pattern']}'"
else
"Edited whitelist entry '#{CurrentUser.is_admin? ? vals['pattern'] : vals['note']}'"
end

View File

@ -28,6 +28,7 @@ module PostIndex
tag_count_general: { type: "integer" },
tag_count_artist: { type: "integer" },
tag_count_contributor: { type: "integer" },
tag_count_character: { type: "integer" },
tag_count_copyright: { type: "integer" },
tag_count_meta: { type: "integer" },
@ -240,6 +241,7 @@ module PostIndex
tag_count_general: tag_count_general,
tag_count_artist: tag_count_artist,
tag_count_contributor: tag_count_contributor,
tag_count_character: tag_count_character,
tag_count_copyright: tag_count_copyright,
tag_count_meta: tag_count_meta,
@ -247,8 +249,8 @@ module PostIndex
tag_count_invalid: tag_count_invalid,
tag_count_lore: tag_count_lore,
tag_count_people: tag_count_people,
comment_count: options[:comment_count] || comment_count,
comment_count: options[:comment_count] || comment_count,
file_size: file_size,
parent: parent_id,
pools: options[:pools] || ::Pool.where("? = ANY(post_ids)", id).pluck(:id),

View File

@ -47,6 +47,7 @@ export { default as PostReplacement } from '../src/javascripts/post_replacement.
export { default as PostVersions } from '../src/javascripts/post_versions.js';
export { default as Replacer } from '../src/javascripts/replacer.js';
export { default as Shortcuts } from '../src/javascripts/shortcuts.js';
export { default as StaffNote } from '../src/javascripts/staff_notes.js';
export { default as Utility } from '../src/javascripts/utility.js';
export { default as TagRelationships } from '../src/javascripts/tag_relationships.js';
export { default as Takedown } from '../src/javascripts/takedowns.js';

View File

@ -0,0 +1,70 @@
import zxcvbn from "zxcvbn";
import Page from "./utility/page";
let Password = {};
Password.init_validation = function () {
if (Page.matches("users", "new") || Page.matches("users", "create"))
Password.bootstrap_input($("#user_password"), [$("#user_name"), $("#user_email")]);
if (Page.matches("maintenance-user-password-resets", "edit"))
Password.bootstrap_input($("#password"));
if (Page.matches("maintenance-user-passwords", "edit"))
Password.bootstrap_input($("#user_password"));
};
Password.bootstrap_input = function ($password, $inputs = []) {
// Set up the UI
$password.parent().addClass("password-input");
const hint = $("<div>")
.addClass("password-feedback")
.insertAfter($password);
const display = $("<div>")
.addClass("password-strength")
.insertAfter($password);
const progress = $("<div>")
.addClass("password-progress")
.css("width", 0)
.appendTo(display);
// Listen to secondary input changes
let extraData = getExtraData();
for (const one of $inputs)
one.on("input", () => {
extraData = getExtraData();
});
// Listen to main input changes
$password.on("input", () => {
const analysis = zxcvbn($password.val() + "", extraData);
progress.css("width", ((analysis.score * 25) + 10) + "%");
hint.html("");
if (analysis.feedback.warning)
$("<span>")
.text(analysis.feedback.warning)
.addClass("password-warning")
.appendTo(hint);
for (const one of analysis.feedback.suggestions)
$("<span>")
.text(one)
.appendTo(hint);
});
function getExtraData () {
const output = [];
for (const one of $inputs) {
const val = one.val() + "";
if (val) output.push(one.val() + "");
}
return output;
}
};
$(() => {
Password.init_validation();
});
export default Password;

View File

@ -0,0 +1,25 @@
const StaffNote = {};
StaffNote.initialize_all = function () {
$(".expand-new-staff-note").on("click", StaffNote.show_new_note_form);
$(".edit-staff-note-link").on("click", StaffNote.show_edit_form);
};
StaffNote.show_new_note_form = function (e) {
e.preventDefault();
$(e.target).hide();
var $form = $(e.target).closest("div.new-staff-note").find("form");
$form.show();
$form[0].scrollIntoView(false);
};
StaffNote.show_edit_form = function (e) {
e.preventDefault();
$(this).closest(".staff-note").find(".edit_staff_note").show();
};
$(document).ready(function () {
StaffNote.initialize_all();
});
export default StaffNote;

View File

@ -11,6 +11,7 @@
Related:
<a href="#" @click.prevent="findRelated()">Tags</a> |
<a href="#" @click.prevent="findRelated(1)">Artists</a> |
<a href="#" @click.prevent="findRelated(2)">Contributors</a> |
<a href="#" @click.prevent="findRelated(3)">Copyrights</a> |
<a href="#" @click.prevent="findRelated(4)">Characters</a> |
<a href="#" @click.prevent="findRelated(9)">People</a> |

View File

@ -28,7 +28,7 @@
<template v-if="normalMode">
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="names">Artists</label>
<label class="section-label" for="names">Artists and Contributors</label>
<div><a href="/help/tags#catchange">How do I tag an artist?</a></div>
<div>Please don't use <a href="/wiki_pages/anonymous_artist">anonymous_artist</a> or <a href="/wiki_pages/unknown_artist">unknown_artist</a> tags unless they fall under those definitions on the wiki.</div>
</div>

View File

@ -54,7 +54,6 @@
@import "specific/iqdb_queries.scss";
@import "specific/keyboard_shortcuts.scss";
@import "specific/lockdown.scss";
@import "specific/maintenance.scss";
@import "specific/meta_searches.scss";
@import "specific/moderator_dashboard.scss";
@import "specific/news_updates.scss";

View File

@ -156,6 +156,7 @@ $user-home-greeting-color: gold;
$tag-categories: (
"0": "general",
"1": "artist",
"2": "contributor",
"3": "copyright",
"4": "character",
"5": "species",
@ -168,6 +169,7 @@ $tag-categories: (
$tag-categories-short: (
"gen": "general",
"art": "artist",
"cont": "contributor",
"copy": "copyright",
"char": "character",
"spec": "species",

View File

@ -24,6 +24,10 @@ section.posts-container {
h2.posts-container-header {
grid-column: -1 / 1;
}
.no-results {
grid-column: -1 / 1;
}
}

View File

@ -1,5 +0,0 @@
div#c-maintenance-user-login-reminders {
div#a-new {
width: 50em;
}
}

View File

@ -1,8 +1,6 @@
div#c-sessions {
div#a-new {
label#remember-label {
label[for=session_remember] {
display: inline;
font-weight: normal;
font-style: italic;

View File

@ -1,11 +1,15 @@
.staff-note-list {
.staff-note {
margin-bottom: $padding-050;
}
// This is getting overwritten through DText styling on user profiles,
// because the whole list is wrapped in a collapsable section.
h4.author-name {
font-size: $h4-size;
}
}
.staff-note {
margin-bottom: $padding-050;
&[data-is-deleted="true"] {
background: $background-article-hidden;
}
}

View File

@ -223,13 +223,83 @@ div#c-users {
color: themed("color-link-active");
}
}
}
div#a-new {
max-width: 60em;
// User signup and login
#c-users #a-new,
#c-sessions #a-new,
#c-maintenance-user-password-resets #a-new,
#c-maintenance-user-login-reminders #a-new {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(auto, 360px));
gap: 1em;
p {
font-size: 1.2em;
line-height: 1.4em;
margin-bottom: 1em;
}
.simple_form.session_form {
box-sizing: border-box;
max-width: 360px;
margin: 0;
h1 {
margin-bottom: 0.5em;
text-align: center;
}
div.input {
input[type="text"], input[type="email"], input[type="password"], select {
width: 100%;
max-width: unset;
box-sizing: border-box;
}
}
}
.session_info {
display: flex;
flex-flow: column;
justify-content: center;
box-sizing: border-box;
max-width: 360px;
padding: 0.5rem;
border-radius: 3px;
background-color: themed("color-section");
h3 { margin-bottom: 1em; }
}
// Password validation
.password-input {
input[type="password"] {
border-radius: 3px 3px 0 0;
}
.password-strength {
width: 100%;
height: 0.25rem;
border-radius: 0 0 3px 3px;
background: white;
overflow: hidden;
margin: 0;
.password-progress {
background: linear-gradient(to right, palette("text-red") 0%, palette("text-yellow") 25%, palette("text-green") 100%);
background-size: 360px 100%;
height: 100%;
transition: width 1s ease-in-out;
}
}
.password-feedback {
display: flex;
flex-flow: column;
padding-left: 1em;
margin-top: 0.5em;
span { display: list-item; }
.password-warning { font-weight: bold; }
}
}

View File

@ -83,6 +83,9 @@ body {
--color-tag-artist: #f2ac08;
--color-tag-artist-alt: #fbd67f;
--color-tag-contributor: #c0c0c0;
--color-tag-contributor-alt: #71706e;
--color-tag-copyright: #d0d;
--color-tag-copyright-alt: #ff5eff;

View File

@ -325,7 +325,7 @@ class ElasticPostQueryBuilder < ElasticQueryBuilder
order.push({id: :desc})
end
if !CurrentUser.user.is_staff? && Security::Lockdown.hide_pending_posts_for > 0
if !CurrentUser.user.nil? && !CurrentUser.user.is_staff? && Security::Lockdown.hide_pending_posts_for > 0
should = [
{
range: {

View File

@ -1,16 +1,15 @@
# frozen_string_literal: true
class SessionCreator
attr_reader :session, :cookies, :name, :password, :ip_addr, :remember, :secure
attr_reader :request, :session, :cookies, :name, :password, :remember
def initialize(session, cookies, name, password, ip_addr, remember = false, secure = false)
def initialize(request, session, cookies, name, password, remember = false)
@request = request
@session = session
@cookies = cookies
@name = name
@password = password
@ip_addr = ip_addr
@remember = remember
@secure = secure
end
def authenticate
@ -18,12 +17,13 @@ class SessionCreator
user = User.find_by_name(name)
session[:user_id] = user.id
session[:last_authenticated_at] = Time.now.utc.to_s
session[:ph] = user.password_token
user.update_column(:last_ip_addr, ip_addr) unless user.is_blocked?
user.update_column(:last_ip_addr, request.remote_ip) unless user.is_blocked?
if remember
verifier = ActiveSupport::MessageVerifier.new(Danbooru.config.remember_key, serializer: JSON, digest: "SHA256")
cookies.encrypted[:remember] = {value: verifier.generate("#{user.id}:#{user.password_token}", purpose: "rbr", expires_in: 14.days), expires: Time.now + 14.days, httponly: true, same_site: :lax, secure: Rails.env.production?}
cookies.encrypted[:remember] = { value: verifier.generate("#{user.id}:#{user.password_token}", purpose: "rbr", expires_in: 14.days), expires: Time.now + 14.days, httponly: true, same_site: :lax, secure: Rails.env.production? }
end
return true
else

View File

@ -6,6 +6,9 @@ class TagCategory
"gen" => 0,
"artist" => 1,
"art" => 1,
"contributor" => 2,
"contrib" => 2,
"cont" => 2,
"copyright" => 3,
"copy" => 3,
"co" => 3,
@ -27,6 +30,7 @@ class TagCategory
CANONICAL_MAPPING = {
"General" => 0,
"Artist" => 1,
"Contributor" => 2,
"Copyright" => 3,
"Character" => 4,
"Species" => 5,
@ -39,6 +43,7 @@ class TagCategory
REVERSE_MAPPING = {
0 => "general",
1 => "artist",
2 => "contributor",
3 => "copyright",
4 => "character",
5 => "species",
@ -51,6 +56,7 @@ class TagCategory
SHORT_NAME_MAPPING = {
"gen" => "general",
"art" => "artist",
"cont" => "contributor",
"copy" => "copyright",
"char" => "character",
"spec" => "species",
@ -63,6 +69,7 @@ class TagCategory
HEADER_MAPPING = {
"general" => "General",
"artist" => "Artists",
"contributor" => "Contributors",
"copyright" => "Copyrights",
"character" => "Characters",
"species" => "Species",
@ -75,6 +82,7 @@ class TagCategory
ADMIN_ONLY_MAPPING = {
"general" => false,
"artist" => false,
"contributor" => false,
"copyright" => false,
"character" => false,
"species" => false,
@ -105,13 +113,13 @@ class TagCategory
},
}.freeze
CATEGORIES = %w[general species character copyright artist invalid lore meta people].freeze
CATEGORIES = %w[general species character copyright artist contributor invalid lore meta people].freeze
CATEGORY_IDS = CANONICAL_MAPPING.values
SHORT_NAME_LIST = SHORT_NAME_MAPPING.keys
HUMANIZED_LIST = %w[character copyright artist].freeze
SPLIT_HEADER_LIST = %w[invalid artist copyright character people species general meta lore].freeze
CATEGORIZED_LIST = %w[invalid artist copyright character people species meta general lore].freeze
SPLIT_HEADER_LIST = %w[invalid artist contributor copyright character people species general meta lore].freeze
CATEGORIZED_LIST = %w[invalid artist contributor copyright character people species meta general lore].freeze
SHORT_NAME_REGEX = SHORT_NAME_LIST.join("|").freeze
ALL_NAMES_REGEX = MAPPING.keys.join("|").freeze

View File

@ -16,6 +16,10 @@ class ModAction < ApplicationRecord
avoid_posting_delete: %i[id artist_name],
avoid_posting_undelete: %i[id artist_name],
avoid_posting_destroy: %i[id artist_name],
staff_note_create: %i[id user_id body],
staff_note_update: %i[id user_id body old_body],
staff_note_delete: %i[id user_id],
staff_note_undelete: %i[id user_id],
blip_delete: %i[blip_id user_id],
blip_hide: %i[blip_id user_id],
blip_unhide: %i[blip_id user_id],
@ -91,9 +95,20 @@ class ModAction < ApplicationRecord
takedown_process: %i[takedown_id],
}.freeze
ProtectedActionKeys = %w[staff_note_create staff_note_update staff_note_delete staff_note_undelete ip_ban_create ip_ban_delete].freeze
KnownActionKeys = KnownActions.keys.freeze
def self.search(params)
module SearchMethods
def visible(user)
if user.is_staff?
all
else
where.not(action: ProtectedActionKeys)
end
end
def search(params)
q = super
q = q.where_user(:creator_id, :creator, params)
@ -104,6 +119,15 @@ class ModAction < ApplicationRecord
q.apply_basic_order(params)
end
end
def can_view?(user)
if user.is_staff?
true
else
ProtectedActionKeys.exclude?(action)
end
end
def values
original_values = self[:values]
@ -131,7 +155,7 @@ class ModAction < ApplicationRecord
end
def hidden_attributes
super + [:values]
super + %i[values values_old]
end
def method_attributes
@ -145,4 +169,6 @@ class ModAction < ApplicationRecord
def initialize_creator
self.creator_id = CurrentUser.id
end
extend SearchMethods
end

View File

@ -1,8 +1,33 @@
# frozen_string_literal: true
class StaffNote < ApplicationRecord
belongs_to :creator, class_name: "User"
belongs_to_creator
belongs_to_updater
belongs_to :user
after_create :log_create
after_update :log_update
scope :active, -> { where(is_deleted: false) }
module LogMethods
def log_create
ModAction.log(:staff_note_create, { id: id, user_id: user_id, body: body })
end
def log_update
if saved_change_to_body?
ModAction.log(:staff_note_update, { id: id, user_id: user_id, body: body, old_body: body_before_last_save })
end
if saved_change_to_is_deleted?
if is_deleted?
ModAction.log(:staff_note_delete, { id: id, user_id: user_id })
else
ModAction.log(:staff_note_undelete, { id: id, user_id: user_id })
end
end
end
end
module SearchMethods
def search(params)
@ -12,28 +37,45 @@ class StaffNote < ApplicationRecord
q = q.attribute_matches(:body, params[:body_matches])
q = q.where_user(:user_id, :user, params)
q = q.where_user(:creator_id, :creator, params)
q = q.where_user(:updater_id, :updater, params)
if params[:without_system_user]&.truthy?
q = q.where.not(creator: User.system)
end
if params[:is_deleted].present?
q = q.attribute_matches(:is_deleted, params[:is_deleted])
elsif !params[:include_deleted]&.truthy?
q = q.active
end
q.apply_basic_order(params)
end
def default_order
order("resolved asc, id desc")
order("id desc")
end
end
include LogMethods
extend SearchMethods
def resolve!
self.resolved = true
save
def user_name
User.id_to_name(user_id)
end
def unresolve!
self.resolved = false
save
def user_name=(name)
self.user_id = User.name_to_id(name)
end
def can_edit?(user)
return false unless user.is_staff?
user.id == creator_id || user.is_admin?
end
def can_delete?(user)
return false unless user.is_staff?
return true if creator_id == user.id || user.is_admin?
user_id != user.id
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require "zxcvbn"
class User < ApplicationRecord
class Error < Exception ; end
class PrivilegeError < Exception
@ -71,7 +73,8 @@ class User < ApplicationRecord
validates :per_page, inclusion: { :in => 1..320 }
validates :comment_threshold, presence: true
validates :comment_threshold, numericality: { only_integer: true, less_than: 50_000, greater_than: -50_000 }
validates :password, length: { :minimum => 6, :if => ->(rec) { rec.new_record? || rec.password.present? || rec.old_password.present? } }
validates :password, length: { minimum: 8, if: ->(rec) { rec.new_record? || rec.password.present? || rec.old_password.present? } }
validate :password_is_secure, if: ->(rec) { rec.new_record? || rec.password.present? || rec.old_password.present? }
validates :password, confirmation: true
validates :password_confirmation, presence: { if: ->(rec) { rec.new_record? || rec.old_password.present? } }
validate :validate_ip_addr_is_not_banned, :on => :create
@ -108,7 +111,7 @@ class User < ApplicationRecord
has_many :post_sets, -> { order(name: :asc) }, foreign_key: :creator_id
has_many :post_versions
has_many :post_votes
has_many :staff_notes, -> { order("staff_notes.id desc") }
has_many :staff_notes, -> { active.order("staff_notes.id desc") }
has_many :user_name_change_requests, -> { order(id: :asc) }
belongs_to :avatar, class_name: 'Post', optional: true
@ -227,6 +230,16 @@ class User < ApplicationRecord
def upgrade_password(pass)
self.update_columns(password_hash: '', bcrypt_password_hash: User.bcrypt(pass))
end
def password_is_secure
analysis = Zxcvbn.test(password, [name, email])
return unless analysis.score < 2
if analysis.feedback.warning
errors.add(:password, "is insecure: #{analysis.feedback.warning}")
else
errors.add(:password, "is insecure")
end
end
end
module AuthenticationMethods

View File

@ -1,13 +0,0 @@
<% if CurrentUser.can_view_staff_notes? %>
<div class="staff-notes-section styled-dtext">
<details>
<summary>Staff Notes (<%= user.staff_notes.count %>)</summary>
<div>
<h4><%= link_to "Staff Notes", user_staff_notes_path(user_id: user.id) %></h4>
<%= render "admin/staff_notes/partials/list_of_notes", staff_notes: user.staff_notes.limit(15), show_receiver_name: false %>
<h4>New Staff Note</h4>
<%= render "admin/staff_notes/partials/new", user: user, staff_note: StaffNote.new(user_id: user.id) %>
</div>
</details>
</div>
<% end %>

View File

@ -1,3 +0,0 @@
<div id="p-staff-notes-list" class="staff-note-list">
<%= render partial: "admin/staff_notes/partials/staff_note", collection: staff_notes, locals: { show_receiver_name: show_receiver_name } %>
</div>

View File

@ -1,6 +0,0 @@
<%= error_messages_for :staff_note %>
<%= custom_form_for(staff_note, url: user_staff_notes_path(user_id: user.id), method: :post) do |f| %>
<%= f.input :body, as: :dtext, label: false, allow_color: true %>
<%= f.button :submit, "Submit" %>
<% end %>

View File

@ -1,19 +0,0 @@
<article class="staff-note comment-post-grid" id="staff-note-<%= staff_note.id %>">
<div class="author-info">
<div class="name-rank">
<h4 class="author-name"><%= link_to_user staff_note.creator %></h4>
<%= staff_note.creator.level_string %>
</div>
<div class="post-time">
<%= link_to time_ago_in_words_tagged(staff_note.created_at), user_staff_notes_path(user_id: staff_note.user_id, anchor: "staff-note-#{staff_note.id}") %>
</div>
</div>
<div class="content">
<% if show_receiver_name %>
<h4>On <%= link_to_user staff_note.user %></h4>
<% end %>
<div class="body dtext-container">
<%= format_text(staff_note.body, allow_color: true) %>
</div>
</div>
</article>

View File

@ -2,7 +2,7 @@
<div id="a-new">
<h1>Remove stuck DNP tags</h1>
<p>
Use this to remove <%= link_to "avoid_posting", wiki_page_path(id: "avoid_posting") %> and <%= link_to "conditional_dnp", wiki_page_path(id: "conditional_dnp") %> from posts of artists who got removed from the DNP list.
Use this to remove <%= link_to "avoid_posting", avoid_posting_static_path %> and <%= link_to "conditional_dnp", show_or_new_wiki_pages_path(title: "conditional_dnp") %> from posts of artists who got removed from the DNP list.
</p>
<p>
Limit of 1,000 posts at a time.

View File

@ -7,5 +7,5 @@
<%= f.input :artist_name, label: "Artist Name", hide_unless_value: true %>
<%= f.input :artist_id, label: "Artist ID", hide_unless_value: true %>
<%= f.input :any_other_name_matches, label: "Other Names", hide_unless_value: true %>
<%= f.input :is_active, label: "Active?", collection: [%w[Yes true], %w[No false], %w[Any any]] %>
<%= f.input :is_active, label: "Active?", collection: [["Yes", true], ["No", false]], include_blank: true %>
<% end %>

View File

@ -14,6 +14,6 @@
<% if CurrentUser.is_staff? %>
<%= f.input :staff_notes, label: "Staff Notes" %>
<% end %>
<%= f.input :is_active, label: "Active?", collection: [%w[Yes true], %w[No false], %w[Any any]] %>
<%= f.input :is_active, label: "Active?", collection: [["Yes", true], ["No", false]], include_blank: true %>
<%= f.input :order, collection: [["Artist Name", "artist_name"], %w[Created created_at], %w[Updated updated_at]], include_blank: true %>
<% end %>

View File

@ -1,4 +1,10 @@
<% content_for(:secondary_links) do %>
<%= subnav_link_to "Listing", bulk_update_requests_path %>
<%= subnav_link_to "New", new_bulk_update_request_path %>
<%= subnav_link_to "Alias Listing", tag_aliases_path %>
<%= subnav_link_to "Implication Listing", tag_implications_path %>
<%= subnav_link_to "MetaSearch", meta_searches_tags_path %>
<%= subnav_link_to "New BUR", new_bulk_update_request_path %>
<%= subnav_link_to "Request implication", new_tag_implication_request_path %>
<%= subnav_link_to "Request alias", new_tag_alias_request_path %>
<%= subnav_link_to "Help", help_page_path(id: "tag_relationships#bur") %>
<% end %>

View File

@ -1,12 +1,35 @@
<div id="c-maintenance-user-api-keys">
<div id="a-show">
<h1>API Key</h1>
<p>You must re-enter your password to view or change your API key.</p>
<%= custom_form_for CurrentUser.user, url: view_user_api_key_path(CurrentUser.user), method: :post do |f| %>
<%= f.input :password, :as => :password, :input_html => {:autocomplete => "off"} %>
<%= f.button :submit, "Submit" %>
<% end %>
<h2><b>Your API key is like your password</b></h2>
<p>
Anyone who has it has full access to your account. Don't give your API key
to third-party apps you don't trust, and don't post your API key in public places.
</p>
<table class="striped">
<thead>
<tr>
<th>API Key</th>
<th>Created</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr id="api-key-<%= @api_key.id %>">
<td id="api-key"><code><%= @api_key.key %></code></td>
<td id="api-key-created"><%= compact_time(@api_key.created_at) %></td>
<td id="api-key-updated"><%= compact_time(@api_key.updated_at) %></td>
<td>
<%= button_to("Regenerate", user_api_key_path(CurrentUser.user), method: :put) %>
<%= button_to("Delete", user_api_key_path(CurrentUser.user), method: :delete) %>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -1,38 +0,0 @@
<div id="c-maintenance-user-api-keys">
<div id="a-view">
<h1>API Key</h1>
<p>
<h2><b>Your API key is like your password</b></h2>
Anyone who has it has full access to your account. Don't give your API key
to third-party apps you don't trust, and don't post your API key in public places.
</p>
<table class="striped">
<thead>
<tr>
<th>API Key</th>
<th>Created</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td id="api-key"><code><%= @api_key.key %></code></td>
<td id="api-key-created"><%= compact_time @api_key.created_at %></td>
<td id="api-key-updated"><%= compact_time @api_key.updated_at %></td>
<td>
<%= button_to "Regenerate", user_api_key_path(CurrentUser.user), method: :put, params: { 'user[password]': @password } %>
<%= button_to "Delete", user_api_key_path(CurrentUser.user), method: :delete, params: { 'user[password]': @password } %>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<% content_for(:page_title) do %>
API Key
<% end %>

View File

@ -1,12 +1,8 @@
<div id="c-maintenance-user-login-reminders">
<div id="a-new">
<div id="c-maintenance-user-login-reminders"><div id="a-new">
<%= form_tag(maintenance_user_login_reminder_path, :class => "simple_form session_form") do %>
<h1>Login Reminder</h1>
<p>If you supplied an email address when signing up, <%= Danbooru.config.app_name %> can email you your login information. Password details will not be provided and will not be changed.</p>
<p>If you didn't supply a valid email address, you are out of luck.</p>
<%= form_tag(maintenance_user_login_reminder_path, :class => "simple_form") do %>
<div class="input email required">
<label for="user_email" class="required">Email</label>
<%= email_field(:user, :email) %>
@ -14,8 +10,12 @@
<%= submit_tag "Submit" %>
<% end %>
</div>
</div>
<section class="session_info">
<p>If you supplied an email address when signing up, <%= Danbooru.config.app_name %> can email you your login information. Password details will not be provided and will not be changed.</p>
<p>If you didn't supply a valid email address, you are out of luck.</p>
</section>
</div></div>
<%= render "sessions/secondary_links" %>

View File

@ -1,26 +1,28 @@
<div id="c-maintenance-user-password-resets">
<div id="a-edit">
<h1>Reset Password</h1>
<div id="c-maintenance-user-password-resets"><div id="a-edit">
<% if @nonce %>
<%= form_tag(maintenance_user_password_reset_path, :method => :put) do %>
<%= form_tag(maintenance_user_password_reset_path, :method => :put, :class => "simple_form session_form") do %>
<h1>Reset Password</h1>
<%= hidden_field_tag :uid, params[:uid] %>
<%= hidden_field_tag :key, params[:key] %>
<div class="input">
<label for="password">Password</label>
<%= password_field_tag :password %>
</div>
<div class="input">
<label for="password_confirm">Confirm Password</label>
<%= password_field_tag :password_confirm %>
</div>
<%= submit_tag "Reset" %>
<% end %>
<% else %>
<p>Invalid reset</p>
<% end %>
</div>
</div>
</div></div>
<%= render "sessions/secondary_links" %>

View File

@ -1,20 +1,19 @@
<div id="c-maintenance-user-password-resets">
<div id="a-new">
<div id="c-maintenance-user-password-resets"><div id="a-new">
<%= form_tag(maintenance_user_password_reset_path, :class => "simple_form session_form") do %>
<h1>Reset Password</h1>
<p>If you supplied an email address when signing up, <%= Danbooru.config.app_name %> can reset your password. You will receive an email confirming your request for a new password.</p>
<p>If you didn't supply a valid email address, there is no way to recover your account.</p>
<%= form_tag(maintenance_user_password_reset_path, :class => "simple_form") do %>
<div class="input email required">
<label for="nonce_email" class="required">Email</label>
<%= text_field_tag :email %>
</div>
<%= submit_tag "Submit" %>
<% end %>
</div>
</div>
<section class="session_info">
<p>If you supplied an email address when signing up, <%= Danbooru.config.app_name %> can reset your password. You will receive an email confirming your request for a new password.</p>
<p>If you didn't supply a valid email address, there is no way to recover your account.</p>
</section>
</div></div>
<%= render "sessions/secondary_links" %>

View File

@ -1,15 +1,12 @@
<div id="c-maintenance-user-passwords">
<div id="a-edit">
<div id="c-maintenance-user-passwords"><div id="a-edit">
<%= custom_form_for(@user, html: { class: "session_form" }) do |f| %>
<h1>Change Password</h1>
<%= custom_form_for @user do |f| %>
<%= f.input :old_password, :as => :password, :input_html => {:autocomplete => "off"} %>
<%= f.input :password, :label => "New password", :input_html => {:autocomplete => "off"} %>
<%= f.input :password_confirmation %>
<%= f.button :submit, "Submit" %>
<% end %>
</div>
</div>
</div></div>
<% content_for(:page_title) do %>
Change Password

View File

@ -1,4 +1,4 @@
<%= form_search(path: mod_actions_path) do |f| %>
<%= f.user :creator %>
<%= f.input :action, label: "Action", collection: ModAction::KnownActionKeys.map {|k| [k.to_s.capitalize.tr("_"," "), k.to_s]}, include_blank: true %>
<%= f.input :action, label: "Action", collection: ModAction::KnownActionKeys.reject { |k| !CurrentUser.is_staff? && ModAction::ProtectedActionKeys.include?(k) }.map { |k| [k.to_s.capitalize.tr("_", " "), k.to_s] }, include_blank: true %>
<% end %>

View File

@ -1,3 +1,3 @@
<p>Nobody here but us chickens!</p>
<p class="no-results">Nobody here but us chickens!</p>
<p class="paginator"><%= link_to "Go back", :back, :rel => "prev" %></p>

View File

@ -0,0 +1,16 @@
<% content_for(:page_title) do %>
Confirm Password
<% end %>
<%= render "secondary_links" %>
<div id="c-sessions"><div id="a-confirm-password">
<h1>Confirm password</h1>
<p>You must re-enter your password to continue.</p>
<%= simple_form_for(:session, url: session_path) do |f| %>
<%= f.input(:url, as: :hidden, input_html: { value: params[:url] }) %>
<%= f.input(:name, as: :hidden, input_html: { value: CurrentUser.user.name }) %>
<%= f.input(:password, hint: link_to("Forgot password?", new_maintenance_user_password_reset_path), input_html: { autocomplete: "current-password" }) %>
<%= f.submit("Continue") %>
<% end %>
</div></div>

View File

@ -1,38 +1,24 @@
<div id="c-sessions">
<div id="a-new">
<section>
<div id="c-sessions"><div id="a-new">
<%= simple_form_for(:session, url: session_path, html: { class: "session_form" }) do |f| %>
<h1>Sign in</h1>
<%= form_tag(session_path, :class => "simple_form") do %>
<%= hidden_field_tag "url", params[:url] %>
<div class="input">
<label for="name">Username</label>
<%= text_field_tag :name %>
</div>
<div class="input">
<label for="password">Password</label>
<%= password_field_tag :password %>
</div>
<div class="input">
<%= check_box_tag :remember, "1", true %>
<label for="remember" id="remember-label">Remember</label>
</div>
<div class="input">
<%= submit_tag "Submit", :data => { :disable_with => "Signing in..." } %>
</div>
<%= f.input(:url, as: :hidden, input_html: { value: params[:url] }) %>
<%= f.input(:name, label: "Username") %>
<%= f.input(:password) %>
<%= f.input(:remember, as: :boolean, input_html: { checked: "checked" }) %>
<%= f.submit("Continue") %>
<% end %>
</section>
<section class="box-section">
<h2><%= link_to "Don't have an account? Sign up here.", new_user_path() %></h2>
<h2><%= link_to "Reset Password", new_maintenance_user_password_reset_path %></h2>
<h2><%= link_to "Login Reminder", new_maintenance_user_login_reminder_path %></h2>
<section class="session_info">
<h3>
Don't have an account?<br />
<%= link_to("Sign up here.", new_user_path) %>
</h3>
<br />
<h3><%= link_to("Reset Password", new_maintenance_user_password_reset_path) %></h3>
<h3><%= link_to("Login Reminder", new_maintenance_user_login_reminder_path) %></h3>
</section>
</div>
</div>
</div></div>
<% content_for(:page_title) do %>
Sign in

View File

@ -1,6 +1,8 @@
<%= form_search path: admin_staff_notes_path, always_display: true do |f| %>
<%= form_search path: staff_notes_path do |f| %>
<%= f.user :creator %>
<%= f.user :updater %>
<%= f.user :user %>
<%= f.input :body_matches, label: "Body" %>
<%= f.input :without_system_user, as: :boolean, label: "Hide automated?" %>
<%= f.input :include_deleted, as: :boolean, label: "Show deleted?" %>
<% end %>

View File

@ -0,0 +1,4 @@
<% content_for(:secondary_links) do %>
<%= subnav_link_to "Listing", staff_notes_path %>
<%= subnav_link_to "Search", search_staff_notes_path %>
<% end %>

View File

@ -0,0 +1,13 @@
<div id="c-staff-notes">
<div id="a-edit">
<h1>Edit Staff Note For <%= link_to_user @staff_note.user %></h1>
<%= render "staff_notes/partials/edit", staff_note: @staff_note %>
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
Edit Staff Note
<% end %>

View File

@ -7,12 +7,12 @@
<% end %>
<%= render "search" %>
<%= render "admin/staff_notes/partials/list_of_notes", staff_notes: @notes, show_receiver_name: @user.blank? %>
<%= render "staff_notes/partials/list_of_notes", staff_notes: @notes, show_receiver_name: @user.blank? %>
<%= numbered_paginator(@notes) %>
</div>
</div>
<%= render "users/secondary_links" %>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
<%= @user ? "#{@user.name} - Staff Notes" : "Staff Notes" %>

View File

@ -2,11 +2,11 @@
<div id="a-new">
<h1>New Staff Note for <%= link_to_user(@user) %></h1>
<%= render "admin/staff_notes/partials/new", staff_note: @staff_note, user: @user %>
<%= render "staff_notes/partials/new", staff_note: @staff_note, user: @user %>
</div>
</div>
<%= render "users/secondary_links" %>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
New Staff Note

View File

@ -0,0 +1,6 @@
<%= error_messages_for :staff_note %>
<%= custom_form_for(staff_note, html: { style: ("display: none;" if local_assigns[:hidden]) }, url: staff_note_path(staff_note, user_id: staff_note.user_id), method: :patch) do |f| %>
<%= f.input :body, as: :dtext, label: false %>
<%= f.button :submit, "Submit" %>
<% end %>

View File

@ -0,0 +1,15 @@
<% if CurrentUser.can_view_staff_notes? %>
<div class="staff-notes-section styled-dtext">
<details>
<summary>Staff Notes (<%= user.staff_notes.count %>)</summary>
<div>
<h4><%= link_to "Staff Notes", staff_notes_path(search: { user_id: user.id }) %></h4>
<%= render "staff_notes/partials/list_of_notes", staff_notes: user.staff_notes.limit(15), show_receiver_name: false %>
<div class="new-staff-note">
<p><%= link_to "Create »", new_staff_note_path(search: { user_id: user.id }), class: "expand-new-staff-note" %></p>
<%= render "staff_notes/partials/new", user: user, staff_note: StaffNote.new(user_id: user.id), hidden: true %>
</div>
</div>
</details>
</div>
<% end %>

View File

@ -0,0 +1,3 @@
<div id="p-staff-notes-list" class="staff-note-list">
<%= render partial: "staff_notes/partials/staff_note", collection: staff_notes, locals: { show_receiver_name: show_receiver_name } %>
</div>

View File

@ -0,0 +1,6 @@
<%= error_messages_for :staff_note %>
<%= custom_form_for(staff_note, html: { style: ("display: none;" if local_assigns[:hidden]) }, url: staff_notes_path(user_id: user.id), method: :post) do |f| %>
<%= f.input :body, as: :dtext, label: false, allow_color: true %>
<%= f.button :submit, "Submit" %>
<% end %>

View File

@ -0,0 +1,39 @@
<article class="staff-note comment-post-grid" data-is-deleted="<%= staff_note.is_deleted %>" id="staff-note-<%= staff_note.id %>">
<div class="author-info">
<div class="name-rank">
<h4 class="author-name"><%= link_to_user staff_note.creator %></h4>
<%= staff_note.creator.level_string %>
</div>
<div class="post-time">
<%= link_to time_ago_in_words_tagged(staff_note.created_at), staff_notes_path(search: { user_id: staff_note.user_id }, anchor: "staff-note-#{staff_note.id}") %>
</div>
</div>
<div class="content">
<% if show_receiver_name %>
<h4>On <%= link_to_user staff_note.user %></h4>
<% end %>
<div class="body dtext-container">
<%= format_text(staff_note.body, allow_color: true) %>
</div>
<%= render "application/update_notice", record: staff_note %>
<div class="content-menu">
<menu>
<% if staff_note.can_edit?(CurrentUser.user) %>
<li><%= link_to "Edit", edit_staff_note_path(staff_note), id: "edit-staff-note-link-#{staff_note.id}", class: "edit-staff-note-link" %></li>
<% end %>
<% if staff_note.can_delete?(CurrentUser.user) %>
<% if staff_note.is_deleted? %>
<li><%= link_to "Undelete", undelete_staff_note_path(staff_note), method: :put %></li>
<% else %>
<li><%= link_to "Delete", delete_staff_note_path(staff_note), method: :put %></li>
<% end %>
<% end %>
</menu>
</div>
<% if staff_note.can_edit?(CurrentUser.user) %>
<%= render "staff_notes/partials/edit", staff_note: staff_note, hidden: true %>
<% end %>
</div>
</article>

View File

@ -0,0 +1,13 @@
<div id="c-staff-notes">
<div id="a-search">
<h1>Search Staff Notes</h1>
<%= render "search" %>
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
Search Staff Notes
<% end %>

View File

@ -0,0 +1,13 @@
<div id="c-staff-notes">
<div id="a-show">
<h1>Staff Note For <%= link_to_user @staff_note.user %></h1>
<%= render "staff_notes/partials/staff_note", staff_note: @staff_note, show_receiver_name: true %>
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
Staff Note - <%= @staff_note.user_name %>
<% end %>

View File

@ -119,6 +119,9 @@
<li><%= link_to("Stuck DNP tags", new_admin_stuck_dnp_path) %></li>
<li><%= link_to("Destroyed Posts", admin_destroyed_posts_path) %></li>
<% end %>
<% if CurrentUser.is_staff? %>
<li><%= link_to("Staff Notes", staff_notes_path) %></li>
<% end %>
<li><%= link_to("Upload Whitelist", upload_whitelists_path) %></li>
<li><%= link_to("Mod Actions", mod_actions_path) %></li>
<li><%= link_to("Bulk Update Requests", bulk_update_requests_path) %></li>

View File

@ -87,6 +87,7 @@
["Tag count", "total_tags"],
["General tag count", "general_tags", "total_tags"],
["Artist tag count", "artist_tags", "total_tags"],
["Contributor tag count", "contributor_tags", "total_tags"],
["Character tag count", "character_tags", "total_tags"],
["Copyright tag count", "copyright_tags", "total_tags"],
["Species tag count", "species_tags", "total_tags"],

View File

@ -1,7 +1,9 @@
<% content_for(:secondary_links) do %>
<%= subnav_link_to "Listing", tag_aliases_path %>
<%= subnav_link_to "Implication Listing", tag_implications_path %>
<%= subnav_link_to "BUR Listing", bulk_update_requests_path %>
<%= subnav_link_to "MetaSearch", meta_searches_tags_path %>
<%= subnav_link_to "Request implication", new_tag_implication_request_path %>
<%= subnav_link_to "Request alias", new_tag_alias_request_path %>
<%= subnav_link_to "Request bulk update", new_bulk_update_request_path %>
<%= subnav_link_to "Help", help_page_path(id: "tag_aliases") %>

View File

@ -1,8 +1,10 @@
<% content_for(:secondary_links) do %>
<%= subnav_link_to "Listing", tag_implications_path %>
<%= subnav_link_to "Alias Listing", tag_aliases_path %>
<%= subnav_link_to "BUR Listing", bulk_update_requests_path %>
<%= subnav_link_to "MetaSearch", meta_searches_tags_path %>
<%= subnav_link_to "Request implication", new_tag_implication_request_path %>
<%= subnav_link_to "Request alias", new_tag_alias_request_path %>
<%= subnav_link_to "Request bulk update", new_bulk_update_request_path %>
<%= subnav_link_to "Help", help_page_path(id: "tag_implications") %>
<% end %>

View File

@ -1,34 +1,30 @@
<div id="c-users">
<div id="a-new">
<div id="c-users"><div id="a-new">
<%= custom_form_for(@user, html: { id: "signup-form", class: "session_form" }) do |f| %>
<h1>Sign Up</h1>
<p>An account is <strong>free</strong> and lets you keep favorites, upload artwork, and write comments.</p>
<div class="box-section background-red">
<p>Make sure to read the <a href="/wiki_pages/e621:rules">site rules</a> before continuing.</p>
<p>You must confirm your email address, so use something you can receive email with.</p>
<p>This site is open to web crawlers so whatever name you choose will be public!</p>
<p>This includes favorites, uploads, and comments. Almost everything is public. So don't choose a name you don't want to be associated with.</p>
<p>Accounts are prefilled with the same blacklist as guests have. You can access your blacklist in your account settings.</p>
</div>
<div id="p3">
<%= custom_form_for(@user, html: {id: "signup-form"}) do |f| %>
<%= f.input :name, :as => :string %>
<%= f.input :email, :required => true, :as => :email %>
<%= f.input :name, label: "Username", as: :string %>
<%= f.input :email, :required => true, as: :email %>
<%= f.input :password %>
<%= f.input :password_confirmation %>
<%= f.input :time_zone, :include_blank => false %>
<%= f.input :time_zone, include_blank: false %>
<% if Danbooru.config.enable_recaptcha? %>
<%= recaptcha_tags theme: 'dark', nonce: content_security_policy_nonce %>
<%= recaptcha_tags theme: "dark", nonce: content_security_policy_nonce %>
<% end %>
<%= f.submit "Sign up", :data => { :disable_with => "Signing up..." } %>
<% end %>
<section class="session_info">
<h3>
Already have an account? <%= link_to "Sign In.", new_session_path %>
</h3>
<p>Please, read the <a href="/wiki_pages/e621:rules">site rules</a> before making an account.</p>
<p>You must confirm your email address, so you should only use one that you have access to.</p>
<p>This site is open to web crawlers, meaning that almost everything is public.</p>
<p>This includes your account name, favorites, uploads, and comments. Do not choose a name you don't want to be associated with.</p>
<p>Accounts have the same blacklist as guests by default. You will be able to modify your blacklist in the account settings.</p>
</div>
</div>
</div>
</div></div>
<%= render "secondary_links" %>

View File

@ -1,7 +1,7 @@
<div id="c-users">
<div id="a-show">
<%= render "statistics", :presenter => @presenter, :user => @user %>
<%= render "admin/staff_notes/partials/for_user", user: @user %>
<%= render "staff_notes/partials/for_user", user: @user %>
<%= render "posts/partials/common/inline_blacklist" %>
<%= render "post_summary", presenter: @presenter, user: @user %>
<%= render "about", presenter: @presenter, user: @user %>

View File

@ -25,7 +25,6 @@ Rails.application.routes.draw do
resource :reowner, controller: 'reowner', only: [:new, :create]
resource :stuck_dnp, controller: "stuck_dnp", only: %i[new create]
resources :destroyed_posts, only: %i[index show update]
resources :staff_notes, only: [:index]
end
namespace :security do
@ -95,6 +94,16 @@ Rails.application.routes.draw do
resources :avoid_posting_versions, only: %i[index]
resources :staff_notes, except: %i[destroy] do
collection do
get :search
end
member do
put :delete
put :undelete
end
end
resources :tickets, except: %i[destroy] do
member do
post :claim
@ -267,7 +276,9 @@ Rails.application.routes.draw do
end
resource :related_tag, :only => [:show, :update]
match "related_tag/bulk", to: "related_tags#bulk", via: [:get, :post]
resource :session, only: [:new, :create, :destroy]
resource :session, only: %i[new create destroy] do
get :confirm_password, on: :collection
end
resources :stats, only: [:index]
resources :tags, constraints: id_name_constraint do
resource :correction, :only => [:new, :create, :show], :controller => "tag_corrections"
@ -291,10 +302,7 @@ Rails.application.routes.draw do
resources :uploads
resources :users do
resource :password, :only => [:edit], :controller => "maintenance/user/passwords"
resource :api_key, :only => [:show, :view, :update, :destroy], :controller => "maintenance/user/api_keys" do
post :view
end
resources :staff_notes, only: [:index, :new, :create], controller: "admin/staff_notes"
resource :api_key, only: %i[show update destroy], controller: "maintenance/user/api_keys"
collection do
get :home

View File

@ -0,0 +1,7 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "config", "environment"))
client = Post.document_store.client
client.indices.put_mapping(index: Post.document_store.index_name, body: { properties: { tag_count_contributor: { type: "integer" } } })

View File

@ -0,0 +1,9 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "config", "environment"))
client = Post.document_store.client
Post.find_in_batches(batch_size: 10_000) do |posts|
client.bulk(body: posts.map { |post| { update: { _index: Post.document_store.index_name, _id: post.id, data: { doc: { tag_count_contributor: 0 } } } } }, refresh: true)
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class SoftDeletableStaffNotes < ActiveRecord::Migration[7.1]
def up
add_column :staff_notes, :is_deleted, :boolean, null: false, default: false
add_reference :staff_notes, :updater, foreign_key: { to_table: :users }, null: true
execute("UPDATE staff_notes SET updater_id = creator_id")
change_column_null :staff_notes, :updater_id, false
remove_column :staff_notes, :resolved, :boolean, null: false, default: false
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddContributorCategory < ActiveRecord::Migration[7.1]
def change
Post.without_timeout do
add_column(:posts, :tag_count_contributor, :integer, default: 0, null: false)
end
end
end

View File

@ -37,7 +37,7 @@ POSTVOTES = presets[:postvotes]
POOLS = presets[:pools]
DISTRIBUTION = ENV.fetch("DISTRIBUTION", 10).to_i
DEFAULT_PASSWORD = ENV.fetch("PASSWORD", "qwerty")
DEFAULT_PASSWORD = ENV.fetch("PASSWORD", "hexerade")
CurrentUser.user = User.system

View File

@ -9,8 +9,8 @@ require "tempfile"
admin = User.find_or_create_by!(name: "admin") do |user|
user.created_at = 2.weeks.ago
user.password = "qwerty"
user.password_confirmation = "qwerty"
user.password = "hexerade"
user.password_confirmation = "hexerade"
user.password_hash = ""
user.email = "admin@e621.local"
user.can_upload_free = true
@ -38,7 +38,7 @@ def api_request(path)
end
def import_mascots
api_request("/mascots.json?limit=1").each do |mascot|
api_request("/mascots.json?limit=3").each do |mascot|
puts mascot["url_path"]
Mascot.create!(
creator: CurrentUser.user,
@ -48,7 +48,7 @@ def import_mascots
artist_url: mascot["artist_url"],
artist_name: mascot["artist_name"],
available_on_string: Danbooru.config.app_name,
active: mascot["active"],
active: true,
)
end
end

View File

@ -1685,7 +1685,8 @@ CREATE TABLE public.posts (
duration numeric,
is_comment_disabled boolean DEFAULT false NOT NULL,
is_comment_locked boolean DEFAULT false NOT NULL,
transcript text DEFAULT ''::text NOT NULL
transcript text DEFAULT ''::text NOT NULL,
tag_count_contributor integer DEFAULT 0 NOT NULL
);
@ -1781,7 +1782,8 @@ CREATE TABLE public.staff_notes (
user_id bigint NOT NULL,
creator_id integer NOT NULL,
body character varying,
resolved boolean DEFAULT false NOT NULL
is_deleted boolean DEFAULT false NOT NULL,
updater_id bigint NOT NULL
);
@ -4230,6 +4232,13 @@ CREATE INDEX index_staff_audit_logs_on_user_id ON public.staff_audit_logs USING
CREATE INDEX index_staff_notes_on_creator_id ON public.staff_notes USING btree (creator_id);
--
-- Name: index_staff_notes_on_updater_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_staff_notes_on_updater_id ON public.staff_notes USING btree (updater_id);
--
-- Name: index_staff_notes_on_user_id; Type: INDEX; Schema: public; Owner: -
--
@ -4671,6 +4680,14 @@ ALTER TABLE ONLY public.avoid_postings
ADD CONSTRAINT fk_rails_d45cc0f1a1 FOREIGN KEY (creator_id) REFERENCES public.users(id);
--
-- Name: staff_notes fk_rails_eaa7223eea; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.staff_notes
ADD CONSTRAINT fk_rails_eaa7223eea FOREIGN KEY (updater_id) REFERENCES public.users(id);
--
-- PostgreSQL database dump complete
--
@ -4678,6 +4695,7 @@ ALTER TABLE ONLY public.avoid_postings
SET search_path TO "$user", public;
INSERT INTO "schema_migrations" (version) VALUES
('20241114055212'),
('20241110171706'),
('20241110171006'),
('20241110170952'),
@ -4693,6 +4711,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20240726170041'),
('20240709134926'),
('20240706061122'),
('20240205174652'),
('20240103002049'),
('20240103002040'),
('20240101042716'),

View File

@ -21,7 +21,8 @@
"vue": "^3.1.0",
"vue-loader": "^17.4.2",
"webpack": "^5.51.1",
"zingtouch": "^1.0.6"
"zingtouch": "^1.0.6",
"zxcvbn": "^4.4.2"
},
"version": "0.1.0",
"devDependencies": {

View File

@ -5,14 +5,14 @@ FactoryBot.define do
sequence :name do |n|
"user#{n}"
end
password { "password" }
password_confirmation { "password" }
password { "6cQE!wbA" }
password_confirmation { "6cQE!wbA" }
sequence(:email) { |n| "user_email_#{n}@example.com" }
default_image_size { "large" }
base_upload_limit { 10 }
level { 20 }
created_at {Time.now}
last_logged_in_at {Time.now}
created_at { Time.now }
last_logged_in_at { Time.now }
factory(:banned_user) do
transient { ban_duration { 3 } }

View File

@ -36,7 +36,7 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
context "on api authentication" do
setup do
@user = create(:user, password: "password")
@user = create(:user, password: "6cQE!wbA")
@api_key = ApiKey.generate!(@user)
ActionController::Base.allow_forgery_protection = true
@ -108,7 +108,7 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
token = css_select("form input[name=authenticity_token]").first["value"]
# login
post session_path, params: { authenticity_token: token, name: @user.name, password: "password" }
post session_path, params: { authenticity_token: token, session: { name: @user.name, password: "6cQE!wbA" } }
assert_redirected_to posts_path
# try to submit a form with cookies but without the csrf token
@ -122,9 +122,9 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
context "on session cookie authentication" do
should "succeed" do
user = create(:user, password: "password")
user = create(:user, password: "6cQE!wbA")
post session_path, params: { name: user.name, password: "password" }
post session_path, params: { session: { name: user.name, password: "6cQE!wbA" } }
get edit_user_path(user)
assert_response :success

View File

@ -7,62 +7,43 @@ module Maintenance
class ApiKeysControllerTest < ActionDispatch::IntegrationTest
context "An api keys controller" do
setup do
@user = create(:privileged_user, :password => "password")
ApiKey.generate!(@user)
@user = create(:privileged_user, password: "6cQE!wbA")
@api_key = ApiKey.generate!(@user)
end
context "#show" do
should "render" do
get_auth maintenance_user_api_key_path, @user, params: {user_id: @user.id}
context "show action" do
should "let a user see their own API keys" do
get_auth maintenance_user_api_key_path(@user.id), @user
assert_response :success
end
assert_select "#api-key-#{@api_key.id}", count: 1
end
context "#view" do
context "with a correct password" do
should "succeed" do
post_auth view_maintenance_user_api_key_path(user_id: @user.id), @user, params: {user: {password: "password"}}
should "not let a user see API keys belonging to other users" do
get_auth maintenance_user_api_key_path(@user.id), create(:user)
assert_response :success
assert_select "#api-key-#{@api_key.id}", count: 0
end
# hard to test this in integrationtest
# context "if the user doesn't already have an api key" do
# setup do
# ::User.any_instance.stubs(:api_key).returns(nil)
# cookies[:user_name] = @user.name
# cookies[:password_hash] = @user.bcrypt_cookie_password_hash
# end
# should "generate one" do
# ApiKey.expects(:generate!)
# assert_difference("ApiKey.count", 1) do
# post view_maintenance_user_api_key_path(user_id: @user.id), params: {user: {password: "password"}}
# end
# assert_not_nil(@user.reload.api_key)
# end
# end
should "not generate another API key if the user already has one" do
assert_difference("ApiKey.count", 0) do
post_auth view_maintenance_user_api_key_path(user_id: @user.id), @user, params: {user: {password: "password"}}
end
should "redirect to the confirm password page if the user hasn't recently authenticated" do
post session_path, params: { session: { name: @user.name, password: @user.password } }
travel_to 2.hours.from_now do
get maintenance_user_api_key_path(@user.id)
end
assert_redirected_to confirm_password_session_path(url: maintenance_user_api_key_path(@user.id))
end
end
context "#update" do
context "update action" do
should "regenerate the API key" do
old_key = @user.api_key
put_auth maintenance_user_api_key_path, @user, params: {user_id: @user.id, user: {password: "password"}}
put_auth maintenance_user_api_key_path, @user
assert_not_equal(old_key.key, @user.reload.api_key.key)
end
end
context "#destroy" do
context "destroy action" do
should "delete the API key" do
delete_auth maintenance_user_api_key_path, @user, params: {user_id: @user.id, user: {password: "password"}}
delete_auth maintenance_user_api_key_path, @user
assert_nil(@user.reload.api_key)
end
end

View File

@ -19,7 +19,7 @@ module Maintenance
context "#destroy" do
should "render" do
delete_auth maintenance_user_deletion_path, @user, params: { password: "password" }
delete_auth maintenance_user_deletion_path, @user, params: { password: "6cQE!wbA" }
assert_redirected_to(posts_path)
end
end

View File

@ -21,7 +21,7 @@ module Maintenance
context "#create" do
context "with the correct password" do
should "work" do
post_auth maintenance_user_email_change_path, @user, params: { email_change: { password: "password", email: "abc@ogres.net" } }
post_auth maintenance_user_email_change_path, @user, params: { email_change: { password: "6cQE!wbA", email: "abc@ogres.net" } }
assert_redirected_to(home_users_path)
@user.reload
assert_equal("abc@ogres.net", @user.email)
@ -37,7 +37,7 @@ module Maintenance
end
should "not work with an invalid email" do
post_auth maintenance_user_email_change_path, @user, params: { email_change: { password: "password", email: "" } }
post_auth maintenance_user_email_change_path, @user, params: { email_change: { password: "6cQE!wbA", email: "" } }
@user.reload
assert_not_equal("", @user.email)
assert_match(/Email can't be blank/, flash[:notice])
@ -45,7 +45,7 @@ module Maintenance
should "work with a valid email when the users current email is invalid" do
@user = create(:user, email: "")
post_auth maintenance_user_email_change_path, @user, params: { email_change: { password: "password", email: "abc@ogres.net" } }
post_auth maintenance_user_email_change_path, @user, params: { email_change: { password: "6cQE!wbA", email: "abc@ogres.net" } }
@user.reload
assert_equal("abc@ogres.net", @user.email)
end

View File

@ -15,7 +15,7 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
should "create a new session" do
user = create(:user)
post session_path, params: { name: user.name, password: "password" }
post session_path, params: { session: { name: user.name, password: "6cQE!wbA" } }
user.reload
assert_redirected_to(posts_path)
@ -33,8 +33,8 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
end
should "fail when provided an invalid password" do
user = create(:user, password: "xxxxxx", password_confirmation: "xxxxxx")
post session_path, params: { name: user.name, password: "yyy" }
user = create(:user, password: "6cQE!wbA", password_confirmation: "6cQE!wbA")
post session_path, params: { session: { name: user.name, password: "yyy" } }
assert_nil(session[:user_id])
assert_equal("Username/Password was incorrect", flash[:notice])
@ -45,7 +45,7 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
should "clear the session" do
user = create(:user)
post session_path, params: { name: user.name, password: "password" }
post session_path, params: { session: { name: user.name, password: "6cQE!wbA" } }
assert_not_nil(session[:user_id])
delete_auth(session_path, user)

View File

@ -93,7 +93,7 @@ class UserFeedbacksControllerTest < ActionDispatch::IntegrationTest
end
should "delete a feedback" do
assert_difference({ "UserFeedback.count" => -1, "ModAction.count" => 1 }) do
assert_difference({ "UserFeedback.count" => -1, "ModAction.count" => 2 }) do
delete_auth user_feedback_path(@user_feedback), @critic
end
end
@ -101,7 +101,7 @@ class UserFeedbacksControllerTest < ActionDispatch::IntegrationTest
context "by a moderator" do
should "allow destroying feedbacks they created" do
as(@mod) { @user_feedback = create(:user_feedback, user: @user) }
assert_difference({ "UserFeedback.count" => -1, "ModAction.count" => 1 }) do
assert_difference({ "UserFeedback.count" => -1, "ModAction.count" => 2 }) do
delete_auth user_feedback_path(@user_feedback), @mod
end
end
@ -126,13 +126,13 @@ class UserFeedbacksControllerTest < ActionDispatch::IntegrationTest
context "by an admin" do
should "allow destroying feedbacks they created" do
as(@admin) { @user_feedback = create(:user_feedback, user: @user) }
assert_difference({ "UserFeedback.count" => -1, "ModAction.count" => 1 }) do
assert_difference({ "UserFeedback.count" => -1, "ModAction.count" => 2 }) do
delete_auth user_feedback_path(@user_feedback), @admin
end
end
should "allow destroying feedbacks they did not create" do
assert_difference({ "UserFeedback.count" => -1, "ModAction.count" => 1 }) do
assert_difference({ "UserFeedback.count" => -1, "ModAction.count" => 2 }) do
delete_auth user_feedback_path(@user_feedback, format: :json), @admin
end
end

View File

@ -76,7 +76,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
context "create action" do
should "create a user" do
assert_difference(-> { User.count }, 1) do
post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" } }
post users_path, params: { user: { name: "xxx", password: "nePD.3L4", password_confirmation: "nePD.3L4" } }
end
created_user = User.find(session[:user_id])
assert_equal("xxx", created_user.name)

View File

@ -95,7 +95,7 @@ end
class ActionDispatch::IntegrationTest
def method_authenticated(method_name, url, user, options)
post session_path, params: { name: user.name, password: user.password }
post session_path, params: { session: { name: user.name, password: user.password } }
self.send(method_name, url, **options)
end

View File

@ -22,7 +22,7 @@ class UserDeletionTest < ActiveSupport::TestCase
setup do
@user = create(:admin_user)
CurrentUser.user = @user
@deletion = UserDeletion.new(@user, "password")
@deletion = UserDeletion.new(@user, "6cQE!wbA")
end
should "fail" do
@ -43,7 +43,7 @@ class UserDeletionTest < ActiveSupport::TestCase
@user.update(email: "ted@danbooru.com")
@deletion = UserDeletion.new(@user, "password")
@deletion = UserDeletion.new(@user, "6cQE!wbA")
with_inline_jobs { @deletion.delete! }
@user.reload
end
@ -58,7 +58,7 @@ class UserDeletionTest < ActiveSupport::TestCase
should "reset the password" do
assert_raises(BCrypt::Errors::InvalidHash) do
User.authenticate(@user.name, "password")
User.authenticate(@user.name, "6cQE!wbA")
end
end

View File

@ -116,8 +116,8 @@ class UserTest < ActiveSupport::TestCase
end
should "authenticate" do
assert(User.authenticate(@user.name, "password"), "Authentication should have succeeded")
assert(!User.authenticate(@user.name, "password2"), "Authentication should not have succeeded")
assert(User.authenticate(@user.name, "6cQE!wbA"), "Authentication should have succeeded")
assert_not(User.authenticate(@user.name, "password2"), "Authentication should not have succeeded")
end
should "normalize its level" do
@ -209,8 +209,8 @@ class UserTest < ActiveSupport::TestCase
should "fail if the confirmation does not match" do
@user = create(:user)
@user.password = "zugzug6"
@user.password_confirmation = "zugzug5"
@user.password = "6cQE!wbA"
@user.password_confirmation = "7cQE!wbA"
@user.save
assert_equal(["Password confirmation doesn't match Password"], @user.errors.full_messages)
end
@ -220,7 +220,15 @@ class UserTest < ActiveSupport::TestCase
@user.password = "x5"
@user.password_confirmation = "x5"
@user.save
assert_equal(["Password is too short (minimum is 6 characters)"], @user.errors.full_messages)
assert_equal(["Password is too short (minimum is 8 characters)", "Password is insecure"], @user.errors.full_messages)
end
should "not be insecure" do
@user = create(:user)
@user.password = "qwerty123"
@user.password_confirmation = "qwerty123"
@user.save
assert_equal(["Password is insecure: This is similar to a commonly used password"], @user.errors.full_messages)
end
# should "not change the password if the password and old password are blank" do

View File

@ -3716,3 +3716,8 @@ zingtouch@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/zingtouch/-/zingtouch-1.0.6.tgz#456cf2b0a69f91a5ffbd8a83b18033c671f1096d"
integrity sha512-S7jcR7cSRy28VmQBO0Tq7ZJV4pzfvvrTU9FrrL0K1QPpfBal9wm0oKhoCuifc+PPCq+hQMTJr5E9XKUQDm00VA==
zxcvbn@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30"
integrity sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==