Merge main into mod-panel

This commit is contained in:
jvyden 2022-08-05 17:02:19 -04:00
commit b6da930e20
No known key found for this signature in database
GPG key ID: 18BCF2BE0262B278
300 changed files with 8417 additions and 700 deletions

View file

@ -43,13 +43,14 @@ public class LandingPage : BaseLayout
const int maxShownLevels = 5;
this.LatestTeamPicks = await this.Database.Slots.Where
(s => s.TeamPick)
(s => s.Type == SlotType.User)
.Where(s => s.TeamPick)
.OrderByDescending(s => s.FirstUploaded)
.Take(maxShownLevels)
.Include(s => s.Creator)
.ToListAsync();
this.NewestLevels = await this.Database.Slots.OrderByDescending(s => s.FirstUploaded).Take(maxShownLevels).Include(s => s.Creator).ToListAsync();
this.NewestLevels = await this.Database.Slots.Where(s => s.Type == SlotType.User).OrderByDescending(s => s.FirstUploaded).Take(maxShownLevels).Include(s => s.Creator).ToListAsync();
return this.Page();
}

View file

@ -127,28 +127,36 @@
<div class="ui container">
<div style="display: flex; align-items: center; font-size: 1.2rem;">
<i class="warning icon"></i>
<span style="font-size: 1.2rem;">JavaScript not enabled</span>
<span style="font-size: 1.2rem;">@Model.Translate(BaseLayoutStrings.JavaScriptWarnTitle)</span>
</div>
<p>
While we intend to have as little JavaScript as possible, we can not
guarantee everything will work without it. We recommend that you whitelist JavaScript for Project Lighthouse.
It's not <i>too</i> bloated, we promise.
</p>
<p>@Model.Translate(BaseLayoutStrings.JavaScriptWarn)</p>
</div>
</div>
</noscript>
@* ReSharper disable HeuristicUnreachableCode *@
@* ReSharper disable CSharpWarnings::CS0162 *@
@if (!ServerStatics.IsDebug && VersionHelper.IsDirty)
{
<div class="ui bottom attached red message large">
<div class="ui container">
<i class="warning icon"></i>
<span style="font-size: 1.2rem;">Potential License Violation</span>
<p>This instance is a public-facing instance that has been modified without the changes published. You may be in violation of the <a href="https://github.com/LBPUnion/project-lighthouse/blob/main/LICENSE">GNU Affero General Public License v3.0</a>.</p>
<p>If you believe this is an error, please create an issue with the output of <code>git status</code> ran from the root of the server source code in the description on our <a href="https://github.com/LBPUnion/project-lighthouse/issues">issue tracker</a>.</p>
<p>If not, please publish the source code somewhere accessible to your users.</p>
<span style="font-size: 1.2rem;">@Model.Translate(BaseLayoutStrings.LicenseWarnTitle)</span>
<p>
@Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn1,
"<a href=\"https://github.com/LBPUnion/project-lighthouse/blob/main/LICENSE\">GNU Affero General Public License v3.0</a>"))
</p>
<p>
@Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn2,
"<code>git status</code>", "<a href=\"https://github.com/LBPUnion/project-lighthouse/issues\">", "</a>"))
</p>
<p>
@Html.Raw(Model.Translate(BaseLayoutStrings.LicenseWarn3))
</p>
</div>
</div>
}
@* ReSharper restore CSharpWarnings::CS0162 *@
@* ReSharper restore HeuristicUnreachableCode *@
</header>
<div class="main">
<div class="ui container">
@ -165,10 +173,10 @@
<div class="ui black attached inverted segment">
<div class="ui container">
<p>Page generated by @VersionHelper.FullVersion.</p>
<p>@Model.Translate(BaseLayoutStrings.GeneratedBy, VersionHelper.FullVersion)</p>
@if (VersionHelper.IsDirty)
{
<p>This page was generated using a modified version of Project Lighthouse. Please make sure you are properly disclosing the source code to any users who may be using this instance.</p>
<p>@Model.Translate(BaseLayoutStrings.GeneratedModified)</p>
}
</div>
</div>

View file

@ -52,6 +52,8 @@ public class BaseLayout : PageModel
{
if (ServerStatics.IsUnitTesting) return LocalizationManager.DefaultLang;
if (this.language != null) return this.language;
if (this.User?.IsAPirate ?? false) return "en-PT";
IRequestCultureFeature? requestCulture = Request.HttpContext.Features.Get<IRequestCultureFeature>();
if (requestCulture == null) return this.language = LocalizationManager.DefaultLang;

View file

@ -3,6 +3,7 @@ using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Localization.StringLists;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
@ -26,19 +27,19 @@ public class LoginForm : BaseLayout
{
if (string.IsNullOrWhiteSpace(username))
{
this.Error = "The username field is required.";
this.Error = this.Translate(ErrorStrings.UsernameInvalid);
return this.Page();
}
if (string.IsNullOrWhiteSpace(password))
{
this.Error = "The password field is required.";
this.Error = this.Translate(ErrorStrings.PasswordInvalid);
return this.Page();
}
if (!await this.Request.CheckCaptchaValidity())
{
this.Error = "You must complete the captcha correctly.";
this.Error = this.Translate(ErrorStrings.CaptchaFailed);
return this.Page();
}
@ -60,7 +61,7 @@ public class LoginForm : BaseLayout
if (user.IsBanned)
{
Logger.Warn($"User {user.Username} (id: {user.UserId}) failed to login on web due to being banned", LogArea.Login);
this.Error = "You have been banned. Please contact an administrator for more information.\nReason: " + user.BannedReason;
this.Error = this.Translate(ErrorStrings.UserIsBanned, user.BannedReason);
return this.Page();
}
@ -73,6 +74,7 @@ public class LoginForm : BaseLayout
UserId = user.UserId,
User = user,
EmailToken = CryptoHelper.GenerateAuthToken(),
ExpiresAt = DateTime.Now + TimeSpan.FromHours(6),
};
this.Database.EmailSetTokens.Add(emailSetToken);
@ -85,6 +87,7 @@ public class LoginForm : BaseLayout
{
UserId = user.UserId,
UserToken = CryptoHelper.GenerateAuthToken(),
ExpiresAt = DateTime.Now + TimeSpan.FromDays(7),
};
this.Database.WebTokens.Add(webToken);

View file

@ -52,7 +52,7 @@
string style = "";
if (Model.User?.UserId == comment.PosterUserId)
{
style = "visibility: hidden";
style = "pointer-events: none";
}
}
<div class="voting" style="@(style)">

View file

@ -1,5 +1,6 @@
@using System.Globalization
@using LBPUnion.ProjectLighthouse.Levels
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.PlayerData.Photo
@ -18,6 +19,27 @@
<b>
<a href="/user/@Model.Creator?.UserId">@Model.Creator?.Username</a>
</b>
@if (Model.Slot != null)
{
switch (Model.Slot.Type)
{
case SlotType.User:
<span>
in level <b><a href="/slot/@Model.SlotId">@Model.Slot.Name</a></b>
</span>
break;
case SlotType.Developer:
<span>in a story mode level</span>
break;
case SlotType.Pod:
<span>in the pod</span>
break;
case SlotType.Local:
<span>in a level on the moon</span>
break;
}
}
at @DateTime.UnixEpoch.AddSeconds(Model.Timestamp).ToString(CultureInfo.CurrentCulture)
</i>
</p>
@ -124,4 +146,4 @@
context.setTransform(1, 0, 0, 1, 0, 0);
})
}, false);
</script>
</script>

View file

@ -1,4 +1,5 @@
@page "/passwordReset"
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PasswordResetPage
@{
@ -26,7 +27,7 @@
{
<div class="ui negative message">
<div class="header">
Uh oh!
@Model.Translate(GeneralStrings.Error)
</div>
<p>@Model.Error</p>
</div>

View file

@ -1,4 +1,5 @@
@page "/passwordResetRequest"
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PasswordResetRequestForm
@{
@ -10,7 +11,7 @@
{
<div class="ui negative message">
<div class="header">
Uh oh!
@Model.Translate(GeneralStrings.Error)
</div>
<p style="white-space: pre-line">@Model.Error</p>
</div>
@ -20,7 +21,7 @@
{
<div class="ui positive message">
<div class="header">
Success!
@Model.Translate(GeneralStrings.Success)
</div>
<p style="white-space: pre-line">@Model.Status</p>
</div>

View file

@ -40,6 +40,7 @@ public class PhotosPage : BaseLayout
this.Photos = await this.Database.Photos.Include
(p => p.Creator)
.Include(p => p.Slot)
.Where(p => p.Creator!.Username.Contains(this.SearchValue) || p.PhotoSubjectCollection.Contains(this.SearchValue))
.OrderByDescending(p => p.Timestamp)
.Skip(pageNumber * ServerStatics.PageSize)

View file

@ -0,0 +1,31 @@
@page "/pirate"
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.PirateSignupPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "ARRRRRRRRRR!";
}
<!--suppress GrazieInspection -->
@if (!Model.User!.IsAPirate)
{
<p>So, ye wanna be a pirate? Well, ye came to the right place!</p>
<p>Just click this 'ere button, and welcome aboard!</p>
<p>If you ever wanna walk the plank, come back 'ere.</p>
<form method="post">
@Html.AntiForgeryToken()
<input type="submit" class="ui blue button" value="Aye aye, captain!"/>
</form>
}
else
{
<p>Back so soon, aye?</p>
<p>If you're gonna walk the plank, then do it!</p>
<form method="post">
@Html.AntiForgeryToken()
<input type="submit" class="ui red button" value="Walk the plank"/>
</form>
}

View file

@ -0,0 +1,32 @@
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages;
public class PirateSignupPage : BaseLayout
{
public PirateSignupPage(Database database) : base(database)
{}
public async Task<IActionResult> OnGet()
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.RedirectToPage("/login");
return this.Page();
}
public async Task<IActionResult> OnPost()
{
User? user = this.Database.UserFromWebRequest(this.Request);
if (user == null) return this.Redirect("/login");
user.IsAPirate = !user.IsAPirate;
await this.Database.SaveChangesAsync();
return this.Redirect("/");
}
}

View file

@ -1,5 +1,6 @@
@page "/register"
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.RegisterForm
@{
@ -26,12 +27,14 @@
{
<div class="ui negative message">
<div class="header">
Uh oh!
@Model.Translate(GeneralStrings.Error)
</div>
<p>@Model.Error</p>
</div>
}
<p><b>@Model.Translate(RegisterStrings.UsernameNotice)</b></p>
<form class="ui form" onsubmit="return onSubmit(this)" method="post">
@Html.AntiForgeryToken()

View file

@ -3,6 +3,7 @@ using JetBrains.Annotations;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Localization.StringLists;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
@ -28,7 +29,7 @@ public class RegisterForm : BaseLayout
if (this.Request.Query.ContainsKey("token"))
{
if (!this.Database.IsRegistrationTokenValid(this.Request.Query["token"]))
return this.StatusCode(403, "Invalid Token");
return this.StatusCode(403, this.Translate(ErrorStrings.TokenInvalid));
}
else
{
@ -42,44 +43,44 @@ public class RegisterForm : BaseLayout
if (string.IsNullOrWhiteSpace(username))
{
this.Error = "The username field is blank.";
this.Error = this.Translate(ErrorStrings.UsernameInvalid);
return this.Page();
}
if (string.IsNullOrWhiteSpace(password))
{
this.Error = "Password field is required.";
this.Error = this.Translate(ErrorStrings.PasswordInvalid);
return this.Page();
}
if (string.IsNullOrWhiteSpace(emailAddress) && ServerConfiguration.Instance.Mail.MailEnabled)
{
this.Error = "Email address field is required.";
this.Error = this.Translate(ErrorStrings.EmailInvalid);
return this.Page();
}
if (password != confirmPassword)
{
this.Error = "Passwords do not match!";
this.Error = this.Translate(ErrorStrings.PasswordDoesntMatch);
return this.Page();
}
if (await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower()) != null)
{
this.Error = "The username you've chosen is already taken.";
this.Error = this.Translate(ErrorStrings.UsernameTaken);
return this.Page();
}
if (ServerConfiguration.Instance.Mail.MailEnabled &&
await this.Database.Users.FirstOrDefaultAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == emailAddress.ToLower()) != null)
{
this.Error = "The email address you've chosen is already taken.";
this.Error = this.Translate(ErrorStrings.EmailTaken);
return this.Page();
}
if (!await this.Request.CheckCaptchaValidity())
{
this.Error = "You must complete the captcha correctly.";
this.Error = this.Translate(ErrorStrings.CaptchaFailed);
return this.Page();
}
@ -94,6 +95,7 @@ public class RegisterForm : BaseLayout
{
UserId = user.UserId,
UserToken = CryptoHelper.GenerateAuthToken(),
ExpiresAt = DateTime.Now + TimeSpan.FromDays(7),
};
this.Database.WebTokens.Add(webToken);
@ -116,7 +118,7 @@ public class RegisterForm : BaseLayout
if (this.Request.Query.ContainsKey("token"))
{
if (!this.Database.IsRegistrationTokenValid(this.Request.Query["token"]))
return this.StatusCode(403, "Invalid Token");
return this.StatusCode(403, this.Translate(ErrorStrings.TokenInvalid));
}
else
{

View file

@ -13,7 +13,7 @@ public class ReportPage : BaseLayout
public ReportPage(Database database) : base(database)
{}
public GriefReport Report;
public GriefReport Report = null!; // Report is not used if it's null in OnGet
public async Task<IActionResult> OnGet([FromRoute] int reportId)
{

View file

@ -49,15 +49,18 @@ public class SetEmailForm : BaseLayout
UserId = user.UserId,
User = user,
EmailToken = CryptoHelper.GenerateAuthToken(),
ExpiresAt = DateTime.Now + TimeSpan.FromHours(6),
};
this.Database.EmailVerificationTokens.Add(emailVerifyToken);
// The user just set their email address. Now, let's grant them a token to proceed with verifying the email.
// TODO: insecure
WebToken webToken = new()
{
UserId = user.UserId,
UserToken = CryptoHelper.GenerateAuthToken(),
ExpiresAt = DateTime.Now + TimeSpan.FromDays(7),
};
this.Response.Cookies.Append

View file

@ -3,6 +3,7 @@
@using LBPUnion.ProjectLighthouse.Administration
@using LBPUnion.ProjectLighthouse.Configuration
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.PlayerData.Reviews
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SlotPage
@ -163,6 +164,22 @@
</div>
</div>
@if (Model.Photos.Count != 0)
{
<div class="ui purple segment">
<h2>Most recent photos</h2>
<div class="ui center aligned grid">
@foreach (Photo photo in Model.Photos)
{
<div class="eight wide column">
@await Html.PartialAsync("Partials/PhotoPartial", photo)
</div>
}
</div>
</div>
}
@if (Model.User != null && Model.User.IsModerator)
{
<div class="ui green segment">

View file

@ -15,6 +15,7 @@ public class SlotPage : BaseLayout
{
public List<Comment> Comments = new();
public List<Review> Reviews = new();
public List<Photo> Photos = new();
public readonly bool CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled;
public readonly bool ReviewsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelReviewsEnabled;
@ -25,8 +26,34 @@ public class SlotPage : BaseLayout
public async Task<IActionResult> OnGet([FromRoute] int id)
{
Slot? slot = await this.Database.Slots.Include(s => s.Creator).FirstOrDefaultAsync(s => s.SlotId == id);
Slot? slot = await this.Database.Slots.Include
(s => s.Creator)
.Where(s => s.Type == SlotType.User)
.FirstOrDefaultAsync(s => s.SlotId == id);
if (slot == null) return this.NotFound();
System.Diagnostics.Debug.Assert(slot.Creator != null);
// Determine if user can view slot according to creator's privacy settings
if (this.User == null || !this.User.IsAdmin)
{
switch (slot.Creator.ProfileVisibility)
{
case PrivacyType.PSN:
{
if (this.User != null) return this.NotFound();
break;
}
case PrivacyType.Game:
{
if (slot.Creator != this.User) return this.NotFound();
break;
}
case PrivacyType.All: break;
default: throw new ArgumentOutOfRangeException();
}
}
this.Slot = slot;
@ -57,6 +84,12 @@ public class SlotPage : BaseLayout
this.Reviews = new List<Review>();
}
this.Photos = await this.Database.Photos.Include(p => p.Creator)
.OrderByDescending(p => p.Timestamp)
.Where(r => r.SlotId == id)
.Take(10)
.ToListAsync();
if (this.User == null) return this.Page();
foreach (Comment c in this.Comments)

View file

@ -3,6 +3,7 @@ using System.Text;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Levels;
using LBPUnion.ProjectLighthouse.PlayerData;
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types;
using Microsoft.AspNetCore.Mvc;
@ -55,6 +56,7 @@ public class SlotsPage : BaseLayout
this.SearchValue = name.Trim();
this.SlotCount = await this.Database.Slots.Include(p => p.Creator)
.Where(p => p.Type == SlotType.User)
.Where(p => p.Name.Contains(finalSearch.ToString()))
.Where(p => p.Creator != null && (targetAuthor == null || string.Equals(p.Creator.Username.ToLower(), targetAuthor.ToLower())))
.Where(p => targetGame == null || p.GameVersion == targetGame)
@ -66,8 +68,10 @@ public class SlotsPage : BaseLayout
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/slots/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Slots = await this.Database.Slots.Include(p => p.Creator)
.Where(p => p.Type == SlotType.User)
.Where(p => p.Name.Contains(finalSearch.ToString()))
.Where(p => p.Creator != null && (targetAuthor == null || string.Equals(p.Creator.Username.ToLower(), targetAuthor.ToLower())))
.Where(p => p.Creator!.LevelVisibility == PrivacyType.All) // TODO: change check for when user is logged in
.Where(p => targetGame == null || p.GameVersion == targetGame)
.OrderByDescending(p => p.FirstUploaded)
.Skip(pageNumber * ServerStatics.PageSize)

View file

@ -1,6 +1,7 @@
@page "/user/{userId:int}"
@using System.Web
@using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@using LBPUnion.ProjectLighthouse.PlayerData
@using LBPUnion.ProjectLighthouse.Types
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UserPage
@ -9,7 +10,7 @@
Layout = "Layouts/BaseLayout";
Model.ShowTitleInPage = false;
Model.Title = Model.ProfileUser!.Username + "'s user page";
Model.Title = Model.Translate(ProfileStrings.Title, Model.ProfileUser!.Username);
Model.Description = Model.ProfileUser!.Biography;
}
@ -74,16 +75,16 @@
{
<a class="ui blue button" href="/passwordReset">
<i class="key icon"></i>
<span>Reset Password</span>
<span>@Model.Translate(GeneralStrings.ResetPassword)</span>
</a>
}
</div>
<div class="eight wide column">
<div class="ui blue segment">
<h2>Biography</h2>
<h2>@Model.Translate(ProfileStrings.Biography)</h2>
@if (string.IsNullOrWhiteSpace(Model.ProfileUser.Biography))
{
<p>@Model.ProfileUser.Username hasn't introduced themselves yet</p>
<p>@Model.Translate(ProfileStrings.NoBiography, Model.ProfileUser.Username)</p>
}
else
{
@ -93,8 +94,8 @@
</div>
<div class="eight wide column">
<div class="ui red segment">
<h2>Recent Activity</h2>
<p>Coming soon?</p>
<h2>@Model.Translate(GeneralStrings.RecentActivity)</h2>
<p>@Model.Translate(GeneralStrings.Soon)</p>
</div>
</div>
</div>
@ -103,7 +104,7 @@
@if (Model.Photos != null && Model.Photos.Count != 0)
{
<div class="ui purple segment">
<h2>Most recent photos</h2>
<h2>@Model.Translate(GeneralStrings.RecentPhotos)</h2>
<div class="ui center aligned grid">
@foreach (Photo photo in Model.Photos)

View file

@ -28,7 +28,29 @@ public class UserPage : BaseLayout
this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId);
if (this.ProfileUser == null) return this.NotFound();
this.Photos = await this.Database.Photos.OrderByDescending(p => p.Timestamp).Where(p => p.CreatorId == userId).Take(6).ToListAsync();
// Determine if user can view profile according to profileUser's privacy settings
if (this.User == null || !this.User.IsAdmin)
{
switch (this.ProfileUser.ProfileVisibility)
{
case PrivacyType.PSN:
{
if (this.User != null) return this.NotFound();
break;
}
case PrivacyType.Game:
{
if (this.ProfileUser != this.User) return this.NotFound();
break;
}
case PrivacyType.All: break;
default: throw new ArgumentOutOfRangeException();
}
}
this.Photos = await this.Database.Photos.Include(p => p.Slot).OrderByDescending(p => p.Timestamp).Where(p => p.CreatorId == userId).Take(6).ToListAsync();
if (this.CommentsEnabled)
{
this.Comments = await this.Database.Comments.Include(p => p.Poster)

View file

@ -38,6 +38,7 @@ public class UsersPage : BaseLayout
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/users/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
this.Users = await this.Database.Users.Where(u => u.PermissionLevel != PermissionLevel.Banned && u.Username.Contains(this.SearchValue))
.Where(u => u.ProfileVisibility == PrivacyType.All) // TODO: change check for when user is logged in
.OrderByDescending(b => b.UserId)
.Skip(pageNumber * ServerStatics.PageSize)
.Take(ServerStatics.PageSize)