Rework the rate limit implementation to make it more flexible: * Allow setting different rate limits for different actions. Before we had a single rate limit for all write actions. Now different controller endpoints can have different limits. * Allow actions to be rate limited by user ID, by IP address, or both. Before actions were only limited by user ID, which meant non-logged-in actions like creating new accounts or attempting to login couldn't be rate limited. Also, because actions were limited by user ID only, you could use multiple accounts with the same IP to get around limits. Other changes: * Remove the API Limit field from user profile pages. * Remove the `remaining_api_limit` field from the `/profile.json` endpoint. * Rename the `X-Api-Limit` header to `X-Rate-Limit` and change it from a number to a JSON object containing all the rate limit info (including the refill rate, the burst factor, the cost of the call, and the current limits). * Fix a potential race condition where, if you flooded requests fast enough, you could exceed the rate limit. This was because we checked and updated the rate limit in two separate steps, which was racy; simultaneous requests could pass the check before the update happened. The new code uses some tricky SQL to check and update multiple limits in a single statement.
272 lines
8.2 KiB
Plaintext
272 lines
8.2 KiB
Plaintext
<div>
|
|
<h2>Statistics</h2>
|
|
<table width="100%" class="user-statistics table-sm">
|
|
<tbody>
|
|
<tr>
|
|
<th>User ID</th>
|
|
<td><%= user.id %></td>
|
|
</tr>
|
|
<tr>
|
|
<th>Join Date</th>
|
|
<td><%= presenter.join_date %></td>
|
|
</tr>
|
|
|
|
<% if policy(User).can_see_last_logged_in_at? %>
|
|
<tr>
|
|
<th>Last Seen</th>
|
|
<td><%= time_ago_in_words_tagged(user.last_logged_in_at) %></td>
|
|
</tr>
|
|
<% end %>
|
|
|
|
<% if policy(IpAddress).show? %>
|
|
<tr>
|
|
<th>Last IP</th>
|
|
<td>
|
|
<% if user.last_ip_addr.present? %>
|
|
<%= link_to user.last_ip_addr, ip_addresses_path(search: { ip_addr: user.last_ip_addr }) %>
|
|
(<%= link_to "info", ip_address_path(user.last_ip_addr) %>,
|
|
<%= link_to "users", ip_addresses_path(search: { ip_addr: user.last_ip_addr, group_by: "user" }) %>,
|
|
<%= link_to "IPs", ip_addresses_path(search: { user_id: user.id, group_by: "ip_addr" }) %>)
|
|
<% else %>
|
|
<em>unknown</em>
|
|
(<%= link_to "IPs", ip_addresses_path(search: { user_id: user.id, group_by: "ip_addr" }) %>)
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
|
|
<% if policy(user.email_address).show? %>
|
|
<tr class="user-email-address">
|
|
<th>Email Address</th>
|
|
<td>
|
|
<% if user.email_address.present? %>
|
|
<%= user.email_address.address %>
|
|
|
|
<% if user == CurrentUser.user %>
|
|
(<%= link_to "edit", edit_user_email_path(user) %>)
|
|
<% end %>
|
|
|
|
<% if user.email_address.is_verified? %>
|
|
<%= checkmark_icon(class: "user-verified-email-icon", title: "Verified email") %>
|
|
<% elsif user == CurrentUser.user %>
|
|
<%= link_to verify_user_email_path(user) do %>
|
|
<%= exclamation_icon(class: "user-unverified-email-icon", title: "Unverified email. Click here to verify your email.") %>
|
|
<% end %>
|
|
<% else %>
|
|
<%= exclamation_icon(class: "user-unverified-email-icon", title: "Unverified email") %>
|
|
<% end %>
|
|
<% else %>
|
|
<em>none</em>
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
|
|
<tr>
|
|
<th>Inviter</th>
|
|
<% if user.inviter %>
|
|
<td><%= link_to_user user.inviter %> <%= link_to "»", users_path(search: { inviter: { name: user.inviter.name }}) %></td>
|
|
<% else %>
|
|
<td>None</td>
|
|
<% end %>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Level</th>
|
|
<td>
|
|
<%= user.level_string %>
|
|
|
|
<% if !user.is_platinum? %>
|
|
<% if CurrentUser.user == user %>
|
|
(<%= link_to "Upgrade account", new_user_upgrade_path %>)
|
|
<% else %>
|
|
(<%= link_to "Gift upgrade", new_user_upgrade_path(user_id: user.id) %>)
|
|
<% end %>
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Permissions</th>
|
|
<td><%= presenter.permissions %></td>
|
|
</tr>
|
|
|
|
<% if user.is_banned? && user.recent_ban %>
|
|
<tr>
|
|
<th>Ban reason</th>
|
|
<td>
|
|
<span class="prose">
|
|
<%= format_text presenter.ban_reason, inline: true %>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
|
|
<tr>
|
|
<th>Upload Limit</th>
|
|
<td>
|
|
<%= render "users/upload_limit", user: user %>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Uploads</th>
|
|
<td>
|
|
<%= presenter.upload_count(self) %>
|
|
<% if presenter.has_uploads? %>
|
|
(<%= link_to "tag changes report", post_versions_path(search: { updater_id: user.id, version: 1 }, type: "current") %>)
|
|
<% end %>
|
|
<% if CurrentUser.is_moderator? %>
|
|
[<%= link_to "sample", posts_path(:tags => "user:#{user.name} order:random limit:#{PostSets::Post::MAX_PER_PAGE}") %>]
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Deleted Uploads</th>
|
|
<td>
|
|
<%= presenter.deleted_upload_count(self) %>
|
|
<% if CurrentUser.is_moderator? %>
|
|
[<%= link_to "sample", posts_path(:tags => "user:#{user.name} order:random limit:#{PostSets::Post::MAX_PER_PAGE} status:deleted") %>]
|
|
<% end %>
|
|
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Favorites</th>
|
|
<td>
|
|
<%= presenter.favorite_count(self) %>
|
|
<% if CurrentUser.is_moderator? %>
|
|
[<%= link_to "sample", posts_path(:tags => "fav:#{user.name} order:random limit:#{PostSets::Post::MAX_PER_PAGE}") %>]
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
|
|
<% if CurrentUser.user == user || CurrentUser.user.is_admin? %>
|
|
<tr>
|
|
<th>Votes</th>
|
|
<td>
|
|
<%= link_to user.post_votes.count, post_votes_path(search: { user_name: user.name }) %> posts,
|
|
<%= link_to user.comment_votes.count, comment_votes_path(search: { user_name: user.name }) %> comments,
|
|
<%= link_to user.forum_post_votes.count, forum_post_votes_path(search: { creator_name: user.name }) %> forum posts
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
|
|
<tr>
|
|
<th>Favorite Groups</th>
|
|
<td><%= presenter.favorite_group_count(self) %></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Post Changes</th>
|
|
<td>
|
|
<%= presenter.post_version_count(self) %>
|
|
<% if CurrentUser.id == user.id %>
|
|
(<%= link_to "refresh", new_maintenance_user_count_fixes_path %>)
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Note Changes</th>
|
|
<td>
|
|
<%= presenter.note_version_count(self) %> in <%= presenter.noted_posts_count(self) %> posts
|
|
(<%= link_to "note changes report", note_versions_path(search: { updater_id: user.id, version: 1 }, type: "current") %>)
|
|
</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Wiki Page Changes</th>
|
|
<td><%= presenter.wiki_page_version_count(self) %></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Artist Changes</th>
|
|
<td><%= presenter.artist_version_count(self) %></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Commentary Changes</th>
|
|
<td><%= presenter.artist_commentary_version_count(self) %></td>
|
|
</tr>
|
|
|
|
<% if PoolVersion.enabled? %>
|
|
<tr>
|
|
<th>Pool Changes</th>
|
|
<td><%= presenter.pool_version_count(self) %></td>
|
|
</tr>
|
|
<% end %>
|
|
|
|
<tr>
|
|
<th>Forum Posts</th>
|
|
<td><%= presenter.forum_post_count(self) %></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Approvals</th>
|
|
<td><%= presenter.approval_count(self) %></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Comments</th>
|
|
<td><%= presenter.comment_count(self) %> in <%= presenter.commented_posts_count(self) %> posts</td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<th>Appeals</th>
|
|
<td><%= presenter.appeal_count(self) %></td>
|
|
</tr>
|
|
|
|
<% if CurrentUser.user.id == user.id || CurrentUser.is_moderator? %>
|
|
<tr>
|
|
<th>Flags</th>
|
|
<td><%= presenter.flag_count(self) %></td>
|
|
</tr>
|
|
<% end %>
|
|
|
|
<tr>
|
|
<th>Feedback</th>
|
|
<td><%= presenter.feedbacks(self) %></td>
|
|
</tr>
|
|
|
|
<% if policy(UserNameChangeRequest.new(user: user)).show? %>
|
|
<% user.user_name_change_requests.visible(CurrentUser.user).tap do |changes| %>
|
|
<% if changes.present? %>
|
|
<tr>
|
|
<th>Previous Names</th>
|
|
<td>
|
|
<%= changes.map do |change| %>
|
|
<% link_to change.original_name, change %>
|
|
<% end.join(", ").html_safe %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
<% end %>
|
|
<% end %>
|
|
|
|
<% if CurrentUser.id == user.id %>
|
|
<% if SavedSearch.labels_for(CurrentUser.user.id).present? %>
|
|
<tr>
|
|
<th>Saved Searches</th>
|
|
<td>
|
|
<% SavedSearch.labels_for(CurrentUser.user.id).each do |label| %>
|
|
<%= link_to label, posts_path(tags: "search:#{label}") %>
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
|
|
<tr>
|
|
<th>API Key</th>
|
|
<td>
|
|
<%= link_to "View", user_api_keys_path(CurrentUser.user) %>
|
|
(<%= link_to_wiki "help", "help:api" %>)
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|