├── .gitignore ├── TrickingLibrary.Api ├── Form │ ├── CommentForm.cs │ ├── UpdateCategoryForm.cs │ ├── UpdateStagedTrickForm.cs │ ├── UpdateDifficultyForm.cs │ ├── CreateCategoryForm.cs │ ├── CreateDifficultyForm.cs │ ├── UpdateTrickForm.cs │ ├── InviteModeratorForm.cs │ ├── SubmissionForm.cs │ ├── Validation │ │ ├── CommentFormValidation.cs │ │ ├── CreateCategoryFormValidation.cs │ │ ├── InviteModeratorFormValidation.cs │ │ ├── CreateDifficultyFormValidation.cs │ │ ├── ReviewFormValidation.cs │ │ ├── SubmissionFormValidation.cs │ │ ├── UpdateCategoryFormValidation.cs │ │ ├── UpdateDifficultyFormValidation.cs │ │ ├── CreateTrickFormValidation.cs │ │ ├── UpdateStagedTrickFormValidation.cs │ │ └── UpdateTrickFormValidation.cs │ └── CreateTrickForm.cs ├── Pages │ ├── _ViewImports.cshtml │ ├── BasePage.cs │ ├── Account │ │ ├── Login.cshtml │ │ ├── Moderator.cshtml │ │ ├── Register.cshtml │ │ ├── Login.cshtml.cs │ │ ├── Register.cshtml.cs │ │ └── Moderator.cshtml.cs │ └── Shared │ │ └── _Layout.cshtml ├── Settings │ └── FileType.cs ├── Services │ ├── Email │ │ ├── SendGridOptions.cs │ │ └── EmailClient.cs │ └── Storage │ │ ├── IS3Client.cs │ │ ├── S3Settings.cs │ │ ├── FileSettings.cs │ │ ├── IFileProvider.cs │ │ ├── S3FileProvider.cs │ │ ├── LinodeS3Client.cs │ │ ├── RegisterService.cs │ │ ├── TemporaryFileStorage.cs │ │ └── LocalFileProvider.cs ├── BackgroundServices │ ├── VideoEditing │ │ └── EditVideoMessage.cs │ └── SubmissionVoting │ │ ├── ISubmissionVoteSink.cs │ │ ├── VoteForm.cs │ │ └── SubmissionVotingService.cs ├── appsettings.json ├── test-moderator-invite.http ├── ApiIdentityDbContext.cs ├── Controllers │ ├── ApiController.cs │ ├── AuthController.cs │ ├── FileController.cs │ ├── CommentController.cs │ ├── AdminController.cs │ ├── UserController.cs │ ├── SubmissionsController.cs │ └── ModerationItemController.cs ├── ViewModels │ ├── CommentViewModels.cs │ ├── ModerationItemViewModels.cs │ ├── ReviewViewModels.cs │ ├── CategoryViewModels.cs │ ├── DifficultyViewModels.cs │ ├── UserViewModels.cs │ ├── SubmissionViewModels.cs │ └── TrickViewModels.cs ├── appsettings.Development.json ├── Properties │ └── launchSettings.json ├── appsettings.Production.json ├── TrickingLibrary.Api.csproj ├── TrickingLibraryConstants.cs ├── CommentCreationContext.cs └── ModerationItemReviewContext.cs ├── web-client ├── data │ ├── events.js │ └── functions.js ├── static │ └── favicon.ico ├── middleware │ ├── admin.js │ └── mod.js ├── .env.production ├── .editorconfig ├── components │ ├── _shared.js │ ├── auth │ │ └── if-auth.vue │ ├── moderation │ │ ├── simple-info-card.vue │ │ ├── moderation-category-overview.vue │ │ ├── moderation-difficulty-overview.vue │ │ └── moderation-item-feed.vue │ ├── popup.vue │ ├── content-creation │ │ ├── _shared.js │ │ ├── content-creation-dialog.vue │ │ ├── category-form.vue │ │ ├── difficulty-form.vue │ │ └── submission-steps.vue │ ├── comments │ │ ├── _shared.js │ │ ├── comment-input.vue │ │ ├── comment-section.vue │ │ ├── comment-body.vue │ │ └── comment.vue │ ├── item-content-layout.vue │ ├── moderation.js │ ├── trick-list.vue │ ├── submission-feed.vue │ ├── user-header.vue │ ├── profile-completed-tricks.vue │ ├── video-player.vue │ ├── nav-bar-search.vue │ ├── feed.js │ ├── front-page │ │ ├── front-page-trick-feed.vue │ │ ├── front-page-category-feed.vue │ │ └── front-page-difficulty-feed.vue │ ├── submission.vue │ └── trick-info-card.vue ├── store │ ├── index.js │ ├── popup.js │ ├── auth.js │ ├── library.js │ └── content-creation.js ├── assets │ └── css │ │ └── main.scss ├── plugins │ └── axios.js ├── package.json ├── pages │ ├── trick │ │ └── _trick │ │ │ ├── history.vue │ │ │ └── index.vue │ ├── category │ │ └── _category │ │ │ ├── history.vue │ │ │ └── index.vue │ ├── difficulty │ │ └── _difficulty │ │ │ ├── history.vue │ │ │ └── index.vue │ ├── index.vue │ ├── moderation │ │ └── index.vue │ ├── profile │ │ ├── _username.vue │ │ └── index.vue │ └── admin │ │ └── index.vue ├── layouts │ ├── error.vue │ └── default.vue ├── .gitignore └── nuxt.config.js ├── TrickingLibrary.Models ├── VersionState.cs ├── TrickingLibrary.Models.csproj ├── Moderation │ ├── ModerationTypes.cs │ ├── Review.cs │ └── ModerationItem.cs ├── Abstractions │ ├── VersionedModel.cs │ ├── BaseModel.cs │ └── Mutable.cs ├── SubmissionVote.cs ├── TrickCategory.cs ├── TrickDifficulty.cs ├── Category.cs ├── TrickRelationship.cs ├── Video.cs ├── Difficulty.cs ├── User.cs ├── Submission.cs ├── Trick.cs └── Comment.cs ├── TrickingLibrary.Data ├── FeedQuery.cs ├── VersionMigrations │ ├── IEntityMigrationContext.cs │ ├── CategoryMigrationContext.cs │ ├── DifficultyMigrationContext.cs │ ├── TrickMigrationContext.cs │ └── VersionMigrationContext.cs ├── scripts.md ├── TrickingLibrary.Data.csproj ├── AppDbContext.cs └── QueryExtensions.cs ├── hosting ├── tricking-library-app.service ├── tricking-library-api.service ├── api.raw-coding.net └── app.raw-coding.net ├── .github └── workflows │ ├── app-build-and-deply.yaml │ └── api-build-and-deply.yaml └── TrickingLibrary.sln /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | bin/ 3 | obj/ 4 | 5 | ffmpeg/ 6 | wwwroot/ 7 | tempkey.jwk -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/CommentForm.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Form 2 | { 3 | 4 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" -------------------------------------------------------------------------------- /web-client/data/events.js: -------------------------------------------------------------------------------- 1 | export const EVENTS = { 2 | CONTENT_UPDATED: 'content-updated' 3 | } 4 | -------------------------------------------------------------------------------- /web-client/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T0shik/raw-coding-tricking-library/HEAD/web-client/static/favicon.ico -------------------------------------------------------------------------------- /web-client/middleware/admin.js: -------------------------------------------------------------------------------- 1 | export default function ({store, redirect}) { 2 | if (!store.getters["auth/admin"]) { 3 | redirect('/') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web-client/middleware/mod.js: -------------------------------------------------------------------------------- 1 | export default function ({store, redirect}){ 2 | if(!store.getters["auth/moderator"]){ 3 | redirect('/') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Settings/FileType.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Settings 2 | { 3 | public enum FileType 4 | { 5 | Image, 6 | Video, 7 | } 8 | } -------------------------------------------------------------------------------- /web-client/.env.production: -------------------------------------------------------------------------------- 1 | LOGIN_PATH=https://api.raw-coding.net/account/login 2 | LOGOUT_PATH=https://api.raw-coding.net/api/auth/logout 3 | BROWSER_SIDE_URL=https://api.raw-coding.net 4 | -------------------------------------------------------------------------------- /TrickingLibrary.Models/VersionState.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Models 2 | { 3 | public enum VersionState 4 | { 5 | Live = 0, 6 | Staged = 1, 7 | Outdated = 2, 8 | } 9 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/UpdateCategoryForm.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Form 2 | { 3 | public class UpdateCategoryForm : CreateCategoryForm 4 | { 5 | public int Id { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/UpdateStagedTrickForm.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Form 2 | { 3 | public class UpdateStagedTrickForm : CreateTrickForm 4 | { 5 | public int Id { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/UpdateDifficultyForm.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Form 2 | { 3 | public class UpdateDifficultyForm : CreateDifficultyForm 4 | { 5 | public int Id { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/TrickingLibrary.Models.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/CreateCategoryForm.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Form 2 | { 3 | public class CreateCategoryForm 4 | { 5 | public string Name { get; set; } 6 | public string Description { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/CreateDifficultyForm.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Form 2 | { 3 | public class CreateDifficultyForm 4 | { 5 | public string Name { get; set; } 6 | public string Description { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/UpdateTrickForm.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Form 2 | { 3 | public class UpdateTrickForm : CreateTrickForm 4 | { 5 | public int Id { get; set; } 6 | public string Reason { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/InviteModeratorForm.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Controllers 2 | { 3 | public class InviteModeratorForm 4 | { 5 | public string Email { get; set; } 6 | public string ReturnUrl { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Services/Email/SendGridOptions.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Services.Email 2 | { 3 | public class SendGridOptions 4 | { 5 | public string ApiKey { get; set; } 6 | public string From { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /TrickingLibrary.Data/FeedQuery.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Data 2 | { 3 | public class FeedQuery 4 | { 5 | public string Order { get; set; } 6 | public int Cursor { get; set; } 7 | public int Limit { get; } = 10; 8 | } 9 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/SubmissionForm.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Form 2 | { 3 | public class SubmissionForm 4 | { 5 | public string TrickId { get; set; } 6 | public string Description { get; set; } 7 | public string Video { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/BackgroundServices/VideoEditing/EditVideoMessage.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.BackgroundServices.VideoEditing 2 | { 3 | public class EditVideoMessage 4 | { 5 | public int SubmissionId { get; set; } 6 | public string Input { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /web-client/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/BackgroundServices/SubmissionVoting/ISubmissionVoteSink.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace TrickingLibrary.Api.BackgroundServices.SubmissionVoting 4 | { 5 | public interface ISubmissionVoteSink 6 | { 7 | ValueTask Submit(VoteForm voteForm); 8 | } 9 | } -------------------------------------------------------------------------------- /web-client/data/functions.js: -------------------------------------------------------------------------------- 1 | export const hasOccurrences = (searchIndex, query) => { 2 | const queryParts = query.toLowerCase().split(' '); 3 | if (queryParts.length > 0) { 4 | return queryParts.map(x => searchIndex.indexOf(x) > -1).reduce((a, b) => a && b) 5 | } 6 | 7 | return true; 8 | } 9 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Services/Storage/IS3Client.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | 4 | namespace TrickingLibrary.Api.Services.Storage 5 | { 6 | public interface IS3Client 7 | { 8 | Task SaveFile(string fileName, string mime, Stream fileStream); 9 | } 10 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Pages/BasePage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace TrickingLibrary.Api.Pages 5 | { 6 | public class BasePage : PageModel 7 | { 8 | public IList CustomErrors { get; set; } = new List(); 9 | } 10 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/Moderation/ModerationTypes.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Models.Moderation 2 | { 3 | public struct ModerationTypes 4 | { 5 | public const string Trick = "trick"; 6 | public const string Category = "category"; 7 | public const string Difficulty = "difficulty"; 8 | } 9 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/BackgroundServices/SubmissionVoting/VoteForm.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.BackgroundServices.SubmissionVoting 2 | { 3 | public class VoteForm 4 | { 5 | public int SubmissionId { get; set; } 6 | public int Value { get; set; } 7 | public string UserId { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "ModerationSettings": { 10 | "GoalCount": 1 11 | }, 12 | "AllowedHosts": "*" 13 | } 14 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/test-moderator-invite.http: -------------------------------------------------------------------------------- 1 | POST https://localhost:5001/api/admin/moderators 2 | Content-Type: application/json 3 | 4 | { 5 | "Email": "atwieslander@gmail.com", 6 | "ReturnUrl": "https://localhost:3000/" 7 | } 8 | 9 | ### 10 | GET https://localhost:5001/api/admin/moderators 11 | Accept: application/json 12 | 13 | ### -------------------------------------------------------------------------------- /TrickingLibrary.Models/Abstractions/VersionedModel.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Models.Abstractions 2 | { 3 | public abstract class VersionedModel : Mutable 4 | { 5 | public string Slug { get; set; } 6 | public int Version { get; set; } = 1; 7 | public VersionState State { get; set; } = VersionState.Staged; 8 | } 9 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/Abstractions/BaseModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TrickingLibrary.Models.Abstractions 4 | { 5 | public abstract class BaseModel 6 | { 7 | public TKey Id { get; set; } 8 | public bool Deleted { get; set; } 9 | 10 | public DateTime Created { get; set; } = DateTime.UtcNow; 11 | } 12 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/SubmissionVote.cs: -------------------------------------------------------------------------------- 1 | using TrickingLibrary.Models.Abstractions; 2 | 3 | namespace TrickingLibrary.Models 4 | { 5 | public class SubmissionVote : Mutable 6 | { 7 | public int SubmissionId { get; set; } 8 | public Submission Submission { get; set; } 9 | public int Value { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/Abstractions/Mutable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TrickingLibrary.Models.Abstractions 4 | { 5 | public class Mutable : BaseModel 6 | { 7 | public string UserId { get; set; } 8 | public User User { get; set; } 9 | public DateTime Updated { get; set; } = DateTime.UtcNow; 10 | } 11 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/TrickCategory.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Models 2 | { 3 | public class TrickCategory 4 | { 5 | public int TrickId { get; set; } 6 | public Trick Trick { get; set; } 7 | 8 | public int CategoryId { get; set; } 9 | public Category Category { get; set; } 10 | public bool Active { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/TrickDifficulty.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Models 2 | { 3 | public class TrickDifficulty 4 | { 5 | public int TrickId { get; set; } 6 | public Trick Trick { get; set; } 7 | 8 | public int DifficultyId { get; set; } 9 | public Difficulty Difficulty { get; set; } 10 | public bool Active { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /web-client/components/_shared.js: -------------------------------------------------------------------------------- 1 | import {EVENTS} from "@/data/events"; 2 | 3 | export const onContentUpdate = { 4 | created() { 5 | this.$nuxt.$on(EVENTS.CONTENT_UPDATED, this.onContentUpdate) 6 | }, 7 | destroyed() { 8 | this.$nuxt.$off(EVENTS.CONTENT_UPDATED, this.onContentUpdate) 9 | }, 10 | methods: { 11 | onContentUpdate() { 12 | 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /TrickingLibrary.Models/Category.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using TrickingLibrary.Models.Abstractions; 3 | 4 | namespace TrickingLibrary.Models 5 | { 6 | public class Category : VersionedModel 7 | { 8 | public string Name { get; set; } 9 | public string Description { get; set; } 10 | public IList Tricks { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/TrickRelationship.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Models 2 | { 3 | public class TrickRelationship 4 | { 5 | public int PrerequisiteId { get; set; } 6 | public Trick Prerequisite { get; set; } 7 | public int ProgressionId { get; set; } 8 | public Trick Progression { get; set; } 9 | public bool Active { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /web-client/store/index.js: -------------------------------------------------------------------------------- 1 | const initState = () => ({}) 2 | 3 | export const state = initState 4 | 5 | export const mutations = { 6 | reset(state) { 7 | Object.assign(state, initState()) 8 | } 9 | } 10 | 11 | export const actions = { 12 | async nuxtServerInit({dispatch}) { 13 | await dispatch("auth/initialize") 14 | await dispatch("library/loadContent") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TrickingLibrary.Models/Video.cs: -------------------------------------------------------------------------------- 1 | using TrickingLibrary.Models.Abstractions; 2 | 3 | namespace TrickingLibrary.Models 4 | { 5 | public class Video : BaseModel 6 | { 7 | public int? SubmissionId { get; set; } 8 | public Submission Submission { get; set; } 9 | public string VideoLink { get; set; } 10 | public string ThumbLink { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Services/Storage/S3Settings.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Services.Storage 2 | { 3 | public class S3Settings 4 | { 5 | public string AccessKey { get; set; } 6 | public string SecretKey { get; set; } 7 | public string ServiceUrl { get; set; } 8 | public string Bucket { get; set; } 9 | public string Root { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /hosting/tricking-library-app.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Tricking Library Nuxtjs App 3 | 4 | [Service] 5 | WorkingDirectory=/var/tricking-library/app 6 | ExecStart=npm run start-prod 7 | Restart=always 8 | # Restart service after 10 seconds if the dotnet service crashes: 9 | RestartSec=10 10 | KillSignal=SIGINT 11 | SyslogIdentifier=tricking-library-app 12 | 13 | [Install] 14 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /TrickingLibrary.Api/Services/Storage/FileSettings.cs: -------------------------------------------------------------------------------- 1 | namespace TrickingLibrary.Api.Services.Storage 2 | { 3 | public class FileSettings 4 | { 5 | public string VideoUrl { get; set; } 6 | public string ImageUrl { get; set; } 7 | public string Provider { get; set; } 8 | public string WorkingDirectory { get; set; } 9 | public string FFMPEGPath { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /TrickingLibrary.Data/VersionMigrations/IEntityMigrationContext.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using TrickingLibrary.Models.Abstractions; 3 | 4 | namespace TrickingLibrary.Data.VersionMigrations 5 | { 6 | public interface IEntityMigrationContext 7 | { 8 | IQueryable GetSource(); 9 | void MigrateRelationships(int current, int target); 10 | void VoidRelationships(int id); 11 | } 12 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/Difficulty.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using TrickingLibrary.Models.Abstractions; 3 | 4 | namespace TrickingLibrary.Models 5 | { 6 | public class Difficulty : VersionedModel 7 | { 8 | public string Name { get; set; } 9 | public string Description { get; set; } 10 | public IList Tricks { get; set; } = new List(); 11 | } 12 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/User.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using TrickingLibrary.Models.Abstractions; 3 | 4 | namespace TrickingLibrary.Models 5 | { 6 | public class User : BaseModel 7 | { 8 | public string Username { get; set; } 9 | 10 | public string Image { get; set; } 11 | 12 | public IList Submissions { get; set; } = new List(); 13 | } 14 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/Validation/CommentFormValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TrickingLibrary.Api.Form.Validation 4 | { 5 | public class CommentFormValidation : AbstractValidator 6 | { 7 | public CommentFormValidation() 8 | { 9 | RuleFor(x => x.ParentId).NotEmpty(); 10 | RuleFor(x => x.Content).NotEmpty(); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/Validation/CreateCategoryFormValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TrickingLibrary.Api.Form.Validation 4 | { 5 | public class CreateCategoryFormValidation : AbstractValidator 6 | { 7 | public CreateCategoryFormValidation() 8 | { 9 | RuleFor(x => x.Name).NotEmpty(); 10 | RuleFor(x => x.Description).NotEmpty(); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/Validation/InviteModeratorFormValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TrickingLibrary.Api.Controllers 4 | { 5 | public class InviteModeratorFormValidation : AbstractValidator 6 | { 7 | public InviteModeratorFormValidation() 8 | { 9 | RuleFor(x => x.Email).NotEmpty(); 10 | RuleFor(x => x.ReturnUrl).NotEmpty(); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/Validation/CreateDifficultyFormValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TrickingLibrary.Api.Form.Validation 4 | { 5 | public class CreateDifficultyFormValidation : AbstractValidator 6 | { 7 | public CreateDifficultyFormValidation() 8 | { 9 | RuleFor(x => x.Name).NotEmpty(); 10 | RuleFor(x => x.Description).NotEmpty(); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /web-client/assets/css/main.scss: -------------------------------------------------------------------------------- 1 | .sticky { 2 | position: -webkit-sticky; 3 | position: sticky; 4 | top: 60px; 5 | } 6 | 7 | .fpt-0 { 8 | .v-stepper__content { 9 | padding-top: 0; 10 | } 11 | } 12 | 13 | .pointer { 14 | cursor: pointer; 15 | } 16 | 17 | @media (max-width: 600px) { 18 | .v-autocomplete__content.v-menu__content { 19 | width: 100% !important; 20 | left: 0 !important; 21 | max-width: 100% !important; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/Validation/ReviewFormValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using TrickingLibrary.Models.Moderation; 3 | 4 | namespace TrickingLibrary.Api.Form.Validation 5 | { 6 | public class ReviewFormValidation : AbstractValidator 7 | { 8 | public ReviewFormValidation() 9 | { 10 | RuleFor(x => x.Comment).NotEmpty().When(x => x.Status != ReviewStatus.Approved); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Services/Storage/IFileProvider.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using TrickingLibrary.Api.Settings; 4 | 5 | namespace TrickingLibrary.Api.Services.Storage 6 | { 7 | public interface IFileProvider 8 | { 9 | public Task SaveProfileImageAsync(Stream fileStream); 10 | public Task SaveVideoAsync(Stream fileStream); 11 | public Task SaveThumbnailAsync(Stream fileStream); 12 | } 13 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/Validation/SubmissionFormValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TrickingLibrary.Api.Form.Validation 4 | { 5 | public class SubmissionFormValidation : AbstractValidator 6 | { 7 | public SubmissionFormValidation() 8 | { 9 | RuleFor(x => x.TrickId).NotEmpty(); 10 | RuleFor(x => x.Description).NotEmpty(); 11 | RuleFor(x => x.Video).NotEmpty(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/Validation/UpdateCategoryFormValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TrickingLibrary.Api.Form.Validation 4 | { 5 | public class UpdateCategoryFormValidation : AbstractValidator 6 | { 7 | public UpdateCategoryFormValidation() 8 | { 9 | RuleFor(x => x.Id).GreaterThan(0); 10 | RuleFor(x => x.Name).NotEmpty(); 11 | RuleFor(x => x.Description).NotEmpty(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/Validation/UpdateDifficultyFormValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TrickingLibrary.Api.Form.Validation 4 | { 5 | public class UpdateDifficultyFormValidation : AbstractValidator 6 | { 7 | public UpdateDifficultyFormValidation() 8 | { 9 | RuleFor(x => x.Id).GreaterThan(0); 10 | RuleFor(x => x.Name).NotEmpty(); 11 | RuleFor(x => x.Description).NotEmpty(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /TrickingLibrary.Data/scripts.md: -------------------------------------------------------------------------------- 1 | ### migrations 2 | dotnet ef migrations add -c AppDbContext -s ..\TrickingLibrary.Api -o .\Migrations 3 | 4 | ### migration script 5 | dotnet ef migrations script -i -c AppDbContext -s ..\TrickingLibrary.Api -o script.sql 6 | 7 | ### identity migrations & update 8 | dotnet ef migrations add -c ApiIdentityDbContext -o .\IdentityMigrations 9 | dotnet ef migrations script -i -c ApiIdentityDbContext -o script.sql 10 | dotnet ef database update -c ApiIdentityDbContext -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/CreateTrickForm.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace TrickingLibrary.Api.Form 4 | { 5 | public class CreateTrickForm 6 | { 7 | public string Name { get; set; } 8 | public string Description { get; set; } 9 | public int Difficulty { get; set; } 10 | public IEnumerable Prerequisites { get; set; } 11 | public IEnumerable Progressions { get; set; } 12 | public IEnumerable Categories { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /web-client/plugins/axios.js: -------------------------------------------------------------------------------- 1 | export default function ({$axios, store}) { 2 | $axios.setHeader('X-Requested-With', 'XMLHttpRequest') 3 | $axios.onRequest(config => { 4 | config.withCredentials = true 5 | }) 6 | 7 | $axios.onError(error => { 8 | if (error.response && error.response.status && error.response.data) { 9 | const {status, data} = error.response 10 | if ((status | 0) === 400) { 11 | store.dispatch('popup/error', data) 12 | return {error} 13 | } 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/Validation/CreateTrickFormValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TrickingLibrary.Api.Form.Validation 4 | { 5 | public class CreateTrickFormValidation : AbstractValidator 6 | { 7 | public CreateTrickFormValidation() 8 | { 9 | RuleFor(x => x.Name).NotEmpty(); 10 | RuleFor(x => x.Description).NotEmpty(); 11 | RuleFor(x => x.Difficulty).NotEmpty(); 12 | RuleFor(x => x.Categories).NotEmpty(); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /hosting/tricking-library-api.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Tricking Library API 3 | 4 | [Service] 5 | WorkingDirectory=/var/tricking-library/api 6 | ExecStart=/usr/bin/dotnet /var/tricking-library/api/TrickingLibrary.Api.dll 7 | Restart=always 8 | # Restart service after 10 seconds if the dotnet service crashes: 9 | RestartSec=10 10 | KillSignal=SIGINT 11 | SyslogIdentifier=tricking-library-api 12 | Environment=ASPNETCORE_ENVIRONMENT=Production 13 | Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false 14 | 15 | [Install] 16 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /web-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-client", 3 | "version": "1.0.0", 4 | "description": "My swell Nuxt.js project", 5 | "author": "Anton Wieslander", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nuxt", 9 | "build": "nuxt build", 10 | "start": "nuxt start", 11 | "start-prod": "nuxt start --dotenv .env.production", 12 | "generate": "nuxt generate" 13 | }, 14 | "dependencies": { 15 | "@nuxtjs/axios": "^5.12.0", 16 | "nuxt": "^2.14.0" 17 | }, 18 | "devDependencies": { 19 | "@nuxtjs/vuetify": "^1.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TrickingLibrary.Models/Moderation/Review.cs: -------------------------------------------------------------------------------- 1 | using TrickingLibrary.Models.Abstractions; 2 | 3 | namespace TrickingLibrary.Models.Moderation 4 | { 5 | public class Review : Mutable 6 | { 7 | public int ModerationItemId { get; set; } 8 | public ModerationItem ModerationItem { get; set; } 9 | 10 | public string Comment { get; set; } 11 | public ReviewStatus Status { get; set; } 12 | } 13 | 14 | public enum ReviewStatus 15 | { 16 | Approved = 0, 17 | Rejected = 1, 18 | Waiting = 2, 19 | } 20 | } -------------------------------------------------------------------------------- /web-client/components/auth/if-auth.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/ApiIdentityDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; 2 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace TrickingLibrary.Api 6 | { 7 | public class ApiIdentityDbContext : IdentityDbContext, 8 | IDataProtectionKeyContext 9 | { 10 | public ApiIdentityDbContext(DbContextOptions options) : base(options) 11 | { 12 | 13 | } 14 | 15 | public DbSet DataProtectionKeys { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /web-client/pages/trick/_trick/history.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/Validation/UpdateStagedTrickFormValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TrickingLibrary.Api.Form.Validation 4 | { 5 | public class UpdateStagedTrickFormValidation : AbstractValidator 6 | { 7 | public UpdateStagedTrickFormValidation() 8 | { 9 | RuleFor(x => x.Id).NotEqual(0); 10 | RuleFor(x => x.Name).NotEmpty(); 11 | RuleFor(x => x.Description).NotEmpty(); 12 | RuleFor(x => x.Difficulty).NotEmpty(); 13 | RuleFor(x => x.Categories).NotEmpty(); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /web-client/components/moderation/simple-info-card.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Form/Validation/UpdateTrickFormValidation.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace TrickingLibrary.Api.Form.Validation 4 | { 5 | public class UpdateTrickFormValidation : AbstractValidator 6 | { 7 | public UpdateTrickFormValidation() 8 | { 9 | RuleFor(x => x.Id).NotEqual(0); 10 | RuleFor(x => x.Reason).NotEmpty(); 11 | RuleFor(x => x.Name).NotEmpty(); 12 | RuleFor(x => x.Description).NotEmpty(); 13 | RuleFor(x => x.Difficulty).NotEmpty(); 14 | RuleFor(x => x.Categories).NotEmpty(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/Moderation/ModerationItem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using TrickingLibrary.Models.Abstractions; 3 | 4 | namespace TrickingLibrary.Models.Moderation 5 | { 6 | public class ModerationItem : Mutable 7 | { 8 | public int Current { get; set; } 9 | public int Target { get; set; } 10 | public string Type { get; set; } 11 | public string Reason { get; set; } 12 | public bool Rejected { get; set; } 13 | public IList Comments { get; set; } = new List(); 14 | public IList Reviews { get; set; } = new List(); 15 | } 16 | } -------------------------------------------------------------------------------- /web-client/pages/category/_category/history.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Controllers/ApiController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Security.Claims; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace TrickingLibrary.Api.Controllers 6 | { 7 | [ApiController] 8 | public class ApiController : ControllerBase 9 | { 10 | protected string UserId => GetClaim(ClaimTypes.NameIdentifier); 11 | protected string Username => GetClaim(ClaimTypes.Name); 12 | protected string Role => GetClaim(TrickingLibraryConstants.Claims.Role); 13 | 14 | private string GetClaim(string claimType) => User.Claims 15 | .FirstOrDefault(x => x.Type.Equals(claimType))?.Value; 16 | } 17 | } -------------------------------------------------------------------------------- /web-client/pages/difficulty/_difficulty/history.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /web-client/store/popup.js: -------------------------------------------------------------------------------- 1 | export const POPUP_TYPES = { 2 | ERROR: 0, 3 | SUCCESS: 1, 4 | } 5 | 6 | const initState = () => ({ 7 | message: "", 8 | type: POPUP_TYPES.ERROR 9 | }) 10 | 11 | export const state = initState 12 | 13 | export const mutations = { 14 | show(state, {message, type}) { 15 | state.message = message 16 | state.type = type 17 | }, 18 | hide(state) { 19 | Object.assign(state, initState()) 20 | } 21 | } 22 | 23 | export const actions = { 24 | error({commit}, message) { 25 | commit('show', {message, type: POPUP_TYPES.ERROR}) 26 | }, 27 | success({commit}, message) { 28 | commit('show', {message, type: POPUP_TYPES.SUCCESS}) 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/ViewModels/CommentViewModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using TrickingLibrary.Models; 4 | 5 | namespace TrickingLibrary.Api.ViewModels 6 | { 7 | public static class CommentViewModels 8 | { 9 | public static readonly Func Create = Projection.Compile(); 10 | 11 | public static Expression> Projection => 12 | comment => new 13 | { 14 | comment.Id, 15 | comment.ParentId, 16 | comment.Content, 17 | comment.HtmlContent, 18 | User = UserViewModels.CreateFlat(comment.User), 19 | }; 20 | } 21 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/Submission.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using TrickingLibrary.Models.Abstractions; 4 | 5 | namespace TrickingLibrary.Models 6 | { 7 | public class Submission : BaseModel 8 | { 9 | public string TrickId { get; set; } 10 | public Video Video { get; set; } 11 | public bool VideoProcessed { get; set; } 12 | public string Description { get; set; } 13 | 14 | public string UserId { get; set; } 15 | public User User { get; set; } 16 | 17 | public IList Votes { get; set; } = new List(); 18 | public IList Comments { get; set; } = new List(); 19 | } 20 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/Trick.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using TrickingLibrary.Models.Abstractions; 3 | 4 | namespace TrickingLibrary.Models 5 | { 6 | public class Trick : VersionedModel 7 | { 8 | public string Name { get; set; } 9 | public string Description { get; set; } 10 | public IList Prerequisites { get; set; } = new List(); 11 | public IList Progressions { get; set; } = new List(); 12 | public IList TrickCategories { get; set; } = new List(); 13 | public IList TrickDifficulties { get; set; } = new List(); 14 | } 15 | } -------------------------------------------------------------------------------- /TrickingLibrary.Data/TrickingLibrary.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Default": "Host=127.0.0.1;Port=5666;Database=tricking_library;User Id=postgres;Password=password;" 4 | }, 5 | "CookieDomain": "localhost", 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Information", 9 | "Microsoft": "Warning", 10 | "Microsoft.Hosting.Lifetime": "Information" 11 | } 12 | }, 13 | "FileSettings": { 14 | "VideoUrl": "https://localhost:5001/api/files/video", 15 | "ImageUrl": "https://localhost:5001/api/files/image", 16 | "Provider": "S3", 17 | "FFMPEGPath": "D:\\WS\\Rider\\TrickingLibrary\\TrickingLibrary.Api\\ffmpeg\\ffmpeg.exe", 18 | "WorkingDirectory": "D:\\WS\\Rider\\TrickingLibrary\\TrickingLibrary.Api\\wwwroot" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web-client/components/popup.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:63905", 7 | "sslPort": 44396 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "TrickingLibrary.Api": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace TrickingLibrary.Api.Controllers 8 | { 9 | [ApiController] 10 | [Route("api/auth")] 11 | public class AuthController : ControllerBase 12 | { 13 | [HttpGet("logout")] 14 | public async Task Logout(string logoutId, 15 | [FromServices] SignInManager signInManager, 16 | [FromServices] IWebHostEnvironment env) 17 | { 18 | await signInManager.SignOutAsync(); 19 | 20 | return Redirect(env.IsDevelopment() ? "https://localhost:3000/" : "/"); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /web-client/components/content-creation/_shared.js: -------------------------------------------------------------------------------- 1 | import {mapActions, mapState} from 'vuex'; 2 | import {EVENTS} from "@/data/events"; 3 | 4 | export const close = { 5 | methods: { 6 | ...mapActions('content-creation', ['cancelUpload']), 7 | close() { 8 | return this.cancelUpload({hard: true}) 9 | } 10 | } 11 | } 12 | 13 | export const form = (formFactory) => ({ 14 | data: () => ({ 15 | form: formFactory() 16 | }), 17 | created: function () { 18 | if (this.setup) 19 | this.setup(this.form) 20 | }, 21 | methods: { 22 | broadcastUpdate(){ 23 | this.$nuxt.$emit(EVENTS.CONTENT_UPDATED) 24 | this.loadContent() 25 | }, 26 | ...mapActions('library', ['loadContent']) 27 | }, 28 | computed: { 29 | ...mapState('content-creation', ['setup']), 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/ViewModels/ModerationItemViewModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using TrickingLibrary.Models; 5 | using TrickingLibrary.Models.Moderation; 6 | 7 | namespace TrickingLibrary.Api.ViewModels 8 | { 9 | public static class ModerationItemViewModels 10 | { 11 | public static readonly Func Create = Projection.Compile(); 12 | 13 | public static Expression> Projection => 14 | modItem => new 15 | { 16 | modItem.Id, 17 | modItem.Current, 18 | modItem.Target, 19 | modItem.Reason, 20 | modItem.Type, 21 | Updated = modItem.Updated.ToLocalTime().ToString("HH:mm dd/MM/yyyy"), 22 | }; 23 | } 24 | } -------------------------------------------------------------------------------- /TrickingLibrary.Models/Comment.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using TrickingLibrary.Models.Abstractions; 3 | using TrickingLibrary.Models.Moderation; 4 | 5 | namespace TrickingLibrary.Models 6 | { 7 | public class Comment : BaseModel 8 | { 9 | public string Content { get; set; } 10 | public string HtmlContent { get; set; } 11 | public int? ModerationItemId { get; set; } 12 | public ModerationItem ModerationItem { get; set; } 13 | 14 | public int? SubmissionId { get; set; } 15 | public Submission Submission { get; set; } 16 | public int? ParentId { get; set; } 17 | public Comment Parent { get; set; } 18 | 19 | public string UserId { get; set; } 20 | public User User { get; set; } 21 | public IList Replies { get; set; } = new List(); 22 | } 23 | } -------------------------------------------------------------------------------- /web-client/layouts/error.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 39 | 40 | 45 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Default": "[secret]" 4 | }, 5 | "AdminPassword": "[secret]", 6 | "CookieDomain": ".raw-coding.net", 7 | "Logging": { 8 | "LogLevel": { 9 | "Default": "Information", 10 | "Microsoft": "Warning", 11 | "Microsoft.Hosting.Lifetime": "Information" 12 | } 13 | }, 14 | "FileSettings": { 15 | "VideoUrl": "[secret]", 16 | "ImageUrl": "[secret]", 17 | "Provider": "S3", 18 | "FFMPEGPath": "ffmpeg", 19 | "WorkingDirectory": "/tmp/tricking_library" 20 | }, 21 | "SendGridOptions": { 22 | "ApiKey": "[secret]", 23 | "From": "[secret]" 24 | }, 25 | "S3Settings": { 26 | "AccessKey": "[secret]", 27 | "SecretKey": "[secret]", 28 | "ServiceUrl": "[secret]", 29 | "Bucket": "[secret]", 30 | "Root": "tricking-library" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /web-client/components/comments/_shared.js: -------------------------------------------------------------------------------- 1 | export const COMMENT_PARENT_TYPE = { 2 | MODERATION_ITEM: 0, 3 | SUBMISSION: 1, 4 | COMMENT: 2, 5 | } 6 | 7 | export const configurable = { 8 | props: { 9 | parentId: { 10 | type: Number, 11 | required: true 12 | }, 13 | parentType: { 14 | type: Number, 15 | required: true 16 | } 17 | } 18 | } 19 | 20 | export const creator = { 21 | methods: { 22 | emitComment(comment) { 23 | this.$emit('comment-created', comment) 24 | }, 25 | cancel() { 26 | this.$emit('cancel') 27 | } 28 | } 29 | } 30 | 31 | export const container = { 32 | data: () => ({ 33 | comments: [] 34 | }), 35 | methods: { 36 | appendComment(comment) { 37 | this.comments.push(comment) 38 | }, 39 | prependComment(comment) { 40 | this.comments.unshift(comment) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web-client/pages/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /.github/workflows/app-build-and-deply.yaml: -------------------------------------------------------------------------------- 1 | name: Nuxtjs app Build and Deploy to Linode 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | name: Build our App 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Pull Code 12 | uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2-beta 14 | with: 15 | node-version: '12' 16 | - name: Install Dependencies 17 | run: npm install 18 | working-directory: web-client 19 | - name: Install Dependencies 20 | run: npm run build 21 | working-directory: web-client 22 | - name: Push to Linode 23 | run: | 24 | echo "$ssh_key" > ~/ssh_key 25 | chmod 600 ~/ssh_key 26 | rsync -e "ssh -i ~/ssh_key -o StrictHostKeyChecking=no" -avzr ./web-client/ "$user"@"$target_ip":/var/tricking-library/app 27 | env: 28 | ssh_key: ${{ secrets.CICD_SSH }} 29 | user: ${{ secrets.CICD_USER }} 30 | target_ip: ${{ secrets.LINODE_IP }} 31 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/ViewModels/ReviewViewModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using TrickingLibrary.Models.Moderation; 4 | 5 | namespace TrickingLibrary.Api.ViewModels 6 | { 7 | public static class ReviewViewModels 8 | { 9 | public static readonly Func Create = Projection.Compile(); 10 | 11 | public static Expression> Projection => 12 | review => new 13 | { 14 | review.Id, 15 | review.ModerationItemId, 16 | review.Comment, 17 | review.Status, 18 | }; 19 | 20 | public static readonly Func CreateWithUser = WithUserProjection.Compile(); 21 | 22 | public static Expression> WithUserProjection => 23 | review => new 24 | { 25 | review.Id, 26 | review.ModerationItemId, 27 | review.Comment, 28 | review.Status, 29 | User = UserViewModels.CreateFlat(review.User), 30 | }; 31 | } 32 | } -------------------------------------------------------------------------------- /web-client/components/comments/comment-input.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 44 | 45 | 48 | -------------------------------------------------------------------------------- /web-client/store/auth.js: -------------------------------------------------------------------------------- 1 | const initState = () => ({ 2 | profile: null 3 | }) 4 | 5 | export const state = initState 6 | 7 | const ROLES = { 8 | MODERATOR: "Mod", 9 | ADMIN: "Admin", 10 | } 11 | 12 | export const getters = { 13 | authenticated: state => state.profile != null, 14 | moderator: (state, getters) => getters.authenticated 15 | && (getters.admin || state.profile.role === ROLES.MODERATOR), 16 | admin: (state, getters) => getters.authenticated && state.profile.role === ROLES.ADMIN, 17 | } 18 | 19 | export const mutations = { 20 | saveProfile(state, {profile}) { 21 | state.profile = profile 22 | } 23 | } 24 | 25 | export const actions = { 26 | initialize({commit}) { 27 | return this.$axios.$get('/api/users/me') 28 | .then(profile => commit('saveProfile', {profile})) 29 | .catch(() => { 30 | }) 31 | }, 32 | login() { 33 | if (process.server) return; 34 | const returnUrl = encodeURIComponent(location.href) 35 | window.location = `${this.$config.auth.loginPath}?returnUrl=${returnUrl}` 36 | }, 37 | logout() { 38 | if (process.server) return; 39 | window.location = this.$config.auth.logoutPath 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /hosting/api.raw-coding.net: -------------------------------------------------------------------------------- 1 | server { 2 | server_name api.raw-coding.net; 3 | location / { 4 | proxy_pass http://localhost:5000; 5 | proxy_http_version 1.1; 6 | proxy_set_header Upgrade $http_upgrade; 7 | proxy_set_header Connection keep-alive; 8 | proxy_set_header Host $host; 9 | proxy_cache_bypass $http_upgrade; 10 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 11 | proxy_set_header X-Forwarded-Proto $scheme; 12 | } 13 | 14 | 15 | listen 443 ssl; # managed by Certbot 16 | ssl_certificate /etc/letsencrypt/live/api.raw-coding.net/fullchain.pem; # managed by Certbot 17 | ssl_certificate_key /etc/letsencrypt/live/api.raw-coding.net/privkey.pem; # managed by Certbot 18 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 19 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 20 | 21 | } 22 | server { 23 | if ($host = api.raw-coding.net) { 24 | return 301 https://$host$request_uri; 25 | } # managed by Certbot 26 | 27 | 28 | server_name api.raw-coding.net; 29 | listen 80; 30 | return 404; # managed by Certbot 31 | } -------------------------------------------------------------------------------- /hosting/app.raw-coding.net: -------------------------------------------------------------------------------- 1 | server { 2 | server_name app.raw-coding.net; 3 | 4 | location / { 5 | proxy_pass http://localhost:3000; 6 | proxy_http_version 1.1; 7 | proxy_set_header Upgrade $http_upgrade; 8 | proxy_set_header Connection keep-alive; 9 | proxy_set_header Host $host; 10 | proxy_cache_bypass $http_upgrade; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_set_header X-Forwarded-Proto $scheme; 13 | } 14 | 15 | listen 443 ssl; # managed by Certbot 16 | ssl_certificate /etc/letsencrypt/live/app.raw-coding.net/fullchain.pem; # managed by Certbot 17 | ssl_certificate_key /etc/letsencrypt/live/app.raw-coding.net/privkey.pem; # managed by Certbot 18 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 19 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot 20 | 21 | } 22 | server { 23 | if ($host = app.raw-coding.net) { 24 | return 301 https://$host$request_uri; 25 | } # managed by Certbot 26 | 27 | 28 | listen 80; 29 | server_name app.raw-coding.net; 30 | return 404; # managed by Certbot 31 | } -------------------------------------------------------------------------------- /web-client/components/comments/comment-section.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /web-client/components/item-content-layout.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 40 | 41 | 44 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Pages/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model TrickingLibrary.Api.Pages.Account.Login 3 | 4 | @{ 5 | ViewData["Title"] = "Login"; 6 | Layout = "_Layout"; 7 | } 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 |
21 | @if (Model.CustomErrors.Count > 0) 22 | { 23 |
24 | @foreach (var error in Model.CustomErrors) 25 | { 26 |
@error
27 | } 28 |
29 | } 30 |
31 | 32 |
33 |
34 |
35 | Create Account 36 |
37 |
-------------------------------------------------------------------------------- /TrickingLibrary.Api/Services/Storage/S3FileProvider.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | 4 | namespace TrickingLibrary.Api.Services.Storage 5 | { 6 | public class S3FileProvider : IFileProvider 7 | { 8 | private readonly IS3Client _client; 9 | 10 | public S3FileProvider(IS3Client client) 11 | { 12 | _client = client; 13 | } 14 | 15 | public Task SaveProfileImageAsync(Stream fileStream) 16 | { 17 | var fileName = TrickingLibraryConstants.Files.GenerateProfileFileName(); 18 | return _client.SaveFile(fileName, "image/jpg", fileStream); 19 | } 20 | 21 | public Task SaveVideoAsync(Stream fileStream) 22 | { 23 | var fileName = TrickingLibraryConstants.Files.GenerateConvertedFileName(); 24 | return _client.SaveFile(fileName, "video/mp4", fileStream); 25 | } 26 | 27 | public Task SaveThumbnailAsync(Stream fileStream) 28 | { 29 | var fileName = TrickingLibraryConstants.Files.GenerateThumbnailFileName(); 30 | return _client.SaveFile(fileName, "image/jpg", fileStream); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /web-client/pages/moderation/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 36 | 37 | 40 | -------------------------------------------------------------------------------- /web-client/pages/trick/_trick/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /web-client/components/moderation.js: -------------------------------------------------------------------------------- 1 | const endpointResolver = (type) => { 2 | if (type === MODERATION_TYPES.TRICK) return 'tricks' 3 | if (type === MODERATION_TYPES.CATEGORY) return 'categories' 4 | if (type === MODERATION_TYPES.DIFFICULTY) return 'difficulties' 5 | } 6 | 7 | export const MODERATION_TYPES = { 8 | TRICK: 'trick', 9 | CATEGORY: 'category', 10 | DIFFICULTY: 'difficulty', 11 | } 12 | 13 | export const REVIEW_STATUS = { 14 | APPROVED: 0, 15 | REJECTED: 1, 16 | WAITING: 2, 17 | } 18 | export const VERSION_STATE = { 19 | LIVE: 0, 20 | STAGED: 1, 21 | OUTDATED: 2, 22 | } 23 | 24 | const reviewStatusColor = (status) => { 25 | if (REVIEW_STATUS.APPROVED === status) return "green" 26 | if (REVIEW_STATUS.REJECTED === status) return "red" 27 | if (REVIEW_STATUS.WAITING === status) return "orange" 28 | return '' 29 | } 30 | 31 | const reviewStatusIcon = (status) => { 32 | if (REVIEW_STATUS.APPROVED === status) return "mdi-check" 33 | if (REVIEW_STATUS.REJECTED === status) return "mdi-close" 34 | if (REVIEW_STATUS.WAITING === status) return "mdi-clock" 35 | return '' 36 | } 37 | 38 | export const modItemRenderer = { 39 | methods: { 40 | endpointResolver, 41 | reviewStatusColor, 42 | reviewStatusIcon, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /web-client/pages/profile/_username.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | 33 | 36 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Services/Email/EmailClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Options; 3 | using SendGrid; 4 | using SendGrid.Helpers.Mail; 5 | 6 | namespace TrickingLibrary.Api.Services.Email 7 | { 8 | public class EmailClient 9 | { 10 | private readonly IOptionsMonitor _optionsMonitor; 11 | private readonly SendGridClient _client; 12 | 13 | public EmailClient(IOptionsMonitor optionsMonitor) 14 | { 15 | _optionsMonitor = optionsMonitor; 16 | _client = new SendGridClient(_optionsMonitor.CurrentValue.ApiKey); 17 | } 18 | 19 | public Task SendModeratorInviteAsync(string email, string link) 20 | { 21 | var htmlContent = $@"You are invited to be a moderator on Tricking Library 22 | 23 | follow the link to register."; 24 | var msg = MailHelper.CreateSingleEmail( 25 | new EmailAddress(_optionsMonitor.CurrentValue.From), 26 | new EmailAddress(email), 27 | "Tricking Library Moderator Invite", 28 | "", 29 | htmlContent 30 | ); 31 | return _client.SendEmailAsync(msg); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /web-client/components/trick-list.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /web-client/pages/category/_category/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /web-client/components/submission-feed.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 44 | 45 | 48 | -------------------------------------------------------------------------------- /web-client/pages/difficulty/_difficulty/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /web-client/components/user-header.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/TrickingLibrary.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | af4900d4-9fae-4d2e-b3ba-4f40f4ff2ac2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /web-client/store/library.js: -------------------------------------------------------------------------------- 1 | const initState = () => ({ 2 | dictionary: { 3 | tricks: null, 4 | categories: null, 5 | difficulties: null, 6 | }, 7 | lists: { 8 | tricks: [], 9 | categories: [], 10 | difficulties: [], 11 | } 12 | }) 13 | 14 | export const state = initState 15 | 16 | const setEntities = (state, type, data) => { 17 | state.dictionary[type] = {} 18 | state.lists[type] = [] 19 | data.forEach(x => { 20 | state.lists[type].push(x) 21 | state.dictionary[type][x.id] = x 22 | state.dictionary[type][x.slug] = x 23 | }) 24 | } 25 | 26 | export const mutations = { 27 | setTricks(state, {tricks}) { 28 | setEntities(state, 'tricks', tricks) 29 | }, 30 | setDifficulties(state, {difficulties}) { 31 | setEntities(state, 'difficulties', difficulties) 32 | }, 33 | setCategories(state, {categories}) { 34 | setEntities(state, 'categories', categories) 35 | }, 36 | reset(state) { 37 | Object.assign(state, initState()) 38 | } 39 | } 40 | 41 | export const actions = { 42 | loadContent({commit}) { 43 | return Promise.all([ 44 | this.$axios.$get("/api/tricks").then(tricks => commit('setTricks', {tricks})), 45 | this.$axios.$get("/api/difficulties").then(difficulties => commit('setDifficulties', {difficulties})), 46 | this.$axios.$get("/api/categories").then(categories => commit('setCategories', {categories})), 47 | ]) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/ViewModels/CategoryViewModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using TrickingLibrary.Models; 5 | 6 | namespace TrickingLibrary.Api.ViewModels 7 | { 8 | public static class CategoryViewModels 9 | { 10 | public static readonly Func CreateFlat = FlatProjection.Compile(); 11 | 12 | public static Expression> FlatProjection => 13 | category => new 14 | { 15 | category.Id, 16 | category.Slug, 17 | category.Name, 18 | category.State, 19 | category.Version, 20 | }; 21 | 22 | public static readonly Func Create = Projection.Compile(); 23 | 24 | public static Expression> Projection => 25 | category => new 26 | { 27 | category.Id, 28 | category.Slug, 29 | category.Name, 30 | category.Description, 31 | category.Version, 32 | category.State, 33 | Updated = category.Updated.ToLocalTime().ToString("HH:mm dd/MM/yyyy"), 34 | Tricks = category.Tricks.AsQueryable() 35 | .Where(x => x.Active) 36 | .Select(x => x.TrickId) 37 | .ToList(), 38 | }; 39 | } 40 | } -------------------------------------------------------------------------------- /web-client/components/comments/comment-body.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 42 | 43 | 46 | -------------------------------------------------------------------------------- /web-client/components/profile-completed-tricks.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 44 | 45 | 48 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/ViewModels/DifficultyViewModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using TrickingLibrary.Models; 5 | 6 | namespace TrickingLibrary.Api.ViewModels 7 | { 8 | public static class DifficultyViewModels 9 | { 10 | public static readonly Func CreateFlat = ProjectionFlat.Compile(); 11 | 12 | public static Expression> ProjectionFlat => 13 | difficulty => new 14 | { 15 | difficulty.Id, 16 | difficulty.Name, 17 | difficulty.State, 18 | difficulty.Slug, 19 | difficulty.Version, 20 | }; 21 | 22 | public static readonly Func Create = Projection.Compile(); 23 | 24 | public static Expression> Projection => 25 | difficulty => new 26 | { 27 | difficulty.Id, 28 | difficulty.Name, 29 | difficulty.Description, 30 | difficulty.Slug, 31 | difficulty.Version, 32 | difficulty.State, 33 | Updated = difficulty.Updated.ToLocalTime().ToString("HH:mm dd/MM/yyyy"), 34 | Tricks = difficulty.Tricks.AsQueryable() 35 | .Where(x => x.Active) 36 | .Select(x => x.TrickId) 37 | .ToList(), 38 | }; 39 | } 40 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Pages/Account/Moderator.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model TrickingLibrary.Api.Pages.Account.Moderator 3 | 4 | @{ 5 | ViewData["Title"] = "Register"; 6 | Layout = "_Layout"; 7 | } 8 | 9 |
10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 | @if (Model.CustomErrors.Count > 0) 33 | { 34 |
35 | @foreach (var error in Model.CustomErrors) 36 | { 37 |
@error
38 | } 39 |
40 | } 41 |
42 | 43 |
44 |
-------------------------------------------------------------------------------- /web-client/pages/admin/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 52 | 53 | 56 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Services/Storage/LinodeS3Client.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using Amazon.S3; 4 | using Amazon.S3.Model; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace TrickingLibrary.Api.Services.Storage 8 | { 9 | public class LinodeS3Client : IS3Client 10 | { 11 | private readonly S3Settings _settings; 12 | 13 | public LinodeS3Client(IOptionsMonitor optionsMonitor) 14 | { 15 | _settings = optionsMonitor.CurrentValue; 16 | } 17 | 18 | public async Task SaveFile(string fileName, string mime, Stream fileStream) 19 | { 20 | using var client = Client; 21 | var request = new PutObjectRequest 22 | { 23 | BucketName = _settings.Bucket, 24 | Key = $"{_settings.Root}/{fileName}", 25 | ContentType = mime, 26 | InputStream = fileStream, 27 | CannedACL = S3CannedACL.PublicRead, 28 | }; 29 | await client.PutObjectAsync(request); 30 | return ObjectUrl(fileName); 31 | } 32 | 33 | private string ObjectUrl(string fileName) => 34 | $"{_settings.ServiceUrl}/{_settings.Bucket}/{_settings.Root}/{fileName}"; 35 | 36 | private AmazonS3Client Client => new AmazonS3Client( 37 | _settings.AccessKey, 38 | _settings.SecretKey, 39 | new AmazonS3Config 40 | { 41 | ServiceURL = _settings.ServiceUrl, 42 | } 43 | ); 44 | } 45 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Pages/Account/Register.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model TrickingLibrary.Api.Pages.Account.Register 3 | 4 | @{ 5 | ViewData["Title"] = "Register"; 6 | Layout = "_Layout"; 7 | } 8 | 9 |
10 | 11 | @if (Model.CustomErrors.Count > 0) 12 | { 13 |
14 | @foreach (var error in Model.CustomErrors) 15 | { 16 |
@error
17 | } 18 |
19 | } 20 |
21 | 22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 | 37 | 38 | 39 |
40 |
41 | 42 |
43 |
44 |
45 | Back to Login 46 |
47 |
-------------------------------------------------------------------------------- /TrickingLibrary.Api/Pages/Account/Login.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | using Microsoft.Extensions.Hosting; 8 | 9 | namespace TrickingLibrary.Api.Pages.Account 10 | { 11 | public class Login : BasePage 12 | { 13 | [BindProperty] public LoginForm Form { get; set; } 14 | 15 | public void OnGet(string returnUrl) 16 | { 17 | Form = new LoginForm {ReturnUrl = returnUrl}; 18 | } 19 | 20 | public async Task OnPostAsync([FromServices] SignInManager signInManager) 21 | { 22 | if (!ModelState.IsValid) 23 | return Page(); 24 | 25 | var signInResult = await signInManager 26 | .PasswordSignInAsync(Form.Username, Form.Password, true, false); 27 | 28 | if (signInResult.Succeeded) 29 | { 30 | return Redirect(Form.ReturnUrl); 31 | } 32 | 33 | CustomErrors.Add("Invalid login attempt, please try again."); 34 | 35 | return Page(); 36 | } 37 | 38 | public class LoginForm 39 | { 40 | [Required] public string ReturnUrl { get; set; } 41 | [Required] public string Username { get; set; } 42 | 43 | [Required] 44 | [DataType(DataType.Password)] 45 | public string Password { get; set; } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Services/Storage/RegisterService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Configuration; 3 | using TrickingLibrary.Api; 4 | using TrickingLibrary.Api.BackgroundServices.VideoEditing; 5 | using TrickingLibrary.Api.Services.Storage; 6 | using TrickingLibrary.Api.Settings; 7 | 8 | // ReSharper disable once CheckNamespace 9 | namespace Microsoft.Extensions.DependencyInjection 10 | { 11 | public static class RegisterService 12 | { 13 | public static IServiceCollection AddFileServices(this IServiceCollection services, IConfiguration config) 14 | { 15 | var settingsSection = config.GetSection(nameof(FileSettings)); 16 | var settings = settingsSection.Get(); 17 | services.Configure(settingsSection); 18 | 19 | services.AddSingleton(); 20 | if (settings.Provider.Equals(TrickingLibraryConstants.Files.Providers.Local)) 21 | { 22 | services.AddSingleton(); 23 | } 24 | else if (settings.Provider.Equals(TrickingLibraryConstants.Files.Providers.S3)) 25 | { 26 | services.Configure(config.GetSection(nameof(S3Settings))); 27 | services.AddSingleton(); 28 | services.AddSingleton(); 29 | } 30 | else 31 | { 32 | throw new Exception($"Invalid File Manager Provider: {settings.Provider}"); 33 | } 34 | 35 | return services; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /web-client/components/video-player.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 44 | 45 | 78 | -------------------------------------------------------------------------------- /web-client/components/nav-bar-search.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 51 | 52 | 55 | -------------------------------------------------------------------------------- /web-client/components/comments/comment.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 52 | 53 | 56 | -------------------------------------------------------------------------------- /web-client/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Certificates 17 | server.cert 18 | server.key 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | # parcel-bundler cache (https://parceljs.org/) 67 | .cache 68 | 69 | # next.js build output 70 | .next 71 | 72 | # nuxt.js build output 73 | .nuxt 74 | 75 | # Nuxt generate 76 | dist 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless 83 | 84 | # IDE / Editor 85 | .idea 86 | 87 | # Service worker 88 | sw.* 89 | 90 | # macOS 91 | .DS_Store 92 | 93 | # Vim swap files 94 | *.swp 95 | -------------------------------------------------------------------------------- /TrickingLibrary.Data/VersionMigrations/CategoryMigrationContext.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.EntityFrameworkCore; 3 | using TrickingLibrary.Models; 4 | using TrickingLibrary.Models.Abstractions; 5 | 6 | namespace TrickingLibrary.Data.VersionMigrations 7 | { 8 | public class CategoryMigrationContext : IEntityMigrationContext 9 | { 10 | private readonly AppDbContext _ctx; 11 | 12 | public CategoryMigrationContext(AppDbContext ctx) 13 | { 14 | _ctx = ctx; 15 | } 16 | 17 | public IQueryable GetSource() 18 | { 19 | return _ctx.Categories; 20 | } 21 | 22 | public void MigrateRelationships(int current, int target) 23 | { 24 | if (current > 0) 25 | { 26 | var relationships = _ctx.TrickCategories 27 | .Where(x => x.CategoryId == current) 28 | .ToList(); 29 | 30 | foreach (var relationship in relationships) 31 | { 32 | relationship.Active = false; 33 | _ctx.Add(new TrickCategory 34 | { 35 | CategoryId = target, 36 | TrickId = relationship.TrickId, 37 | Active = true, 38 | }); 39 | } 40 | } 41 | } 42 | 43 | public void VoidRelationships(int id) 44 | { 45 | var relationships = _ctx.TrickCategories 46 | .Where(x => x.CategoryId == id) 47 | .ToList(); 48 | 49 | foreach (var relationship in relationships) 50 | relationship.Active = false; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /web-client/components/feed.js: -------------------------------------------------------------------------------- 1 | export const feed = (order) => ({ 2 | props: { 3 | contentEndpoint: { 4 | required: false, 5 | type: String, 6 | } 7 | }, 8 | data: () => ({ 9 | content: [], 10 | cursor: 0, 11 | limit: 10, 12 | order: order, 13 | started: false, 14 | finished: false, 15 | loading: false, 16 | }), 17 | watch: { 18 | 'order': function () { 19 | this.reloadContent() 20 | } 21 | }, 22 | methods: { 23 | getContentUrl() { 24 | return `${this.contentEndpoint}${this.query}` 25 | }, 26 | onScroll() { 27 | if (this.finished || this.loading) return; 28 | const loadMore = document.body.offsetHeight - (window.pageYOffset + window.innerHeight) < 500 29 | 30 | if (loadMore) { 31 | this.loadContent() 32 | } 33 | }, 34 | reloadContent() { 35 | this.content = [] 36 | this.cursor = 0 37 | this.finished = false 38 | this.started = false 39 | return this.loadContent() 40 | }, 41 | loadContent() { 42 | this.started = true 43 | this.loading = true 44 | return this.$axios.$get(this.getContentUrl()) 45 | .then(content => { 46 | this.finished = content.length < this.limit 47 | this.parseContent(content) 48 | this.cursor += content.length; 49 | }) 50 | .finally(() => this.loading = false) 51 | }, 52 | parseContent(content) { 53 | content.forEach(x => { 54 | if (!this.content.some(y => y.id === x.id)) 55 | this.content.push(x) 56 | }) 57 | } 58 | }, 59 | computed: { 60 | query() { 61 | return `?order=${this.order}&cursor=${this.cursor}&limit=${this.limit}` 62 | } 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /.github/workflows/api-build-and-deply.yaml: -------------------------------------------------------------------------------- 1 | name: API Build and Deploy to Linode 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | name: Build our App 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Pull Code 12 | uses: actions/checkout@v2 13 | - uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: '3.1.x' 16 | - name: Restore Dependencies 17 | run: dotnet restore 18 | - name: Create Production Build 19 | run: dotnet publish -c Release --no-restore 20 | - name: Inject Secrets 21 | uses: microsoft/variable-substitution@v1 22 | with: 23 | files: './TrickingLibrary.Api/bin/Release/netcoreapp3.1/publish/appsettings.Production.json' 24 | env: 25 | ConnectionStrings.Default: ${{ secrets.POSTGRESQL_CONNECTION_STRING }} 26 | AdminPassword: ${{ secrets.ADMIN_PASSWORD }} 27 | SendGridOptions.ApiKey: ${{ secrets.SEND_GRID_API_KEY }} 28 | SendGridOptions.From: ${{ secrets.SEND_GRID_FROM }} 29 | S3Settings.AccessKey: ${{ secrets.S3_ACCESS_KEY }} 30 | S3Settings.SecretKey: ${{ secrets.S3_SECRET_KEY }} 31 | S3Settings.ServiceUrl: ${{ secrets.S3_SERVICE_URL }} 32 | S3Settings.Bucket: ${{ secrets.S3_BUCKET }} 33 | - name: Push to Linode 34 | run: | 35 | echo "$ssh_key" > ~/ssh_key 36 | chmod 600 ~/ssh_key 37 | rsync -e "ssh -i ~/ssh_key -o StrictHostKeyChecking=no" -avzr ./TrickingLibrary.Api/bin/Release/netcoreapp3.1/publish/ "$user"@"$target_ip":/var/tricking-library/api 38 | env: 39 | ssh_key: ${{ secrets.CICD_SSH }} 40 | user: ${{ secrets.CICD_USER }} 41 | target_ip: ${{ secrets.LINODE_IP }} 42 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Services/Storage/TemporaryFileStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace TrickingLibrary.Api.Services.Storage 8 | { 9 | public class TemporaryFileStorage 10 | { 11 | private readonly FileSettings _settings; 12 | 13 | public TemporaryFileStorage(IOptionsMonitor optionsMonitor) 14 | { 15 | _settings = optionsMonitor.CurrentValue; 16 | } 17 | 18 | public async Task SaveTemporaryFile(IFormFile video) 19 | { 20 | var fileName = string.Concat( 21 | TrickingLibraryConstants.Files.TempPrefix, 22 | DateTime.Now.Ticks, 23 | Path.GetExtension(video.FileName) 24 | ); 25 | var savePath = GetSavePath(fileName); 26 | 27 | await using (var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write)) 28 | { 29 | await video.CopyToAsync(fileStream); 30 | } 31 | 32 | return fileName; 33 | } 34 | 35 | public bool TemporaryFileExists(string fileName) 36 | { 37 | var path = GetSavePath(fileName); 38 | return File.Exists(path); 39 | } 40 | 41 | public void DeleteTemporaryFile(string fileName) 42 | { 43 | var path = GetSavePath(fileName); 44 | if (File.Exists(path)) 45 | { 46 | File.Delete(path); 47 | } 48 | } 49 | 50 | public string GetSavePath(string fileName) 51 | { 52 | return Path.Combine(_settings.WorkingDirectory, fileName); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /TrickingLibrary.Data/VersionMigrations/DifficultyMigrationContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.EntityFrameworkCore; 4 | using TrickingLibrary.Models; 5 | using TrickingLibrary.Models.Abstractions; 6 | 7 | namespace TrickingLibrary.Data.VersionMigrations 8 | { 9 | public class DifficultyMigrationContext : IEntityMigrationContext 10 | { 11 | private readonly AppDbContext _ctx; 12 | 13 | public DifficultyMigrationContext(AppDbContext ctx) 14 | { 15 | _ctx = ctx; 16 | } 17 | 18 | public IQueryable GetSource() 19 | { 20 | return _ctx.Difficulties; 21 | } 22 | 23 | public void MigrateRelationships(int current, int target) 24 | { 25 | if (current > 0) 26 | { 27 | var relationships = _ctx.TrickDifficulties 28 | .Include(x => x.Trick) 29 | .Where(x => x.DifficultyId == current) 30 | .ToList(); 31 | 32 | foreach (var relationship in relationships) 33 | { 34 | _ctx.Add(new TrickDifficulty 35 | { 36 | DifficultyId = target, 37 | TrickId = relationship.TrickId, 38 | Active = relationship.Active, 39 | }); 40 | if (relationship.Trick.State == VersionState.Staged) 41 | _ctx.Remove(relationship); 42 | else 43 | relationship.Active = false; 44 | } 45 | } 46 | } 47 | 48 | public void VoidRelationships(int id) 49 | { 50 | throw new NotImplementedException(); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/ViewModels/UserViewModels.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using TrickingLibrary.Models; 5 | 6 | namespace TrickingLibrary.Api.ViewModels 7 | { 8 | public static class UserViewModels 9 | { 10 | public static Expression> Projection => 11 | user => new 12 | { 13 | user.Id, 14 | user.Username, 15 | user.Image, 16 | Submissions = user.Submissions.AsQueryable().Select(x => new 17 | { 18 | x.Id, 19 | x.TrickId, 20 | Score = x.Votes.Sum(v => v.Value), 21 | }) 22 | .ToList(), 23 | }; 24 | 25 | public static readonly Func CreateFlatCache = FlatProjection.Compile(); 26 | public static object CreateFlat(User user) => CreateFlatCache(user); 27 | 28 | public static Expression> FlatProjection => 29 | user => new 30 | { 31 | user.Id, 32 | user.Username, 33 | user.Image, 34 | }; 35 | 36 | public static Expression> ProfileProjection(string role) => 37 | user => new 38 | { 39 | user.Id, 40 | user.Username, 41 | user.Image, 42 | Submissions = user.Submissions.AsQueryable().Select(x => new 43 | { 44 | x.Id, 45 | x.TrickId, 46 | Score = x.Votes.Sum(v => v.Value), 47 | }) 48 | .ToList(), 49 | Role = role, 50 | }; 51 | } 52 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/TrickingLibraryConstants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Claims; 3 | 4 | namespace TrickingLibrary.Api 5 | { 6 | public struct TrickingLibraryConstants 7 | { 8 | public struct Policies 9 | { 10 | // public const string Anon = nameof(Anon); 11 | // public const string User = IdentityServerConstants.LocalApi.PolicyName; 12 | public const string Mod = nameof(Mod); 13 | public const string Admin = nameof(Admin); 14 | } 15 | 16 | public struct IdentityResources 17 | { 18 | public const string RoleScope = "role"; 19 | } 20 | 21 | public struct Claims 22 | { 23 | public const string Role = "role"; 24 | public static readonly Claim ModeratorClaim = new Claim(Role, Roles.Mod); 25 | } 26 | 27 | public struct Roles 28 | { 29 | public const string Mod = nameof(Mod); 30 | public const string Admin = nameof(Admin); 31 | } 32 | 33 | public struct Files 34 | { 35 | public struct Providers 36 | { 37 | public const string Local = nameof(Local); 38 | public const string S3 = nameof(S3); 39 | } 40 | 41 | public const string TempPrefix = "temp_"; 42 | public const string ConvertedPrefix = "c"; 43 | public const string ThumbnailPrefix = "t"; 44 | public const string ProfilePrefix = "p"; 45 | 46 | public static string GenerateConvertedFileName() => $"{ConvertedPrefix}{DateTime.Now.Ticks}.mp4"; 47 | public static string GenerateThumbnailFileName() => $"{ThumbnailPrefix}{DateTime.Now.Ticks}.jpg"; 48 | public static string GenerateProfileFileName() => $"{ProfilePrefix}{DateTime.Now.Ticks}.jpg"; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /TrickingLibrary.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrickingLibrary.Api", "TrickingLibrary.Api\TrickingLibrary.Api.csproj", "{D8847E97-203B-4B91-9F8F-CD17D5CA5952}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrickingLibrary.Models", "TrickingLibrary.Models\TrickingLibrary.Models.csproj", "{AA586C4F-DDF7-4113-BECA-9A272499FDD2}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrickingLibrary.Data", "TrickingLibrary.Data\TrickingLibrary.Data.csproj", "{41A6FC3E-7BAD-43C1-B4B5-EE7B752719B7}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {D8847E97-203B-4B91-9F8F-CD17D5CA5952}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {D8847E97-203B-4B91-9F8F-CD17D5CA5952}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {D8847E97-203B-4B91-9F8F-CD17D5CA5952}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {D8847E97-203B-4B91-9F8F-CD17D5CA5952}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {AA586C4F-DDF7-4113-BECA-9A272499FDD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {AA586C4F-DDF7-4113-BECA-9A272499FDD2}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {AA586C4F-DDF7-4113-BECA-9A272499FDD2}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {AA586C4F-DDF7-4113-BECA-9A272499FDD2}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {41A6FC3E-7BAD-43C1-B4B5-EE7B752719B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {41A6FC3E-7BAD-43C1-B4B5-EE7B752719B7}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {41A6FC3E-7BAD-43C1-B4B5-EE7B752719B7}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {41A6FC3E-7BAD-43C1-B4B5-EE7B752719B7}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /TrickingLibrary.Api/Services/Storage/LocalFileProvider.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace TrickingLibrary.Api.Services.Storage 7 | { 8 | public class LocalFileProvider : IFileProvider 9 | { 10 | private readonly IWebHostEnvironment _env; 11 | private readonly FileSettings _settings; 12 | 13 | public LocalFileProvider( 14 | IOptionsMonitor fileSettingsMonitor, 15 | IWebHostEnvironment env) 16 | { 17 | _settings = fileSettingsMonitor.CurrentValue; 18 | _env = env; 19 | } 20 | 21 | public async Task SaveProfileImageAsync(Stream fileStream) 22 | { 23 | var fileName = TrickingLibraryConstants.Files.GenerateProfileFileName(); 24 | await SaveFile(fileStream, fileName); 25 | return $"{_settings.ImageUrl}/{fileName}"; 26 | } 27 | 28 | public async Task SaveVideoAsync(Stream fileStream) 29 | { 30 | var fileName = TrickingLibraryConstants.Files.GenerateConvertedFileName(); 31 | await SaveFile(fileStream, fileName); 32 | return $"{_settings.VideoUrl}/{fileName}"; 33 | } 34 | 35 | public async Task SaveThumbnailAsync(Stream fileStream) 36 | { 37 | var fileName = TrickingLibraryConstants.Files.GenerateThumbnailFileName(); 38 | await SaveFile(fileStream, fileName); 39 | return $"{_settings.ImageUrl}/{fileName}"; 40 | } 41 | 42 | private async Task SaveFile(Stream fileStream, string fileName) 43 | { 44 | var savePath = Path.Combine(_env.WebRootPath, fileName); 45 | await using (var stream = File.Create(savePath)) 46 | { 47 | await fileStream.CopyToAsync(stream); 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /TrickingLibrary.Api/Pages/Account/Register.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace TrickingLibrary.Api.Pages.Account 8 | { 9 | public class Register : BasePage 10 | { 11 | [BindProperty] public RegisterForm Form { get; set; } 12 | 13 | public void OnGet(string returnUrl) 14 | { 15 | Form = new RegisterForm {ReturnUrl = returnUrl}; 16 | } 17 | 18 | public async Task OnPostAsync( 19 | [FromServices] UserManager userManager, 20 | [FromServices] SignInManager signInManager) 21 | { 22 | if (!ModelState.IsValid) 23 | return Page(); 24 | 25 | var user = new IdentityUser(Form.Username) {Email = Form.Email}; 26 | 27 | var createUserResult = await userManager.CreateAsync(user, Form.Password); 28 | 29 | if (createUserResult.Succeeded) 30 | { 31 | await signInManager.SignInAsync(user, true); 32 | 33 | return Redirect(Form.ReturnUrl); 34 | } 35 | 36 | foreach (var error in createUserResult.Errors) 37 | { 38 | CustomErrors.Add(error.Description); 39 | } 40 | 41 | return Page(); 42 | } 43 | 44 | public class RegisterForm 45 | { 46 | [Required] public string ReturnUrl { get; set; } 47 | [Required] public string Email { get; set; } 48 | [Required] public string Username { get; set; } 49 | 50 | [Required] 51 | [DataType(DataType.Password)] 52 | public string Password { get; set; } 53 | 54 | [Required] 55 | [DataType(DataType.Password)] 56 | [Compare(nameof(Password))] 57 | public string ConfirmPassword { get; set; } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /web-client/components/front-page/front-page-trick-feed.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 59 | 60 | 65 | -------------------------------------------------------------------------------- /web-client/components/content-creation/content-creation-dialog.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /TrickingLibrary.Data/AppDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using TrickingLibrary.Models; 3 | using TrickingLibrary.Models.Moderation; 4 | 5 | namespace TrickingLibrary.Data 6 | { 7 | public class AppDbContext : DbContext 8 | { 9 | public AppDbContext(DbContextOptions options) : base(options) 10 | { 11 | } 12 | 13 | public DbSet Tricks { get; set; } 14 | public DbSet Submissions { get; set; } 15 | public DbSet SubmissionVotes { get; set; } 16 | public DbSet Difficulties { get; set; } 17 | public DbSet TrickDifficulties { get; set; } 18 | public DbSet TrickRelationships { get; set; } 19 | public DbSet Categories { get; set; } 20 | public DbSet TrickCategories { get; set; } 21 | public DbSet