├── docs ├── ddl │ ├── 01-creazione-tabella-Courses.sql │ ├── 02-inserimento-dati-in-tabella-Courses.sql │ ├── 03-creazione-tabella-Lessons.sql │ ├── 04-inserimento-dati-in-tabella-Lessons.sql │ ├── 05-creazione-vincolo-unique-su-colonna-Title.sql │ ├── 06-aggiunta-colonna-per-concorrenza-ottimistica.sql │ └── 07-inserimento-utenti-e-aggiornamento-authorid.sql ├── images │ └── my-course.png ├── slide │ ├── Sezione01.pdf │ ├── Sezione02.pdf │ ├── Sezione03.pdf │ ├── Sezione04.pdf │ ├── Sezione05.pdf │ ├── Sezione06.pdf │ ├── Sezione07.pdf │ ├── Sezione08.pdf │ ├── Sezione09.pdf │ ├── Sezione10.pdf │ ├── Sezione11.pdf │ ├── Sezione12.pdf │ ├── Sezione13.pdf │ ├── Sezione14.pdf │ ├── Sezione15.pdf │ ├── Sezione16.pdf │ ├── Sezione17.pdf │ ├── Sezione18.pdf │ ├── Sezione19.pdf │ ├── Sezione20.pdf │ └── Sezione21.pdf └── specifica.md ├── global.json ├── readme.md ├── scripts ├── imm1.png ├── imm2.png ├── lambda │ ├── .vscode │ │ └── launch.json │ ├── main.csx │ └── omnisharp.json ├── linq │ ├── .vscode │ │ └── launch.json │ ├── main.csx │ └── omnisharp.json └── readme.md └── src └── MyCourse ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Areas └── Identity │ ├── IdentityHostingStartup.cs │ └── Pages │ ├── Account │ ├── AccessDenied.cshtml │ ├── AccessDenied.cshtml.cs │ ├── ConfirmEmail.cshtml │ ├── ConfirmEmail.cshtml.cs │ ├── ConfirmEmailChange.cshtml │ ├── ConfirmEmailChange.cshtml.cs │ ├── ExternalLogin.cshtml │ ├── ExternalLogin.cshtml.cs │ ├── ForgotPassword.cshtml │ ├── ForgotPassword.cshtml.cs │ ├── ForgotPasswordConfirmation.cshtml │ ├── ForgotPasswordConfirmation.cshtml.cs │ ├── Lockout.cshtml │ ├── Lockout.cshtml.cs │ ├── Login.cshtml │ ├── Login.cshtml.cs │ ├── LoginWith2fa.cshtml │ ├── LoginWith2fa.cshtml.cs │ ├── LoginWithRecoveryCode.cshtml │ ├── LoginWithRecoveryCode.cshtml.cs │ ├── Logout.cshtml │ ├── Logout.cshtml.cs │ ├── Manage │ │ ├── ChangePassword.cshtml │ │ ├── ChangePassword.cshtml.cs │ │ ├── DeletePersonalData.cshtml │ │ ├── DeletePersonalData.cshtml.cs │ │ ├── Disable2fa.cshtml │ │ ├── Disable2fa.cshtml.cs │ │ ├── DownloadPersonalData.cshtml │ │ ├── DownloadPersonalData.cshtml.cs │ │ ├── Email.cshtml │ │ ├── Email.cshtml.cs │ │ ├── EnableAuthenticator.cshtml │ │ ├── EnableAuthenticator.cshtml.cs │ │ ├── ExternalLogins.cshtml │ │ ├── ExternalLogins.cshtml.cs │ │ ├── GenerateRecoveryCodes.cshtml │ │ ├── GenerateRecoveryCodes.cshtml.cs │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ ├── ManageNavPages.cs │ │ ├── PersonalData.cshtml │ │ ├── PersonalData.cshtml.cs │ │ ├── ResetAuthenticator.cshtml │ │ ├── ResetAuthenticator.cshtml.cs │ │ ├── SetPassword.cshtml │ │ ├── SetPassword.cshtml.cs │ │ ├── ShowRecoveryCodes.cshtml │ │ ├── ShowRecoveryCodes.cshtml.cs │ │ ├── TwoFactorAuthentication.cshtml │ │ ├── TwoFactorAuthentication.cshtml.cs │ │ ├── _Layout.cshtml │ │ ├── _ManageNav.cshtml │ │ ├── _StatusMessage.cshtml │ │ └── _ViewImports.cshtml │ ├── Register.cshtml │ ├── Register.cshtml.cs │ ├── RegisterConfirmation.cshtml │ ├── RegisterConfirmation.cshtml.cs │ ├── ResetPassword.cshtml │ ├── ResetPassword.cshtml.cs │ ├── ResetPasswordConfirmation.cshtml │ ├── ResetPasswordConfirmation.cshtml.cs │ ├── _StatusMessage.cshtml │ └── _ViewImports.cshtml │ ├── _ValidationScriptsPartial.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── Controllers ├── CoursesController.cs ├── ErrorController.cs ├── HomeController.cs └── LessonsController.cs ├── Customizations ├── Authorization │ ├── AuthorizePolicy.cs │ └── AuthorizeRole.cs ├── Identity │ ├── CommonPasswordValidator.cs │ └── CustomClaimsPrincipalFactory.cs ├── ModelBinders │ ├── CourseListInputModelBinder.cs │ ├── DecimalModelBinder.cs │ └── DecimalModelBinderProvider.cs ├── TagHelpers │ ├── HtmlSanitizeTagHelper.cs │ ├── InputNumberTagHelper.cs │ ├── OrderLinkTagHelper.cs │ ├── PriceTagHelper.cs │ └── RatingTagHelper.cs └── ViewComponents │ └── PaginationBarViewComponent.cs ├── Data ├── MyCourse.db └── MyCourse.old.db ├── GlobalUsings.cs ├── Migrations ├── 20200510175703_InitialMigration.Designer.cs ├── 20200510175703_InitialMigration.cs ├── 20200517144246_UniqueCourseTitle.Designer.cs ├── 20200517144246_UniqueCourseTitle.cs ├── 20200517152859_TriggersCourseVersion.Designer.cs ├── 20200517152859_TriggersCourseVersion.cs ├── 20200531124830_LessonOrder.Designer.cs ├── 20200531124830_LessonOrder.cs ├── 20200531124956_LessonVersion.Designer.cs ├── 20200531124956_LessonVersion.cs ├── 20200614144118_CourseStatus.Designer.cs ├── 20200614144118_CourseStatus.cs ├── 20200705180058_IdentityModel.Designer.cs ├── 20200705180058_IdentityModel.cs ├── 20200822153858_IdentityApplicationUser.Designer.cs ├── 20200822153858_IdentityApplicationUser.cs ├── 20200906191648_CourseAuthor.Designer.cs ├── 20200906191648_CourseAuthor.cs ├── 20210705180430_Subscriptions.Designer.cs ├── 20210705180430_Subscriptions.cs ├── 20210927072440_TriggerCourseRating.Designer.cs ├── 20210927072440_TriggerCourseRating.cs └── MyCourseDbContextModelSnapshot.cs ├── Models ├── Authorization │ ├── CourseAuthorRequirement.cs │ ├── CourseAuthorRequirementHandler.cs │ ├── CourseLimitRequirement.cs │ ├── CourseLimitRequirementHandler.cs │ ├── CourseSubscriberRequirement.cs │ ├── CourseSubscriberRequirementHandler.cs │ └── MultiAuthorizationPolicyProvider.cs ├── Entities │ ├── ApplicationUser.cs │ ├── Course.cs │ ├── Lesson.cs │ └── Subscription.cs ├── Enums │ ├── CourseStatus.cs │ ├── Currency.cs │ ├── Persistence.cs │ ├── Policy.cs │ └── Role.cs ├── Exceptions │ ├── Application │ │ ├── CourseImageInvalidException.cs │ │ ├── CourseNotFoundException.cs │ │ ├── CourseSubscriptionException.cs │ │ ├── CourseSubscriptionNotFoundException.cs │ │ ├── CourseTitleUnavailableException.cs │ │ ├── InvalidVoteException.cs │ │ ├── LessonNotFoundException.cs │ │ ├── OptimisticConcurrencyException.cs │ │ ├── SendException.cs │ │ └── UserUnkownException.cs │ └── Infrastructure │ │ ├── ConstraintViolationException.cs │ │ ├── ImagePersistenceException.cs │ │ └── PaymentGatewayException.cs ├── InputModels │ ├── Courses │ │ ├── CourseCreateInputModel.cs │ │ ├── CourseDeleteInputModel.cs │ │ ├── CourseEditInputModel.cs │ │ ├── CourseListInputModel.cs │ │ ├── CoursePayInputModel.cs │ │ ├── CourseSubscribeInputModel.cs │ │ └── CourseVoteInputModel.cs │ ├── Lessons │ │ ├── LessonCreateInputModel.cs │ │ ├── LessonDeleteInputModel.cs │ │ └── LessonEditInputModel.cs │ └── Users │ │ └── UserRoleInputModel.cs ├── Options │ ├── ConnectionStringsOptions.cs │ ├── CoursesOptions.cs │ ├── PaypalOptions.cs │ ├── SmtpOptions.cs │ ├── StripeOptions.cs │ └── UsersOptions.cs ├── Services │ ├── Application │ │ ├── Courses │ │ │ ├── AdoNetCourseService.cs │ │ │ ├── CourseService.cs │ │ │ ├── EfCoreCourseService.cs │ │ │ ├── ICachedCourseService.cs │ │ │ ├── ICourseService.cs │ │ │ └── MemoryCacheCourseService.cs │ │ └── Lessons │ │ │ ├── AdoNetLessonService.cs │ │ │ ├── EfCoreLessonService.cs │ │ │ ├── ICachedLessonService.cs │ │ │ ├── ILessonService.cs │ │ │ └── MemoryCacheLessonService.cs │ └── Infrastructure │ │ ├── AdoNetUserStore.cs │ │ ├── IDatabaseAccessor.cs │ │ ├── IEmailClient.cs │ │ ├── IImagePersister.cs │ │ ├── IPaymentGateway.cs │ │ ├── ITransactionLogger.cs │ │ ├── InsecureImagePersister.cs │ │ ├── LocalTransactionLogger.cs │ │ ├── MagickNetImagePersister.cs │ │ ├── MailKitEmailSender.cs │ │ ├── MyCourseDbContext.cs │ │ ├── PaypalPaymentGateway.cs │ │ ├── SqliteDatabaseAccessor.cs │ │ └── StripePaymentGateway.cs ├── ValueObjects │ ├── Money.cs │ └── Sql.cs └── ViewModels │ ├── Courses │ ├── CourseDetailViewModel.cs │ ├── CourseListViewModel.cs │ └── CourseViewModel.cs │ ├── Home │ └── HomeViewModel.cs │ ├── IPaginationInfo.cs │ ├── Lessons │ ├── LessonDetailViewModel.cs │ └── LessonViewModel.cs │ └── ListViewModel.cs ├── MyCourse.csproj ├── Pages ├── Admin │ ├── Users.cshtml │ └── Users.cshtml.cs ├── Contact.cshtml ├── Contact.cshtml.cs ├── Privacy.cshtml ├── _ViewImports.cshtml └── _ViewStart.cshtml ├── Program.cs ├── Properties └── launchSettings.json ├── Startup.cs ├── Views ├── Courses │ ├── Create.cshtml │ ├── Detail.cshtml │ ├── Edit.cshtml │ ├── Index.cshtml │ └── Vote.cshtml ├── Error │ ├── CourseNotFound.cshtml │ └── Index.cshtml ├── Home │ └── Index.cshtml ├── Lessons │ ├── Create.cshtml │ ├── Detail.cshtml │ └── Edit.cshtml ├── Shared │ ├── Components │ │ └── PaginationBar │ │ │ └── Default.cshtml │ ├── Courses │ │ └── _CourseLine.cshtml │ ├── _Layout.cshtml │ ├── _LayoutMinimal.cshtml │ ├── _LoginPartial.cshtml │ ├── _Summernote.cshtml │ └── _Validation.cshtml ├── _ViewImports.cshtml └── _ViewStart.cshtml ├── appsettings.Development.json ├── appsettings.json ├── libman.json └── wwwroot ├── Courses ├── 1.jpg ├── 10.jpg ├── 11.jpg ├── 12.jpg ├── 13.jpg ├── 14.jpg ├── 15.jpg ├── 16.jpg ├── 17.jpg ├── 18.jpg ├── 19.jpg ├── 2.jpg ├── 20.jpg ├── 21.jpg ├── 22.jpg ├── 23.jpg ├── 24.jpg ├── 25.jpg ├── 26.jpg ├── 27.jpg ├── 28.jpg ├── 29.jpg ├── 3.jpg ├── 30.jpg ├── 31.jpg ├── 32.jpg ├── 4.jpg ├── 5.jpg ├── 6.jpg ├── 7.jpg ├── 8.jpg ├── 9.jpg └── default.png ├── images └── hero.jpg ├── lib ├── bootstrap │ └── dist │ │ ├── css │ │ └── bootstrap.min.css │ │ └── js │ │ └── bootstrap.min.js ├── font-awesome │ ├── css │ │ └── all.min.css │ └── webfonts │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.svg │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.svg │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 ├── jquery-validation-unobtrusive │ └── dist │ │ └── jquery.validate.unobtrusive.min.js ├── jquery-validation │ └── dist │ │ ├── additional-methods.min.js │ │ ├── jquery.validate.min.js │ │ └── localization │ │ └── messages_it.min.js ├── jquery │ └── dist │ │ ├── jquery.min.js │ │ └── jquery.slim.min.js ├── popper │ └── dist │ │ └── umd │ │ └── popper.min.js ├── qrcodejs │ └── qrcode.min.js └── summernote │ └── dist │ ├── font │ ├── summernote.eot │ ├── summernote.ttf │ ├── summernote.woff │ └── summernote.woff2 │ ├── lang │ └── summernote-it-IT.js │ ├── summernote-bs4.min.css │ └── summernote-bs4.min.js ├── logo.svg └── style.css /docs/ddl/01-creazione-tabella-Courses.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Courses ( 2 | Id INTEGER NOT NULL CONSTRAINT PK_Courses PRIMARY KEY AUTOINCREMENT, 3 | Title TEXT (100) NOT NULL, 4 | Description TEXT (10000), 5 | ImagePath TEXT (100), 6 | Author TEXT (100) NOT NULL, 7 | Email TEXT (100), 8 | Rating REAL NOT NULL DEFAULT (0), 9 | FullPrice_Amount NUMERIC NOT NULL DEFAULT (0), 10 | FullPrice_Currency TEXT (3) NOT NULL DEFAULT ('EUR'), 11 | CurrentPrice_Amount NUMERIC NOT NULL DEFAULT (0), 12 | CurrentPrice_Currency TEXT (3) NOT NULL DEFAULT ('EUR') 13 | ); -------------------------------------------------------------------------------- /docs/ddl/02-inserimento-dati-in-tabella-Courses.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/ddl/02-inserimento-dati-in-tabella-Courses.sql -------------------------------------------------------------------------------- /docs/ddl/03-creazione-tabella-Lessons.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Lessons ( 2 | Id INTEGER NOT NULL CONSTRAINT PK_Lessons PRIMARY KEY AUTOINCREMENT, 3 | CourseId INTEGER NOT NULL REFERENCES Courses (Id) ON DELETE CASCADE ON UPDATE CASCADE, 4 | Title TEXT (100) NOT NULL, 5 | Description TEXT (10000), 6 | Duration TEXT (8) NOT NULL DEFAULT ('00:00:00') 7 | ); -------------------------------------------------------------------------------- /docs/ddl/04-inserimento-dati-in-tabella-Lessons.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/ddl/04-inserimento-dati-in-tabella-Lessons.sql -------------------------------------------------------------------------------- /docs/ddl/05-creazione-vincolo-unique-su-colonna-Title.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX ux_title ON Courses ( 2 | Title COLLATE NOCASE 3 | ); -------------------------------------------------------------------------------- /docs/ddl/06-aggiunta-colonna-per-concorrenza-ottimistica.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE Courses ADD RowVersion DATETIME; 2 | 3 | CREATE TRIGGER CoursesSetRowVersionOnInsert 4 | AFTER INSERT ON Courses 5 | BEGIN 6 | UPDATE Courses SET RowVersion = CURRENT_TIMESTAMP WHERE Id=NEW.Id; 7 | END; 8 | 9 | CREATE TRIGGER CoursesSetRowVersionOnUpdate 10 | AFTER UPDATE ON Courses WHEN NEW.RowVersion <= OLD.RowVersion 11 | BEGIN 12 | UPDATE Courses SET RowVersion = CURRENT_TIMESTAMP WHERE Id=NEW.Id; 13 | END; 14 | 15 | UPDATE Courses SET RowVersion = CURRENT_TIMESTAMP; -------------------------------------------------------------------------------- /docs/images/my-course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/images/my-course.png -------------------------------------------------------------------------------- /docs/slide/Sezione01.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione01.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione02.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione02.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione03.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione03.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione04.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione04.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione05.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione05.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione06.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione06.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione07.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione07.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione08.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione08.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione09.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione09.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione10.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione10.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione11.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione11.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione12.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione12.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione13.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione13.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione14.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione14.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione15.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione15.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione16.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione16.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione17.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione17.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione18.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione18.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione19.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione19.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione20.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione20.pdf -------------------------------------------------------------------------------- /docs/slide/Sezione21.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/docs/slide/Sezione21.pdf -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0.100" 4 | } 5 | } -------------------------------------------------------------------------------- /scripts/imm1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/scripts/imm1.png -------------------------------------------------------------------------------- /scripts/imm2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/scripts/imm2.png -------------------------------------------------------------------------------- /scripts/lambda/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Script Debug", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "program": "dotnet-script", 9 | "args": [ 10 | "${file}" 11 | ], 12 | "cwd": "${workspaceRoot}", 13 | "stopAtEntry": true 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /scripts/lambda/main.csx: -------------------------------------------------------------------------------- 1 | //Per fare il debug di questi esempi, devi prima installare il global tool dotnet-script con questo comando: 2 | //dotnet tool install -g dotnet-script 3 | //Trovi altre istruzioni nel file /scripts/readme.md 4 | 5 | 6 | //ESEMPIO #1: Definisco una lambda che accetta un parametro DateTime e restituisce un bool, e l'assegno alla variabile canDrive 7 | Func canDrive = dob => { 8 | return dob.AddYears(18) <= DateTime.Today; 9 | }; 10 | 11 | //Eseguo la lambda passandole il parametro DateTime 12 | DateTime dob = new DateTime(2002, 12, 25); 13 | bool result = canDrive(dob); 14 | //Poi stampo il risultato bool che ha restituito 15 | Console.WriteLine(result); 16 | 17 | //ESEMPIO #2: Stavolta definisco una lambda che accetta un parametro DateTime ma non restituisce nulla 18 | Action printDate = date => Console.WriteLine(date); 19 | 20 | //La invoco passandole l'argomento DateTime 21 | DateTime date = DateTime.Today; 22 | printDate(date); 23 | 24 | /*** ESERCIZI! ***/ 25 | 26 | // ESERCIZIO #1: Scrivi una lambda che prende due parametri stringa (nome e cognome) e restituisce la loro concatenazione 27 | // Func<...> concatFirstAndLastName = ...; 28 | // Qui invoca la lambda 29 | 30 | // ESERCIZIO #2: Una lambda che prende tre parametri interi (tre numeri) e restituisce il maggiore dei tre 31 | // Func<...> getMaximum = ...; 32 | // Qui invoca la lambda 33 | 34 | // ESERCIZIO #3: Una lambda che prende due parametri DateTime e non restituisce nulla, ma stampa la minore delle due date in console con un Console.WriteLine 35 | // Action<...> printLowerDate = ...; 36 | // Qui invoca la lambda -------------------------------------------------------------------------------- /scripts/lambda/omnisharp.json: -------------------------------------------------------------------------------- 1 | { 2 | "script": { 3 | "enableScriptNuGetReferences": true, 4 | "defaultTargetFramework": "netcoreapp2.1" 5 | } 6 | } -------------------------------------------------------------------------------- /scripts/linq/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Script Debug", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "program": "dotnet-script", 9 | "args": [ 10 | "${file}" 11 | ], 12 | "cwd": "${workspaceRoot}", 13 | "stopAtEntry": true 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /scripts/linq/main.csx: -------------------------------------------------------------------------------- 1 | //Per fare il debug di questi esempi, devi prima installare il global tool dotnet-script con questo comando: 2 | //dotnet tool install -g dotnet-script 3 | //Trovi altre istruzioni nel file /scripts/readme.md 4 | class Apple { 5 | public string Color { get; set; } 6 | public int Weight { get; set; } //In grammi 7 | } 8 | 9 | List apples = new List { 10 | new Apple { Color = "Red", Weight = 180 }, 11 | new Apple { Color = "Green", Weight = 195 }, 12 | new Apple { Color = "Red", Weight = 190 }, 13 | new Apple { Color = "Green", Weight = 185 }, 14 | }; 15 | 16 | //ESEMPIO #1: Ottengo i pesi delle mele rosse 17 | IEnumerable weightsOfRedApples = apples 18 | .Where(apple => apple.Color == "Red") 19 | .Select(apple => apple.Weight); 20 | 21 | //ESEMPIO #2: Calcolo la media dei pesi ottenuti 22 | double average = weightsOfRedApples.Average(); 23 | Console.WriteLine(); 24 | 25 | //ESERCIZIO #1: Qual è il peso minimo delle 4 mele? 26 | //int minimumWeight = apples...; 27 | 28 | //ESERCIZIO #2: Di che colore è la mela che pesa 190 grammi? 29 | //string color = apples...; 30 | 31 | //ESERCIZIO #3: Quante sono le mele rosse? 32 | //int redAppleCount = apples...; 33 | 34 | //ESERCIZIO #4: Qual è il peso totale delle mele verdi? 35 | //int totalWeight = apples...; 36 | -------------------------------------------------------------------------------- /scripts/linq/omnisharp.json: -------------------------------------------------------------------------------- 1 | { 2 | "script": { 3 | "enableScriptNuGetReferences": true, 4 | "defaultTargetFramework": "netcoreapp2.1" 5 | } 6 | } -------------------------------------------------------------------------------- /scripts/readme.md: -------------------------------------------------------------------------------- 1 | # Eseguire gli script C# 2 | Le sottodirectory che trovi qui (es. lambda), contengono degli script C#. Non si tratta di veri progetti, ma di semplici file .csx che possono comunque essere eseguiti e debuggati da Visual Studio Code installando il tool `dotnet-script` con questo comando. 3 | 4 | ``` 5 | dotnet tool install -g dotnet-script 6 | ``` 7 | 8 | Questo comando va eseguito solo la prima volta, perché verrà installato a livello globale nel tuo sistema e perciò sarà disponibile ovunque da riga di comando. 9 | Per iniziare a lavorare con gli script, portati in una directory vuota e digita: 10 | 11 | ``` 12 | dotnet script init 13 | ``` 14 | 15 | E poi apri questa directory in Visual Studio Code digitando: 16 | 17 | ``` 18 | code . 19 | ``` 20 | 21 | Oppure apri la directory al solito modo, dall'interfaccia grafica di Visual Studio Code. 22 | Ora sei pronto per sperimentare con C#! Avrai il supporto del debugger e dell'IntelliSense! 23 | 24 | ## Risoluzione dei problemi 25 | 26 | Se non riesci a fare il debug degli script o a vedere l'IntelliSense mentre digiti, verifica che OmniSharp si sia attivato. La sua icona, riportata nella status bar di Visual Studio Code, deve essere verde. Potrebbe volerci qualche secondo. Al limite aspetta un paio di minuti e poi chiudi e riapri Visual Studio Code. 27 | 28 | ![Icona di OmniSharp](imm1.png) 29 | 30 | Inoltre, puoi controllare quale versione di .NET Core usare dal file `omnisharp.json`. In questo caso sto usando la 2.1. 31 | 32 | ![Versione di .NET Core](imm2.png) -------------------------------------------------------------------------------- /src/MyCourse/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | Logs -------------------------------------------------------------------------------- /src/MyCourse/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/net6.0/MyCourse.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "launchBrowser": { 19 | "enabled": true, 20 | "args": "${auto-detect-url}", 21 | "windows": { 22 | "command": "cmd.exe", 23 | "args": "/C start ${auto-detect-url}" 24 | }, 25 | "osx": { 26 | "command": "open" 27 | }, 28 | "linux": { 29 | "command": "xdg-open" 30 | } 31 | }, 32 | "env": { 33 | }, 34 | "sourceFileMap": { 35 | "/Views": "${workspaceFolder}/Views" 36 | } 37 | }, 38 | { 39 | "name": ".NET Core Attach", 40 | "type": "coreclr", 41 | "request": "attach", 42 | "processId": "${command:pickProcess}" 43 | } 44 | ,] 45 | } -------------------------------------------------------------------------------- /src/MyCourse/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "emmet.includeLanguages": { "aspnetcorerazor": "html"}, 3 | "browserSync.config": { 4 | "proxy": "localhost:5000", 5 | "files": ["bin/reload.txt", "Views/**/*.cshtml", "wwwroot/**/*"] 6 | }, 7 | "fontAwesomeAutocomplete.patterns": ["**/*.cshtml"], 8 | "fontAwesomeAutocomplete.triggerWord": "f", 9 | "gitlens.currentLine.enabled": false 10 | } -------------------------------------------------------------------------------- /src/MyCourse/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/MyCourse.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/IdentityHostingStartup.cs: -------------------------------------------------------------------------------- 1 | [assembly: HostingStartup(typeof(MyCourse.Areas.Identity.IdentityHostingStartup))] 2 | namespace MyCourse.Areas.Identity; 3 | public class IdentityHostingStartup : IHostingStartup 4 | { 5 | public void Configure(IWebHostBuilder builder) 6 | { 7 | builder.ConfigureServices((context, services) => 8 | { 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/AccessDenied.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model AccessDeniedModel 3 | @{ 4 | ViewData["Title"] = "Accesso negato"; 5 | } 6 | 7 |
8 |

@ViewData["Title"]

9 |

Non hai accesso a questa risorsa.

10 |
11 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace MyCourse.Areas.Identity.Pages.Account; 4 | 5 | public class AccessDeniedModel : PageModel 6 | { 7 | public void OnGet() 8 | { 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ConfirmEmail.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ConfirmEmailModel 3 | @{ 4 | ViewData["Title"] = "Conferma email"; 5 | } 6 | 7 |

@ViewData["Title"]

8 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | using Microsoft.AspNetCore.WebUtilities; 7 | 8 | namespace MyCourse.Areas.Identity.Pages.Account; 9 | 10 | [AllowAnonymous] 11 | public class ConfirmEmailModel : PageModel 12 | { 13 | private readonly UserManager _userManager; 14 | 15 | public ConfirmEmailModel(UserManager userManager) 16 | { 17 | _userManager = userManager; 18 | } 19 | 20 | [TempData] 21 | public string StatusMessage { get; set; } 22 | 23 | public async Task OnGetAsync(string userId, string code) 24 | { 25 | if (userId == null || code == null) 26 | { 27 | return RedirectToPage("/Index"); 28 | } 29 | 30 | var user = await _userManager.FindByIdAsync(userId); 31 | if (user == null) 32 | { 33 | return NotFound($"Non è stato possibile trovare il profilo utente con ID '{userId}'."); 34 | } 35 | 36 | code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); 37 | var result = await _userManager.ConfirmEmailAsync(user, code); 38 | StatusMessage = result.Succeeded ? "Grazie per aver confermato l'email, ora puoi accedere." : "Errore nella conferma dell'email."; 39 | return Page(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ConfirmEmailChangeModel 3 | @{ 4 | ViewData["Title"] = "Conferma cambio email"; 5 | } 6 | 7 |

@ViewData["Title"]

8 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | using Microsoft.AspNetCore.WebUtilities; 7 | 8 | namespace MyCourse.Areas.Identity.Pages.Account; 9 | 10 | [AllowAnonymous] 11 | public class ConfirmEmailChangeModel : PageModel 12 | { 13 | private readonly UserManager _userManager; 14 | private readonly SignInManager _signInManager; 15 | 16 | public ConfirmEmailChangeModel(UserManager userManager, SignInManager signInManager) 17 | { 18 | _userManager = userManager; 19 | _signInManager = signInManager; 20 | } 21 | 22 | [TempData] 23 | public string StatusMessage { get; set; } 24 | 25 | public async Task OnGetAsync(string userId, string email, string code) 26 | { 27 | if (userId == null || email == null || code == null) 28 | { 29 | return RedirectToPage("/Index"); 30 | } 31 | 32 | var user = await _userManager.FindByIdAsync(userId); 33 | if (user == null) 34 | { 35 | return NotFound($"Non è stato possibile trovare l'utente con ID '{userId}'."); 36 | } 37 | 38 | code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); 39 | var result = await _userManager.ChangeEmailAsync(user, email, code); 40 | if (!result.Succeeded) 41 | { 42 | StatusMessage = "Errore nel cambio email."; 43 | return Page(); 44 | } 45 | 46 | // In our UI email and user name are one and the same, so when we update the email 47 | // we need to update the user name. 48 | var setUserNameResult = await _userManager.SetUserNameAsync(user, email); 49 | if (!setUserNameResult.Succeeded) 50 | { 51 | StatusMessage = "Errore nel cambio del nome utente."; 52 | return Page(); 53 | } 54 | 55 | await _signInManager.RefreshSignInAsync(user); 56 | StatusMessage = "Grazie per aver confermato il cambio email."; 57 | return Page(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ExternalLogin.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ExternalLoginModel 3 | @{ 4 | ViewData["Title"] = "Registrati"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

Associa il tuo account @(Model.ProviderDisplayName).

9 |
10 | 11 |

12 | Ti sei autenticato correttamente con @Model.ProviderDisplayName. 13 | Per favore indica un indirizzo email da usare in questo sito e clicca il bottone Registrati in fondo per completare la procedura. 14 |

15 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 | @section Scripts { 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ForgotPassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ForgotPasswordModel 3 | @{ 4 | ViewData["Title"] = "Password dimenticata"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

Inserisci la tua email

9 |
10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 |
23 | 24 | @section Scripts { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Text.Encodings.Web; 3 | using System.Text; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Identity.UI.Services; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using Microsoft.AspNetCore.WebUtilities; 10 | 11 | namespace MyCourse.Areas.Identity.Pages.Account; 12 | 13 | [AllowAnonymous] 14 | public class ForgotPasswordModel : PageModel 15 | { 16 | private readonly UserManager _userManager; 17 | private readonly IEmailSender _emailSender; 18 | 19 | public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender) 20 | { 21 | _userManager = userManager; 22 | _emailSender = emailSender; 23 | } 24 | 25 | [BindProperty] 26 | public InputModel Input { get; set; } 27 | 28 | public class InputModel 29 | { 30 | [Required(ErrorMessage = "L'email è obbligatoria")] 31 | [EmailAddress(ErrorMessage = "Deve essere un indirizzo email valido")] 32 | public string Email { get; set; } 33 | } 34 | 35 | public async Task OnPostAsync() 36 | { 37 | if (ModelState.IsValid) 38 | { 39 | var user = await _userManager.FindByEmailAsync(Input.Email); 40 | if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) 41 | { 42 | // Don't reveal that the user does not exist or is not confirmed 43 | return RedirectToPage("./ForgotPasswordConfirmation"); 44 | } 45 | 46 | // For more information on how to enable account confirmation and password reset please 47 | // visit https://go.microsoft.com/fwlink/?LinkID=532713 48 | var code = await _userManager.GeneratePasswordResetTokenAsync(user); 49 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 50 | var callbackUrl = Url.Page( 51 | "/Account/ResetPassword", 52 | pageHandler: null, 53 | values: new { area = "Identity", code }, 54 | protocol: Request.Scheme); 55 | 56 | await _emailSender.SendEmailAsync( 57 | Input.Email, 58 | "Reimposta password", 59 | $"Per favore, reimposta la password cliccando questo link."); 60 | 61 | return RedirectToPage("./ForgotPasswordConfirmation"); 62 | } 63 | 64 | return Page(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ForgotPasswordConfirmation 3 | @{ 4 | ViewData["Title"] = "Conferma password dimenticata"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

9 | Per favore controlla la tua casella email per completare la reimpostazione della password. 10 |

11 | 12 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace MyCourse.Areas.Identity.Pages.Account; 5 | 6 | [AllowAnonymous] 7 | public class ForgotPasswordConfirmation : PageModel 8 | { 9 | public void OnGet() 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Lockout.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LockoutModel 3 | @{ 4 | ViewData["Title"] = "Bloccato"; 5 | } 6 | 7 |
8 |

@ViewData["Title"]

9 |

Questo account è stato bloccato, per favore prova più tardi.

10 |
11 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Lockout.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace MyCourse.Areas.Identity.Pages.Account; 5 | 6 | [AllowAnonymous] 7 | public class LockoutModel : PageModel 8 | { 9 | public void OnGet() 10 | { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/LoginWith2fa.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LoginWith2faModel 3 | @{ 4 | ViewData["Title"] = "Autenticazione a due fattori"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |
9 |

Il tuo accesso è protetto con un'app authenticator. Aggiungi qui sotto il codice fornito dall'app authenticator.

10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |

35 | Hai perso l'app authenticator? Puoi 36 | accedere con un codice di recupero. 37 |

38 | 39 | @section Scripts { 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LoginWithRecoveryCodeModel 3 | @{ 4 | ViewData["Title"] = "Verifica del codice di recupero"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |
9 |

10 | Hai richiesto l'accesso con un codice di recupero. Questo accesso non verrà ricordato, almeno finché non fornirai 11 | il codice ottenuto dall'app authenticator. In alternativa puoi disabilitare l'autenticazione a due fattori ed eseguire di nuovo l'accesso. 12 |

13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 | @section Scripts { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Logout.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LogoutModel 3 | @{ 4 | ViewData["Title"] = "Uscita"; 5 | } 6 | 7 |
8 |

@ViewData["Title"]

9 |

Sei uscito correttamente dall'applicazione.

10 |
-------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Logout.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | 6 | namespace MyCourse.Areas.Identity.Pages.Account; 7 | 8 | [AllowAnonymous] 9 | public class LogoutModel : PageModel 10 | { 11 | private readonly SignInManager _signInManager; 12 | private readonly ILogger _logger; 13 | 14 | public LogoutModel(SignInManager signInManager, ILogger logger) 15 | { 16 | _signInManager = signInManager; 17 | _logger = logger; 18 | } 19 | 20 | public void OnGet() 21 | { 22 | } 23 | 24 | public async Task OnPost(string returnUrl = null) 25 | { 26 | await _signInManager.SignOutAsync(); 27 | _logger.LogInformation("User logged out."); 28 | if (returnUrl != null) 29 | { 30 | return LocalRedirect(returnUrl); 31 | } 32 | else 33 | { 34 | return RedirectToPage(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ChangePasswordModel 3 | @{ 4 | ViewData["Title"] = "Modifica password"; 5 | ViewData["ActivePage"] = ManageNavPages.ChangePassword; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 | @section Scripts { 35 | 36 | } -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DeletePersonalDataModel 3 | @{ 4 | ViewData["Title"] = "Elimina informazioni personali"; 5 | ViewData["ActivePage"] = ManageNavPages.PersonalData; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 | 15 | 16 |
17 |
18 |
19 | @if (Model.RequirePassword) 20 | { 21 |
22 | 23 | 24 | 25 |
26 | } 27 | 28 |
29 |
30 | 31 | @section Scripts { 32 | 33 | } -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Disable2faModel 3 | @{ 4 | ViewData["Title"] = "Disabilita l'autenticazione a due fattori (2FA)"; 5 | ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; 6 | } 7 | 8 | 9 |

@ViewData["Title"]

10 | 11 | 20 | 21 |
22 |
23 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace MyCourse.Areas.Identity.Pages.Account.Manage; 6 | 7 | public class Disable2faModel : PageModel 8 | { 9 | private readonly UserManager _userManager; 10 | private readonly ILogger _logger; 11 | 12 | public Disable2faModel( 13 | UserManager userManager, 14 | ILogger logger) 15 | { 16 | _userManager = userManager; 17 | _logger = logger; 18 | } 19 | 20 | [TempData] 21 | public string StatusMessage { get; set; } 22 | 23 | public async Task OnGet() 24 | { 25 | var user = await _userManager.GetUserAsync(User); 26 | if (user == null) 27 | { 28 | return NotFound($"Non è stato possibile trovare il profilo utente con ID '{_userManager.GetUserId(User)}'."); 29 | } 30 | 31 | if (!await _userManager.GetTwoFactorEnabledAsync(user)) 32 | { 33 | throw new InvalidOperationException($"Non è stato possibile disabilitare l'autenticazione a due fattori per il profilo utente con ID '{_userManager.GetUserId(User)}' perché non era abilitata."); 34 | } 35 | 36 | return Page(); 37 | } 38 | 39 | public async Task OnPostAsync() 40 | { 41 | var user = await _userManager.GetUserAsync(User); 42 | if (user == null) 43 | { 44 | return NotFound($"Non è stato possibile trovare il profilo utente con ID '{_userManager.GetUserId(User)}'."); 45 | } 46 | 47 | var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); 48 | if (!disable2faResult.Succeeded) 49 | { 50 | throw new InvalidOperationException($"Si è verificato un errore inatteso nel tentativo di disabilitare l'autenticazione a due fattori per il profilo utente con ID '{_userManager.GetUserId(User)}'."); 51 | } 52 | 53 | _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User)); 54 | StatusMessage = "L'autenticazione a due fattori è stata disabilitata. La potrai abilitare di nuovo configurando un'app authenticator."; 55 | return RedirectToPage("./TwoFactorAuthentication"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model DownloadPersonalDataModel 3 | @{ 4 | ViewData["Title"] = "Scarica i tuoi dati personali"; 5 | ViewData["ActivePage"] = ManageNavPages.PersonalData; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 | @section Scripts { 11 | 12 | } -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | 6 | namespace MyCourse.Areas.Identity.Pages.Account.Manage; 7 | 8 | public class DownloadPersonalDataModel : PageModel 9 | { 10 | private readonly UserManager _userManager; 11 | private readonly ILogger _logger; 12 | 13 | public DownloadPersonalDataModel( 14 | UserManager userManager, 15 | ILogger logger) 16 | { 17 | _userManager = userManager; 18 | _logger = logger; 19 | } 20 | 21 | public async Task OnPostAsync() 22 | { 23 | var user = await _userManager.GetUserAsync(User); 24 | if (user == null) 25 | { 26 | return NotFound($"Non è stato possibile trovare il profilo utente con ID '{_userManager.GetUserId(User)}'."); 27 | } 28 | 29 | _logger.LogInformation("User with ID '{UserId}' asked for their personal data.", _userManager.GetUserId(User)); 30 | 31 | // Only include personal data for download 32 | Dictionary personalData = new(); 33 | var personalDataProps = typeof(ApplicationUser).GetProperties().Where( 34 | prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); 35 | foreach (var p in personalDataProps) 36 | { 37 | personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); 38 | } 39 | 40 | var logins = await _userManager.GetLoginsAsync(user); 41 | foreach (var l in logins) 42 | { 43 | personalData.Add($"Chiave di autenticazione con il servizio esterno {l.LoginProvider}", l.ProviderKey); 44 | } 45 | 46 | Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json"); 47 | return new FileContentResult(JsonSerializer.SerializeToUtf8Bytes(personalData), "application/json"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/Email.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model EmailModel 3 | @{ 4 | ViewData["Title"] = "Indirizzo email"; 5 | ViewData["ActivePage"] = ManageNavPages.Email; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 | @if (Model.IsEmailConfirmed) 17 | { 18 |
19 | 20 |
21 | 22 |
23 |
24 | } 25 | else 26 | { 27 |
28 | 29 |
30 | 31 | } 32 |
33 |
34 | 35 | 36 | 37 |
38 | 39 |
40 |
41 |
42 | 43 | @section Scripts { 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ExternalLoginsModel 3 | @{ 4 | ViewData["Title"] = "Gestisci i tuoi servizi di autenticazione esterni"; 5 | ViewData["ActivePage"] = ManageNavPages.ExternalLogins; 6 | } 7 | 8 | 9 | @if (Model.CurrentLogins?.Count > 0) 10 | { 11 |

Servizi di autenticazione registrati

12 | 13 | 14 | @foreach (var login in Model.CurrentLogins) 15 | { 16 | 17 | 18 | 34 | 35 | } 36 | 37 |
@login.ProviderDisplayName 19 | @if (Model.ShowRemoveButton) 20 | { 21 |
22 |
23 | 24 | 25 | 26 |
27 |
28 | } 29 | else 30 | { 31 | @:   32 | } 33 |
38 | } 39 | @if (Model.OtherLogins?.Count > 0) 40 | { 41 |

Aggiunti un altro servizio di autenticazione esterno.

42 |
43 | 53 | } 54 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model GenerateRecoveryCodesModel 3 | @{ 4 | ViewData["Title"] = "Genera i codici di recupero per l'autenticazione a due fattori (2FA)"; 5 | ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; 6 | } 7 | 8 | 9 |

@ViewData["Title"]

10 | 23 |
24 |
25 | 26 |
27 |
-------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IndexModel 3 | @{ 4 | ViewData["Title"] = "Profilo"; 5 | ViewData["ActivePage"] = ManageNavPages.Index; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 | @section Scripts { 29 | 30 | } -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Rendering; 2 | 3 | namespace MyCourse.Areas.Identity.Pages.Account.Manage; 4 | 5 | public static class ManageNavPages 6 | { 7 | public static string Index => "Index"; 8 | 9 | public static string Email => "Email"; 10 | 11 | public static string ChangePassword => "ChangePassword"; 12 | 13 | public static string DownloadPersonalData => "DownloadPersonalData"; 14 | 15 | public static string DeletePersonalData => "DeletePersonalData"; 16 | 17 | public static string ExternalLogins => "ExternalLogins"; 18 | 19 | public static string PersonalData => "PersonalData"; 20 | 21 | public static string TwoFactorAuthentication => "TwoFactorAuthentication"; 22 | 23 | public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); 24 | 25 | public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email); 26 | 27 | public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); 28 | 29 | public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData); 30 | 31 | public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData); 32 | 33 | public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); 34 | 35 | public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData); 36 | 37 | public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); 38 | 39 | private static string PageNavClass(ViewContext viewContext, string page) 40 | { 41 | var activePage = viewContext.ViewData["ActivePage"] as string 42 | ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); 43 | return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model PersonalDataModel 3 | @{ 4 | ViewData["Title"] = "Informazioni personali"; 5 | ViewData["ActivePage"] = ManageNavPages.PersonalData; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 |
11 |
12 |

Il tuo profilo contiene le informazioni personali che ci hai fornito. Questa pagina ti permette di scaricare o di eliminare le tue informazioni personali.

13 |

14 | Eliminare queste informazioni causerà la chiusura del tuo profilo utente e non potrai più accedervi. 15 |

16 |
17 | 18 |
19 |

20 | Elimina 21 |

22 |
23 |
24 | 25 | @section Scripts { 26 | 27 | } -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace MyCourse.Areas.Identity.Pages.Account.Manage; 6 | 7 | public class PersonalDataModel : PageModel 8 | { 9 | private readonly UserManager _userManager; 10 | private readonly ILogger _logger; 11 | 12 | public PersonalDataModel( 13 | UserManager userManager, 14 | ILogger logger) 15 | { 16 | _userManager = userManager; 17 | _logger = logger; 18 | } 19 | 20 | public async Task OnGet() 21 | { 22 | var user = await _userManager.GetUserAsync(User); 23 | if (user == null) 24 | { 25 | return NotFound($"Non è stato possibile caricare il profilo utente con ID '{_userManager.GetUserId(User)}'."); 26 | } 27 | 28 | return Page(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ResetAuthenticatorModel 3 | @{ 4 | ViewData["Title"] = "Reimposta la chiave dell'app authenticator"; 5 | ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; 6 | } 7 | 8 | 9 |

@ViewData["Title"]

10 | 20 |
21 |
22 | 23 |
24 |
-------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace MyCourse.Areas.Identity.Pages.Account.Manage; 6 | 7 | public class ResetAuthenticatorModel : PageModel 8 | { 9 | UserManager _userManager; 10 | private readonly SignInManager _signInManager; 11 | ILogger _logger; 12 | 13 | public ResetAuthenticatorModel( 14 | UserManager userManager, 15 | SignInManager signInManager, 16 | ILogger logger) 17 | { 18 | _userManager = userManager; 19 | _signInManager = signInManager; 20 | _logger = logger; 21 | } 22 | 23 | [TempData] 24 | public string StatusMessage { get; set; } 25 | 26 | public async Task OnGet() 27 | { 28 | var user = await _userManager.GetUserAsync(User); 29 | if (user == null) 30 | { 31 | return NotFound($"Non è stato possibile caricare il profilo utente con ID '{_userManager.GetUserId(User)}'."); 32 | } 33 | 34 | return Page(); 35 | } 36 | 37 | public async Task OnPostAsync() 38 | { 39 | var user = await _userManager.GetUserAsync(User); 40 | if (user == null) 41 | { 42 | return NotFound($"Non è stato possibile caricare il profilo utente con ID '{_userManager.GetUserId(User)}'."); 43 | } 44 | 45 | await _userManager.SetTwoFactorEnabledAsync(user, false); 46 | await _userManager.ResetAuthenticatorKeyAsync(user); 47 | _logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id); 48 | 49 | await _signInManager.RefreshSignInAsync(user); 50 | StatusMessage = "La chiave è stata reimpostata, ora devi riconfigurare l'app authenticator usando la nuova chiave."; 51 | 52 | return RedirectToPage("./EnableAuthenticator"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model SetPasswordModel 3 | @{ 4 | ViewData["Title"] = "Set password"; 5 | ViewData["ActivePage"] = ManageNavPages.ChangePassword; 6 | } 7 | 8 |

Set your password

9 | 10 |

11 | Non hai ancora un profilo esistente su questo sito. Aggiungi un profilo 12 | così che tu possa autenticarti con email e password, così che tu non debba ricorrere a servizi esterni. 13 |

14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 |
28 | 29 |
30 |
31 |
32 | 33 | @section Scripts { 34 | 35 | } -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ShowRecoveryCodesModel 3 | @{ 4 | ViewData["Title"] = "Codici di recupero"; 5 | ViewData["ActivePage"] = "TwoFactorAuthentication"; 6 | } 7 | 8 | 9 |

@ViewData["Title"]

10 | 18 |
19 |
20 |
    21 | @for (var row = 0; row < Model.RecoveryCodes.Length; row++) 22 | { 23 |
  • @Model.RecoveryCodes[row]
  • 24 | } 25 |
26 |
27 |
-------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace MyCourse.Areas.Identity.Pages.Account.Manage; 5 | 6 | public class ShowRecoveryCodesModel : PageModel 7 | { 8 | [TempData] 9 | public string[] RecoveryCodes { get; set; } 10 | 11 | [TempData] 12 | public string StatusMessage { get; set; } 13 | 14 | public IActionResult OnGet() 15 | { 16 | if (RecoveryCodes == null || RecoveryCodes.Length == 0) 17 | { 18 | return RedirectToPage("./TwoFactorAuthentication"); 19 | } 20 | 21 | return Page(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model TwoFactorAuthenticationModel 3 | @{ 4 | ViewData["Title"] = "Autenticazione a due fattori (2FA)"; 5 | ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication; 6 | } 7 | 8 | 9 |

@ViewData["Title"]

10 | @if (Model.Is2faEnabled) 11 | { 12 | if (Model.RecoveryCodesLeft == 0) 13 | { 14 |
15 | Non hai codici di recupero rimanenti. 16 |

Devi generare dei nuovi codici di recupero prima che tu possa accedere con un codice di recupero.

17 |
18 | } 19 | else if (Model.RecoveryCodesLeft == 1) 20 | { 21 |
22 | Hai solo 1 codice di recupero rimanente. 23 |

Dovresti generare dei nuovi codici di recupero.

24 |
25 | } 26 | else if (Model.RecoveryCodesLeft <= 3) 27 | { 28 |
29 | Hai @Model.RecoveryCodesLeft codici di recupero rimanenti. 30 |

Dovresti generare dei nuovi codici di recupero.

31 |
32 | } 33 | 34 | if (Model.IsMachineRemembered) 35 | { 36 |
37 | 38 |
39 | } 40 | Disabilita autenticazione a due fattori
41 | Reimposta codici di recupero 42 | } 43 | 44 |
App authenticator
45 | @if (!Model.HasAuthenticator) 46 | { 47 | Aggiungi un'app authenticator 48 | } 49 | else 50 | { 51 | Configura app authenticator
52 | Reimposta app authenticator 53 | } 54 | 55 | @section Scripts { 56 | 57 | } -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace MyCourse.Areas.Identity.Pages.Account.Manage; 6 | 7 | public class TwoFactorAuthenticationModel : PageModel 8 | { 9 | private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}"; 10 | 11 | private readonly UserManager _userManager; 12 | private readonly SignInManager _signInManager; 13 | private readonly ILogger _logger; 14 | 15 | public TwoFactorAuthenticationModel( 16 | UserManager userManager, 17 | SignInManager signInManager, 18 | ILogger logger) 19 | { 20 | _userManager = userManager; 21 | _signInManager = signInManager; 22 | _logger = logger; 23 | } 24 | 25 | public bool HasAuthenticator { get; set; } 26 | 27 | public int RecoveryCodesLeft { get; set; } 28 | 29 | [BindProperty] 30 | public bool Is2faEnabled { get; set; } 31 | 32 | public bool IsMachineRemembered { get; set; } 33 | 34 | [TempData] 35 | public string StatusMessage { get; set; } 36 | 37 | public async Task OnGet() 38 | { 39 | var user = await _userManager.GetUserAsync(User); 40 | if (user == null) 41 | { 42 | return NotFound($"Non è stato possibile trovare il profilo utente con ID '{_userManager.GetUserId(User)}'."); 43 | } 44 | 45 | HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null; 46 | Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user); 47 | IsMachineRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user); 48 | RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user); 49 | 50 | return Page(); 51 | } 52 | 53 | public async Task OnPost() 54 | { 55 | var user = await _userManager.GetUserAsync(User); 56 | if (user == null) 57 | { 58 | return NotFound($"Non è stato possibile trovare il profilo utente con ID '{_userManager.GetUserId(User)}'."); 59 | } 60 | 61 | await _signInManager.ForgetTwoFactorClientAsync(); 62 | StatusMessage = "Il browser corrente è stato dimenticato. Quando farai di nuovo l'accesso da questo browser, ti verrà chiesto di inserire di nuovo il codice per l'autenticazione a due fattori."; 63 | return RedirectToPage(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | if (ViewData.TryGetValue("ParentLayout", out var parentLayout)) 3 | { 4 | Layout = (string)parentLayout; 5 | } 6 | else 7 | { 8 | Layout = "/Areas/Identity/Pages/_Layout.cshtml"; 9 | } 10 | } 11 | 12 |

Gestisci il tuo profilo

13 | 14 |
15 |
16 |
17 | 18 |
19 |
20 | @RenderBody() 21 |
22 |
23 |
24 | 25 | @section Scripts { 26 | @RenderSection("Scripts", required: false) 27 | } 28 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml: -------------------------------------------------------------------------------- 1 | @inject SignInManager SignInManager 2 | @{ 3 | var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); 4 | } 5 | 16 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/_StatusMessage.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 | @if (!String.IsNullOrEmpty(Model)) 4 | { 5 | var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; 6 | 10 | } -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using MyCourse.Areas.Identity.Pages.Account.Manage 2 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model RegisterConfirmationModel 3 | @{ 4 | ViewData["Title"] = "Conferma registrazione"; 5 | } 6 | 7 |

@ViewData["Title"]

8 | @{ 9 | if (@Model.DisplayConfirmAccountLink) 10 | { 11 |

12 | Quest'applicazione sta funzionando in ambiente Development, perciò per praticità viene mostrato qui il contenuto dell'email di conferma:
13 | Clicca qui per confermare la tua registrazione 14 |

15 | } 16 | else 17 | { 18 |

19 | Per favore controlla la tua casella email per confermare la registrazione. 20 |

21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using System.Text; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Identity.UI.Services; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | using Microsoft.AspNetCore.WebUtilities; 8 | 9 | namespace MyCourse.Areas.Identity.Pages.Account; 10 | 11 | [AllowAnonymous] 12 | public class RegisterConfirmationModel : PageModel 13 | { 14 | private readonly UserManager _userManager; 15 | private readonly IEmailSender _sender; 16 | private readonly IHostEnvironment env; 17 | 18 | public RegisterConfirmationModel(UserManager userManager, IEmailSender sender, IHostEnvironment env) 19 | { 20 | _userManager = userManager; 21 | _sender = sender; 22 | this.env = env; 23 | } 24 | 25 | public string Email { get; set; } 26 | 27 | public bool DisplayConfirmAccountLink { get; set; } 28 | 29 | public string EmailConfirmationUrl { get; set; } 30 | 31 | public async Task OnGetAsync(string email, string returnUrl = null) 32 | { 33 | if (email == null) 34 | { 35 | return RedirectToPage("/Index"); 36 | } 37 | 38 | var user = await _userManager.FindByEmailAsync(email); 39 | if (user == null) 40 | { 41 | return NotFound($"Non è stato possibile trovare l'utente con l'email '{email}'."); 42 | } 43 | 44 | Email = email; 45 | // Once you add a real email sender, you should remove this code that lets you confirm the account 46 | DisplayConfirmAccountLink = env.IsDevelopment(); 47 | if (DisplayConfirmAccountLink) 48 | { 49 | var userId = await _userManager.GetUserIdAsync(user); 50 | var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); 51 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 52 | EmailConfirmationUrl = Url.Page( 53 | "/Account/ConfirmEmail", 54 | pageHandler: null, 55 | values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl }, 56 | protocol: Request.Scheme); 57 | } 58 | 59 | return Page(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ResetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ResetPasswordModel 3 | @{ 4 | ViewData["Title"] = "Reimposta password"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

Reimposta la password.

9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 |
30 | 31 |
32 |
33 |
34 | 35 | @section Scripts { 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ResetPasswordConfirmationModel 3 | @{ 4 | ViewData["Title"] = "Conferma reimpostazione password"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

9 | La tua password è stata reimpostata. Per favore clicca qui per accedere. 10 |

11 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace MyCourse.Areas.Identity.Pages.Account; 5 | 6 | [AllowAnonymous] 7 | public class ResetPasswordConfirmationModel : PageModel 8 | { 9 | public void OnGet() 10 | { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/_StatusMessage.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 | @if (!String.IsNullOrEmpty(Model)) 4 | { 5 | var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; 6 | 10 | } 11 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/Account/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using MyCourse.Areas.Identity.Pages.Account 2 | @using MyCourse.Models.Entities -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using MyCourse.Areas.Identity 3 | @using MyCourse.Areas.Identity.Pages 4 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 5 | -------------------------------------------------------------------------------- /src/MyCourse/Areas/Identity/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "/Views/Shared/_Layout.cshtml"; 3 | } -------------------------------------------------------------------------------- /src/MyCourse/Controllers/ErrorController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Diagnostics; 3 | using Microsoft.AspNetCore.Mvc; 4 | using MyCourse.Models.Exceptions.Application; 5 | using MyCourse.Models.Exceptions.Infrastructure; 6 | 7 | namespace MyCourse.Controllers; 8 | 9 | public class ErrorController : Controller 10 | { 11 | [AllowAnonymous] 12 | public IActionResult Index() 13 | { 14 | var feature = HttpContext.Features.Get(); 15 | switch (feature.Error) 16 | { 17 | case CourseNotFoundException exc: 18 | ViewData["Title"] = "Corso non trovato"; 19 | Response.StatusCode = 404; 20 | return View("CourseNotFound"); 21 | 22 | case CourseSubscriptionException exc: 23 | ViewData["Title"] = "Non è stato possibile iscriverti al corso"; 24 | Response.StatusCode = 400; 25 | return View(); 26 | 27 | case CourseSubscriptionNotFoundException exc: 28 | ViewData["Title"] = "Non sei iscritto al corso"; 29 | Response.StatusCode = 400; 30 | return View(); 31 | 32 | case PaymentGatewayException exc: 33 | ViewData["Title"] = "Si è verificato un errore nel pagamento"; 34 | Response.StatusCode = 400; 35 | return View(); 36 | 37 | case UserUnknownException exc: 38 | ViewData["Title"] = "Utente sconosciuto"; 39 | Response.StatusCode = 400; 40 | return View(); 41 | 42 | case SendException exc: 43 | ViewData["Title"] = "Non è stato possibile inviare il messaggio, riprova più tardi"; 44 | Response.StatusCode = 500; 45 | return View(); 46 | 47 | default: 48 | ViewData["Title"] = "Errore"; 49 | return View(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/MyCourse/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using MyCourse.Models.ViewModels.Courses; 4 | using MyCourse.Models.ViewModels.Home; 5 | 6 | namespace MyCourse.Controllers; 7 | 8 | public class HomeController : Controller 9 | { 10 | 11 | [AllowAnonymous] 12 | public async Task Index([FromServices] ICachedCourseService courseService) 13 | { 14 | ViewData["Title"] = "Benvenuto su MyCourse!"; 15 | List bestRatingCourses = await courseService.GetBestRatingCoursesAsync(); 16 | List mostRecentCourses = await courseService.GetMostRecentCoursesAsync(); 17 | 18 | HomeViewModel viewModel = new() 19 | { 20 | BestRatingCourses = bestRatingCourses, 21 | MostRecentCourses = mostRecentCourses 22 | }; 23 | 24 | return View(viewModel); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/Authorization/AuthorizePolicy.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace MyCourse.Customizations.Authorization; 4 | 5 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)] 6 | public class AuthorizePolicyAttribute : AuthorizeAttribute 7 | { 8 | // Grazie a questo costruttore possiamo fornire le policy come oggetti Policy anziché come stringhe 9 | // Esempio di utilizzo: [AuthorizePolicy(Policy.CourseAuthor)] 10 | public AuthorizePolicyAttribute(params Policy[] policies) 11 | { 12 | // Poi convertiamo il nome della policy in formato stringa, 13 | // così come richiede la proprietà Policy dell'AuthorizeAttribute 14 | // ATTENZIONE: ASP.NET Core normalmente non permette di indicare il nome di più di una policy 15 | // è possibile solo se scriviamo noi logica personalizzata: vedi la classe MultiAuthorizationPolicyProvider 16 | Policy = string.Join(",", policies); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/Authorization/AuthorizeRole.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace MyCourse.Customizations.Authorization; 4 | 5 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)] 6 | public class AuthorizeRoleAttribute : AuthorizeAttribute 7 | { 8 | // Grazie a questo costruttore possiamo fornire i ruoli come Role anziché come stringhe 9 | // Esempio di utilizzo con 1 ruolo: [AuthorizeRole(Role.Teacher)] 10 | // Esempio di utilizzo con 2 ruoli: [AuthorizeRole(Role.Teacher, Role.Administrator)] 11 | public AuthorizeRoleAttribute(params Role[] roles) 12 | { 13 | // Poi li convertiamo a stringa e li separiamo con la virgola 14 | // così come vuole la proprietà Roles dell'attributo Authorize 15 | Roles = string.Join(",", roles.Select(role => role.ToString())); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/Identity/CommonPasswordValidator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace MyCourse.Customizations.Identity; 4 | 5 | public class CommonPasswordValidator : IPasswordValidator where TUser : class 6 | { 7 | private readonly string[] commons; 8 | public CommonPasswordValidator() 9 | { 10 | //Elenco di password comuni 11 | //TODO: impostare un elenco più significativo, magari ottenuto da: 12 | //https://github.com/danielmiessler/SecLists/tree/master/Passwords/Common-Credentials 13 | this.commons = new[] { 14 | "password", "abc", "123", "qwerty" 15 | }; 16 | } 17 | 18 | public Task ValidateAsync(UserManager manager, TUser user, string password) 19 | { 20 | IdentityResult result; 21 | if (commons.Any(common => password.Contains(common, StringComparison.CurrentCultureIgnoreCase))) 22 | { 23 | result = IdentityResult.Failed(new IdentityError { Description = "Password troppo comune" }); 24 | } 25 | else 26 | { 27 | result = IdentityResult.Success; 28 | } 29 | return Task.FromResult(result); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/Identity/CustomClaimsPrincipalFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace MyCourse.Customizations.Identity; 6 | 7 | public class CustomClaimsPrincipalFactory : UserClaimsPrincipalFactory 8 | { 9 | public CustomClaimsPrincipalFactory(UserManager userManager, IOptions optionsAccessor) : base(userManager, optionsAccessor) 10 | { 11 | } 12 | 13 | protected override async Task GenerateClaimsAsync(ApplicationUser user) 14 | { 15 | ClaimsIdentity identity = await base.GenerateClaimsAsync(user); 16 | identity.AddClaim(new Claim("FullName", user.FullName)); 17 | 18 | // In vari punti dell'applicazione stiamo usando la policy "CourseAuthor" 19 | // che scatenerà una query al database per verificare se l'id dell'utente è uguale 20 | // all'id dell'autore del corso a cui sta cercando di accedere. 21 | // Potremmo evitare tale query se aggiungessimo un claim personalizzato contenente gli id dei suoi corsi. 22 | // Dopo aver ottenuto tali id dal database grazie al course service, li aggiungo come claim 23 | // identity.AddClaim(new Claim("AuthorOfCourses", "5,7,22")); 24 | 25 | return identity; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/ModelBinders/CourseListInputModelBinder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.ModelBinding; 2 | using Microsoft.Extensions.Options; 3 | using MyCourse.Models.InputModels.Courses; 4 | 5 | namespace MyCourse.Customizations.ModelBinders; 6 | 7 | public class CourseListInputModelBinder : IModelBinder 8 | { 9 | private readonly IOptionsMonitor coursesOptions; 10 | public CourseListInputModelBinder(IOptionsMonitor coursesOptions) 11 | { 12 | this.coursesOptions = coursesOptions; 13 | } 14 | public Task BindModelAsync(ModelBindingContext bindingContext) 15 | { 16 | //Recuperiamo i valori grazie ai value provider 17 | string search = bindingContext.ValueProvider.GetValue("Search").FirstValue; 18 | string orderBy = bindingContext.ValueProvider.GetValue("OrderBy").FirstValue; 19 | int.TryParse(bindingContext.ValueProvider.GetValue("Page").FirstValue, out int page); 20 | bool.TryParse(bindingContext.ValueProvider.GetValue("Ascending").FirstValue, out bool ascending); 21 | 22 | //Creiamo l'istanza del CourseListInputModel 23 | CoursesOptions options = coursesOptions.CurrentValue; 24 | CourseListInputModel inputModel = new(search, page, orderBy, ascending, options.PerPage, options.Order); 25 | 26 | //Impostiamo il risultato per notificare che la creazione è avvenuta con successo 27 | bindingContext.Result = ModelBindingResult.Success(inputModel); 28 | 29 | //Restituiamo un task completato 30 | return Task.CompletedTask; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/ModelBinders/DecimalModelBinder.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Microsoft.AspNetCore.Mvc.ModelBinding; 3 | 4 | namespace MyCourse.Customizations.ModelBinders; 5 | 6 | public class DecimalModelBinder : IModelBinder 7 | { 8 | public Task BindModelAsync(ModelBindingContext bindingContext) 9 | { 10 | string value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue; 11 | if (decimal.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal decimalValue)) 12 | { 13 | bindingContext.Result = ModelBindingResult.Success(decimalValue); 14 | } 15 | return Task.CompletedTask; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/ModelBinders/DecimalModelBinderProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.ModelBinding; 2 | 3 | namespace MyCourse.Customizations.ModelBinders; 4 | 5 | public class DecimalModelBinderProvider : IModelBinderProvider 6 | { 7 | public IModelBinder GetBinder(ModelBinderProviderContext context) 8 | { 9 | if (context.Metadata.ModelType == typeof(decimal)) 10 | { 11 | return new DecimalModelBinder(); 12 | } 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/TagHelpers/HtmlSanitizeTagHelper.cs: -------------------------------------------------------------------------------- 1 | using AngleSharp.Dom.Html; 2 | using Ganss.XSS; 3 | using Microsoft.AspNetCore.Razor.TagHelpers; 4 | 5 | namespace MyCourse.Customizations.TagHelpers; 6 | 7 | [HtmlTargetElement(Attributes = "html-sanitize")] 8 | public class HtmlSanitizeTagHelper : TagHelper 9 | { 10 | public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 11 | { 12 | //Otteniamo il contenuto del tag 13 | TagHelperContent tagHelperContent = await output.GetChildContentAsync(NullHtmlEncoder.Default); 14 | string content = tagHelperContent.GetContent(NullHtmlEncoder.Default); 15 | 16 | var sanitizer = CreateSanitizer(); 17 | content = sanitizer.Sanitize(content); 18 | 19 | //Reimpostiamo il contenuto del tag 20 | output.Content.SetHtmlContent(content); 21 | } 22 | 23 | private static HtmlSanitizer CreateSanitizer() 24 | { 25 | HtmlSanitizer sanitizer = new(); 26 | 27 | //Tag consentiti 28 | sanitizer.AllowedTags.Clear(); 29 | sanitizer.AllowedTags.Add("b"); 30 | sanitizer.AllowedTags.Add("i"); 31 | sanitizer.AllowedTags.Add("p"); 32 | sanitizer.AllowedTags.Add("br"); 33 | sanitizer.AllowedTags.Add("ul"); 34 | sanitizer.AllowedTags.Add("ol"); 35 | sanitizer.AllowedTags.Add("li"); 36 | sanitizer.AllowedTags.Add("iframe"); 37 | 38 | //Attributi consentiti 39 | sanitizer.AllowedAttributes.Clear(); 40 | sanitizer.AllowedAttributes.Add("src"); 41 | sanitizer.AllowDataAttributes = false; 42 | 43 | //Stili consentiti 44 | sanitizer.AllowedCssProperties.Clear(); 45 | 46 | sanitizer.FilterUrl += FilterUrl; 47 | sanitizer.PostProcessNode += ProcessIFrames; 48 | 49 | return sanitizer; 50 | } 51 | 52 | private static void FilterUrl(object sender, FilterUrlEventArgs filterUrlEventArgs) 53 | { 54 | if (!filterUrlEventArgs.OriginalUrl.StartsWith("//www.youtube.com/") && !filterUrlEventArgs.OriginalUrl.StartsWith("https://www.youtube.com/")) 55 | { 56 | filterUrlEventArgs.SanitizedUrl = null; 57 | } 58 | } 59 | 60 | private static void ProcessIFrames(object sender, PostProcessNodeEventArgs postProcessNodeEventArgs) 61 | { 62 | var iframe = postProcessNodeEventArgs.Node as IHtmlInlineFrameElement; 63 | if (iframe == null) 64 | { 65 | return; 66 | } 67 | var container = postProcessNodeEventArgs.Document.CreateElement("span"); 68 | container.ClassName = "video-container"; 69 | container.AppendChild(iframe.Clone(true)); 70 | postProcessNodeEventArgs.ReplacementNodes.Add(container); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/TagHelpers/InputNumberTagHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 3 | using Microsoft.AspNetCore.Razor.TagHelpers; 4 | 5 | namespace MyCourse.Customizations.TagHelpers; 6 | 7 | [HtmlTargetElement("input", Attributes = "asp-for")] 8 | public class InputNumberTagHelper : TagHelper 9 | { 10 | public override int Order => int.MaxValue; 11 | 12 | [HtmlAttributeName("asp-for")] 13 | public ModelExpression For { get; set; } 14 | 15 | public override void Process(TagHelperContext context, TagHelperOutput output) 16 | { 17 | bool isNumberInputType = output.Attributes.Any(attribute => "type".Equals(attribute.Name, StringComparison.InvariantCultureIgnoreCase) && "number".Equals(attribute.Value as string, StringComparison.InvariantCultureIgnoreCase)); 18 | if (!isNumberInputType) 19 | { 20 | return; 21 | } 22 | if (For.ModelExplorer.ModelType != typeof(decimal)) 23 | { 24 | return; 25 | } 26 | decimal value = (decimal)For.Model; 27 | string formattedValue = value.ToString("F2", CultureInfo.InvariantCulture); 28 | output.Attributes.SetAttribute("value", formattedValue); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/TagHelpers/OrderLinkTagHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.TagHelpers; 2 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 3 | using Microsoft.AspNetCore.Razor.TagHelpers; 4 | using MyCourse.Models.InputModels.Courses; 5 | 6 | namespace MyCourse.Customizations.TagHelpers; 7 | 8 | public class OrderLinkTagHelper : AnchorTagHelper 9 | { 10 | public string OrderBy { get; set; } 11 | public CourseListInputModel Input { get; set; } 12 | 13 | public OrderLinkTagHelper(IHtmlGenerator generator) : base(generator) 14 | { 15 | } 16 | 17 | public override void Process(TagHelperContext context, TagHelperOutput output) 18 | { 19 | output.TagName = "a"; 20 | 21 | //Imposto i valori del link 22 | RouteValues["search"] = Input.Search; 23 | RouteValues["orderby"] = OrderBy; 24 | RouteValues["ascending"] = (Input.OrderBy == OrderBy ? !Input.Ascending : Input.Ascending).ToString().ToLowerInvariant(); 25 | 26 | //Faccio generare l'output all'AnchorTagHelper 27 | base.Process(context, output); 28 | 29 | //Aggiungo l'indicatore di direzione 30 | if (Input.OrderBy == OrderBy) 31 | { 32 | var direction = Input.Ascending ? "up" : "down"; 33 | output.PostContent.SetHtmlContent($" "); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/TagHelpers/PriceTagHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace MyCourse.Customizations.TagHelpers; 4 | 5 | public class PriceTagHelper : TagHelper 6 | { 7 | public Money CurrentPrice { get; set; } 8 | public Money FullPrice { get; set; } 9 | public override void Process(TagHelperContext context, TagHelperOutput output) 10 | { 11 | output.TagName = "span"; 12 | output.Content.AppendHtml($"{CurrentPrice}"); 13 | 14 | if (!CurrentPrice.Equals(FullPrice)) 15 | { 16 | output.Content.AppendHtml($"
{FullPrice}"); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/TagHelpers/RatingTagHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace MyCourse.Customizations.TagHelpers; 4 | 5 | public class RatingTagHelper : TagHelper 6 | { 7 | public double Value { get; set; } 8 | public override void Process(TagHelperContext context, TagHelperOutput output) 9 | { 10 | //double value = (double) context.AllAttributes["value"].Value; 11 | 12 | for (int i = 1; i <= 5; i++) 13 | { 14 | if (Value >= i) 15 | { 16 | output.Content.AppendHtml(""); 17 | } 18 | else if (Value > i - 1) 19 | { 20 | output.Content.AppendHtml(""); 21 | } 22 | else 23 | { 24 | output.Content.AppendHtml(""); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/MyCourse/Customizations/ViewComponents/PaginationBarViewComponent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace MyCourse.Customizations.ViewComponents; 4 | 5 | public class PaginationBarViewComponent : ViewComponent 6 | { 7 | //public IViewComponentResult Invoke(CourseListViewModel model) 8 | public IViewComponentResult Invoke(IPaginationInfo model) 9 | { 10 | //Il numero di pagina corrente 11 | //Il numero di risultati totali 12 | //Il numero di risultati per pagina 13 | //Search, OrderBy e Ascending 14 | return View(model); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MyCourse/Data/MyCourse.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/Data/MyCourse.db -------------------------------------------------------------------------------- /src/MyCourse/Data/MyCourse.old.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/Data/MyCourse.old.db -------------------------------------------------------------------------------- /src/MyCourse/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using MyCourse.Models.Services.Application.Courses; 2 | global using MyCourse.Models.Services.Application.Lessons; 3 | global using MyCourse.Models.ValueObjects; 4 | global using MyCourse.Models.ViewModels; 5 | global using MyCourse.Models.Entities; 6 | global using MyCourse.Models.Enums; 7 | global using MyCourse.Models.Options; 8 | global using MyCourse.Models.Authorization; -------------------------------------------------------------------------------- /src/MyCourse/Migrations/20200517144246_UniqueCourseTitle.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace MyCourse.Migrations; 4 | 5 | public partial class UniqueCourseTitle : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.CreateIndex( 10 | name: "IX_Courses_Title", 11 | table: "Courses", 12 | column: "Title", 13 | unique: true); 14 | } 15 | 16 | protected override void Down(MigrationBuilder migrationBuilder) 17 | { 18 | migrationBuilder.DropIndex( 19 | name: "IX_Courses_Title", 20 | table: "Courses"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/MyCourse/Migrations/20200517152859_TriggersCourseVersion.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace MyCourse.Migrations 4 | { 5 | public partial class TriggersCourseVersion : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.Sql(@"CREATE TRIGGER CoursesSetRowVersionOnInsert 10 | AFTER INSERT ON Courses 11 | BEGIN 12 | UPDATE Courses SET RowVersion = CURRENT_TIMESTAMP WHERE Id=NEW.Id; 13 | END;"); 14 | migrationBuilder.Sql(@"CREATE TRIGGER CoursesSetRowVersionOnUpdate 15 | AFTER UPDATE ON Courses WHEN NEW.RowVersion <= OLD.RowVersion 16 | BEGIN 17 | UPDATE Courses SET RowVersion = CURRENT_TIMESTAMP WHERE Id=NEW.Id; 18 | END;"); 19 | migrationBuilder.Sql("UPDATE Courses SET RowVersion = CURRENT_TIMESTAMP;"); 20 | } 21 | 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.Sql("DROP TRIGGER CoursesSetRowVersionOnInsert;"); 25 | migrationBuilder.Sql("DROP TRIGGER CoursesSetRowVersionOnUpdate;"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/MyCourse/Migrations/20200531124830_LessonOrder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace MyCourse.Migrations; 4 | 5 | public partial class LessonOrder : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "Order", 11 | table: "Lessons", 12 | nullable: false, 13 | defaultValue: 1000); 14 | } 15 | 16 | protected override void Down(MigrationBuilder migrationBuilder) 17 | { 18 | migrationBuilder.DropColumn( 19 | name: "Order", 20 | table: "Lessons"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/MyCourse/Migrations/20200531124956_LessonVersion.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace MyCourse.Migrations; 4 | 5 | public partial class LessonVersion : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "RowVersion", 11 | table: "Lessons", 12 | nullable: true); 13 | migrationBuilder.Sql(@"CREATE TRIGGER LessonsSetRowVersionOnInsert 14 | AFTER INSERT ON Lessons 15 | BEGIN 16 | UPDATE Lessons SET RowVersion = CURRENT_TIMESTAMP WHERE Id=NEW.Id; 17 | END;"); 18 | migrationBuilder.Sql(@"CREATE TRIGGER LessonsSetRowVersionOnUpdate 19 | AFTER UPDATE ON Lessons WHEN NEW.RowVersion <= OLD.RowVersion 20 | BEGIN 21 | UPDATE Lessons SET RowVersion = CURRENT_TIMESTAMP WHERE Id=NEW.Id; 22 | END;"); 23 | migrationBuilder.Sql("UPDATE Lessons SET RowVersion = CURRENT_TIMESTAMP;"); 24 | } 25 | 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.DropColumn( 29 | name: "RowVersion", 30 | table: "Lessons"); 31 | migrationBuilder.Sql("DROP TRIGGER LessonsSetRowVersionOnInsert;"); 32 | migrationBuilder.Sql("DROP TRIGGER LessonsSetRowVersionOnUpdate;"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/MyCourse/Migrations/20200614144118_CourseStatus.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace MyCourse.Migrations; 4 | 5 | public partial class CourseStatus : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "Status", 11 | table: "Courses", 12 | nullable: false, 13 | defaultValue: nameof(Models.Enums.CourseStatus.Deleted) 14 | ); 15 | migrationBuilder.Sql($"UPDATE Courses SET Status='{nameof(Models.Enums.CourseStatus.Draft)}'"); 16 | } 17 | 18 | protected override void Down(MigrationBuilder migrationBuilder) 19 | { 20 | migrationBuilder.DropColumn( 21 | name: "Status", 22 | table: "Courses"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/MyCourse/Migrations/20200822153858_IdentityApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace MyCourse.Migrations; 4 | 5 | public partial class IdentityApplicationUser : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "FullName", 11 | table: "AspNetUsers", 12 | nullable: true); 13 | } 14 | 15 | protected override void Down(MigrationBuilder migrationBuilder) 16 | { 17 | migrationBuilder.DropColumn( 18 | name: "FullName", 19 | table: "AspNetUsers"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/MyCourse/Migrations/20210705180430_Subscriptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace MyCourse.Migrations; 4 | 5 | public partial class Subscriptions : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.CreateTable( 10 | name: "Subscriptions", 11 | columns: table => new 12 | { 13 | UserId = table.Column(type: "TEXT", nullable: false), 14 | CourseId = table.Column(type: "INTEGER", nullable: false), 15 | PaymentDate = table.Column(type: "TEXT", nullable: false), 16 | PaymentType = table.Column(type: "TEXT", nullable: true), 17 | Paid_Amount = table.Column(type: "REAL", nullable: true), 18 | Paid_Currency = table.Column(type: "TEXT", nullable: true), 19 | TransactionId = table.Column(type: "TEXT", nullable: true), 20 | Vote = table.Column(type: "INTEGER", nullable: true) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Subscriptions", x => new { x.CourseId, x.UserId }); 25 | table.ForeignKey( 26 | name: "FK_Subscriptions_AspNetUsers_UserId", 27 | column: x => x.UserId, 28 | principalTable: "AspNetUsers", 29 | principalColumn: "Id", 30 | onDelete: ReferentialAction.Cascade); 31 | table.ForeignKey( 32 | name: "FK_Subscriptions_Courses_CourseId", 33 | column: x => x.CourseId, 34 | principalTable: "Courses", 35 | principalColumn: "Id", 36 | onDelete: ReferentialAction.Cascade); 37 | }); 38 | 39 | migrationBuilder.CreateIndex( 40 | name: "IX_Subscriptions_UserId", 41 | table: "Subscriptions", 42 | column: "UserId"); 43 | } 44 | 45 | protected override void Down(MigrationBuilder migrationBuilder) 46 | { 47 | migrationBuilder.DropTable( 48 | name: "Subscriptions"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/MyCourse/Migrations/20210927072440_TriggerCourseRating.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace MyCourse.Migrations; 4 | 5 | public partial class TriggerCourseRating : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.Sql(@"CREATE TRIGGER SubscriptionsSetCourseRatingOnUpdate 10 | AFTER UPDATE ON Subscriptions 11 | FOR EACH ROW 12 | WHEN NEW.Vote <> OLD.Vote 13 | BEGIN 14 | UPDATE Courses SET Rating = (SELECT AVG(Vote) FROM Subscriptions WHERE CourseId = NEW.CourseId) WHERE Id = NEW.CourseId; 15 | END;"); 16 | } 17 | 18 | protected override void Down(MigrationBuilder migrationBuilder) 19 | { 20 | migrationBuilder.Sql("DROP TRIGGER SubscriptionsSetCourseRatingOnUpdate"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Authorization/CourseAuthorRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace MyCourse.Models.Authorization; 4 | 5 | public class CourseAuthorRequirement : IAuthorizationRequirement 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Authorization/CourseAuthorRequirementHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.AspNetCore.Authorization; 3 | 4 | namespace MyCourse.Models.Authorization; 5 | 6 | public class CourseAuthorRequirementHandler : AuthorizationHandler 7 | { 8 | private readonly IHttpContextAccessor httpContextAccessor; 9 | private readonly ICachedCourseService courseService; 10 | private readonly ILessonService lessonService; 11 | 12 | public CourseAuthorRequirementHandler(IHttpContextAccessor httpContextAccessor, ICachedCourseService courseService, ILessonService lessonService) 13 | { 14 | this.courseService = courseService; 15 | this.lessonService = lessonService; 16 | this.httpContextAccessor = httpContextAccessor; 17 | } 18 | 19 | protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, 20 | CourseAuthorRequirement requirement) 21 | { 22 | string userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); 23 | int courseId; 24 | if (context.Resource is int) 25 | { 26 | courseId = (int)context.Resource; 27 | } 28 | else 29 | { 30 | int id = Convert.ToInt32(httpContextAccessor.HttpContext.Request.RouteValues["id"]); 31 | if (id == 0) 32 | { 33 | context.Fail(); 34 | return; 35 | } 36 | 37 | // A quale controller sto cercando di accedere? 38 | switch (httpContextAccessor.HttpContext.Request.RouteValues["controller"].ToString().ToLowerInvariant()) 39 | { 40 | // Si tratta di una lezione. Otteniamo l'id del corso a cui appartiene 41 | case "lessons": 42 | courseId = (await lessonService.GetLessonAsync(id)).CourseId; 43 | break; 44 | 45 | // L'id era proprio quello di un corso 46 | case "courses": 47 | courseId = id; 48 | break; 49 | 50 | default: 51 | // Controller non supportato 52 | context.Fail(); 53 | return; 54 | } 55 | } 56 | 57 | string authorId = await courseService.GetCourseAuthorIdAsync(courseId); 58 | if (userId == authorId) 59 | { 60 | context.Succeed(requirement); 61 | } 62 | else 63 | { 64 | context.Fail(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Authorization/CourseLimitRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace MyCourse.Models.Authorization; 4 | 5 | public class CourseLimitRequirement : IAuthorizationRequirement 6 | { 7 | public CourseLimitRequirement(int limit) 8 | { 9 | Limit = limit; 10 | } 11 | 12 | public int Limit { get; } 13 | } 14 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Authorization/CourseLimitRequirementHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.AspNetCore.Authorization; 3 | 4 | namespace MyCourse.Models.Authorization; 5 | 6 | public class CourseLimitRequirementHandler : AuthorizationHandler 7 | { 8 | private readonly IHttpContextAccessor httpContextAccessor; 9 | private readonly ICachedCourseService courseService; 10 | 11 | public CourseLimitRequirementHandler(IHttpContextAccessor httpContextAccessor, ICachedCourseService courseService) 12 | { 13 | this.courseService = courseService; 14 | this.httpContextAccessor = httpContextAccessor; 15 | } 16 | 17 | protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, 18 | CourseLimitRequirement requirement) 19 | { 20 | // 1. Leggere l'id dell'utente dalla sua identità 21 | string userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); 22 | 23 | // 2. Estrarre dal database i corsi creati dall'utente 24 | int courseCount = await courseService.GetCourseCountByAuthorIdAsync(userId); 25 | 26 | // 3. Verificare che il numero di corsi sia minore o uguale al limite 27 | if (courseCount <= requirement.Limit) 28 | { 29 | context.Succeed(requirement); 30 | } 31 | else 32 | { 33 | context.Fail(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Authorization/CourseSubscriberRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace MyCourse.Models.Authorization; 4 | 5 | public class CourseSubscriberRequirement : IAuthorizationRequirement 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Authorization/CourseSubscriberRequirementHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.AspNetCore.Authorization; 3 | 4 | namespace MyCourse.Models.Authorization; 5 | 6 | public class CourseSubscriberRequirementHandler : AuthorizationHandler 7 | { 8 | private readonly IHttpContextAccessor httpContextAccessor; 9 | private readonly ICachedCourseService courseService; 10 | private readonly ILessonService lessonService; 11 | 12 | public CourseSubscriberRequirementHandler(IHttpContextAccessor httpContextAccessor, ICachedCourseService courseService, ILessonService lessonService) 13 | { 14 | this.courseService = courseService; 15 | this.lessonService = lessonService; 16 | this.httpContextAccessor = httpContextAccessor; 17 | } 18 | 19 | protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, 20 | CourseSubscriberRequirement requirement) 21 | { 22 | string userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); 23 | int courseId; 24 | if (context.Resource is int) 25 | { 26 | courseId = (int)context.Resource; 27 | } 28 | else 29 | { 30 | int id = Convert.ToInt32(httpContextAccessor.HttpContext.Request.RouteValues["id"]); 31 | if (id == 0) 32 | { 33 | context.Fail(); 34 | return; 35 | } 36 | 37 | // A quale controller sto cercando di accedere? 38 | switch (httpContextAccessor.HttpContext.Request.RouteValues["controller"].ToString().ToLowerInvariant()) 39 | { 40 | // Si tratta di una lezione. Otteniamo l'id del corso a cui appartiene 41 | case "lessons": 42 | courseId = (await lessonService.GetLessonAsync(id)).CourseId; 43 | break; 44 | 45 | // L'id era proprio quello di un corso 46 | case "courses": 47 | courseId = id; 48 | break; 49 | 50 | default: 51 | // Controller non supportato 52 | context.Fail(); 53 | return; 54 | } 55 | } 56 | 57 | bool isSubscribed = await courseService.IsCourseSubscribedAsync(courseId, userId); 58 | if (isSubscribed) 59 | { 60 | context.Succeed(requirement); 61 | } 62 | else 63 | { 64 | context.Fail(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Authorization/MultiAuthorizationPolicyProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace MyCourse.Models.Authorization; 5 | 6 | public class MultiAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider 7 | { 8 | private readonly IOptions options; 9 | private readonly IHttpContextAccessor httpContextAccessor; 10 | 11 | public MultiAuthorizationPolicyProvider(IHttpContextAccessor httpContextAccessor, IOptions options) : base(options) 12 | { 13 | this.httpContextAccessor = httpContextAccessor; 14 | this.options = options; 15 | } 16 | 17 | public override async Task GetPolicyAsync(string policyName) 18 | { 19 | var policy = await base.GetPolicyAsync(policyName); 20 | if (policy != null) 21 | { 22 | return policy; 23 | } 24 | 25 | var policyNames = policyName.Split(',', System.StringSplitOptions.RemoveEmptyEntries).Select(name => name.Trim()).ToArray(); 26 | var builder = new AuthorizationPolicyBuilder(); 27 | builder.RequireAssertion(async (context) => 28 | { 29 | var authService = httpContextAccessor.HttpContext.RequestServices.GetService(); 30 | foreach (var policyName in policyNames) 31 | { 32 | var result = await authService.AuthorizeAsync(context.User, context.Resource, policyName); 33 | if (result.Succeeded) 34 | { 35 | return true; 36 | } 37 | } 38 | 39 | return false; 40 | }); 41 | 42 | return builder.Build(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Entities/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using Microsoft.AspNetCore.Identity; 3 | 4 | namespace MyCourse.Models.Entities; 5 | 6 | public class ApplicationUser : IdentityUser 7 | { 8 | public string FullName { get; set; } 9 | public virtual ICollection AuthoredCourses { get; set; } 10 | public virtual ICollection SubscribedCourses { get; set; } 11 | 12 | public static ApplicationUser FromDataRow(DataRow userRow) 13 | { 14 | ApplicationUser applicationUser = new() 15 | { 16 | Id = Convert.ToString(userRow["Id"]), 17 | UserName = Convert.ToString(userRow["UserName"]), 18 | NormalizedUserName = Convert.ToString(userRow["NormalizedUserName"]), 19 | Email = Convert.ToString(userRow["Email"]), 20 | NormalizedEmail = Convert.ToString(userRow["NormalizedEmail"]), 21 | EmailConfirmed = Convert.ToBoolean(userRow["EmailConfirmed"]), 22 | PasswordHash = Convert.ToString(userRow["PasswordHash"]), 23 | SecurityStamp = Convert.ToString(userRow["SecurityStamp"]), 24 | ConcurrencyStamp = Convert.ToString(userRow["ConcurrencyStamp"]), 25 | PhoneNumber = Convert.ToString(userRow["PhoneNumber"]), 26 | PhoneNumberConfirmed = Convert.ToBoolean(userRow["PhoneNumberConfirmed"]), 27 | TwoFactorEnabled = Convert.ToBoolean(userRow["TwoFactorEnabled"]), 28 | LockoutEnd = (userRow["LockoutEnd"] == DBNull.Value ? (DateTime?)null : Convert.ToDateTime(userRow["LockoutEnd"])), 29 | LockoutEnabled = Convert.ToBoolean(userRow["LockoutEnabled"]), 30 | AccessFailedCount = Convert.ToInt32(userRow["AccessFailedCount"]), 31 | FullName = Convert.ToString(userRow["FullName"]) 32 | }; 33 | return applicationUser; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Entities/Lesson.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Entities; 2 | 3 | public partial class Lesson 4 | { 5 | public Lesson(string title, int courseId) 6 | { 7 | ChangeTitle(title); 8 | CourseId = courseId; 9 | Order = 1000; 10 | Duration = TimeSpan.FromSeconds(0); 11 | } 12 | public int Id { get; private set; } 13 | public int CourseId { get; private set; } 14 | public string Title { get; private set; } 15 | public string Description { get; private set; } 16 | public int Order { get; private set; } 17 | public TimeSpan Duration { get; private set; } //00:00:00 18 | public string RowVersion { get; private set; } 19 | public virtual Course Course { get; set; } 20 | 21 | public void ChangeTitle(string title) 22 | { 23 | if (string.IsNullOrEmpty(title)) 24 | { 25 | throw new ArgumentException("A lesson must have a title"); 26 | } 27 | Title = title; 28 | } 29 | 30 | public void ChangeDescription(string description) 31 | { 32 | Description = description; 33 | } 34 | 35 | public void ChangeDuration(TimeSpan duration) 36 | { 37 | Duration = duration; 38 | } 39 | 40 | public void ChangeOrder(int order) 41 | { 42 | Order = order; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Entities/Subscription.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Entities; 2 | 3 | public class Subscription 4 | { 5 | public Subscription(string userId, int courseId) 6 | { 7 | UserId = userId; 8 | CourseId = courseId; 9 | } 10 | 11 | public string UserId { get; set; } 12 | public int CourseId { get; set; } 13 | public DateTime PaymentDate { get; set; } 14 | public string PaymentType { get; set; } 15 | public Money Paid { get; set; } 16 | public string TransactionId { get; set; } 17 | public int? Vote { get; set; } 18 | 19 | public virtual Course Course { get; set; } 20 | public virtual ApplicationUser User { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Enums/CourseStatus.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Enums; 2 | 3 | public enum CourseStatus 4 | { 5 | Draft, 6 | Published, 7 | Deleted 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Enums/Currency.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Enums; 2 | 3 | public enum Currency 4 | { 5 | EUR, 6 | USD, 7 | GBP 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Enums/Persistence.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Enums; 2 | 3 | public enum Persistence 4 | { 5 | AdoNet, 6 | EfCore 7 | } 8 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Enums/Policy.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Enums; 2 | 3 | public enum Policy 4 | { 5 | CourseAuthor, 6 | CourseSubscriber, 7 | CourseLimit 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Enums/Role.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace MyCourse.Models.Enums; 4 | 5 | public enum Role 6 | { 7 | [Display(Name = "Amministratore")] 8 | Administrator, 9 | [Display(Name = "Docente")] 10 | Teacher 11 | } 12 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Application/CourseImageInvalidException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Application; 2 | 3 | public class CourseImageInvalidException : Exception 4 | { 5 | public CourseImageInvalidException(int courseId, Exception innerException) : base($"Image for course '{courseId}' is not valid", innerException) 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Application/CourseNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Application; 2 | 3 | public class CourseNotFoundException : Exception 4 | { 5 | public CourseNotFoundException(int courseId) : base($"Course {courseId} not found") 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Application/CourseSubscriptionException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Application; 2 | 3 | public class CourseSubscriptionException : Exception 4 | { 5 | public CourseSubscriptionException(int courseId) : base($"Could not subscribe to course {courseId}") 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Application/CourseSubscriptionNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Application; 2 | 3 | public class CourseSubscriptionNotFoundException : Exception 4 | { 5 | public CourseSubscriptionNotFoundException(int courseId) : base($"Subscription to course {courseId} not found") 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Application/CourseTitleUnavailableException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Application; 2 | 3 | public class CourseTitleUnavailableException : Exception 4 | { 5 | public CourseTitleUnavailableException(string title, Exception innerException) : base($"Course title '{title}' existed", innerException) 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Application/InvalidVoteException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Application; 2 | 3 | public class InvalidVoteException : Exception 4 | { 5 | public InvalidVoteException(int vote) : base($"Il voto {vote} non è valido") 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Application/LessonNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Application; 2 | 3 | public class LessonNotFoundException : Exception 4 | { 5 | public LessonNotFoundException(int lessonId) : base($"Lesson {lessonId} not found") 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Application/OptimisticConcurrencyException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Application; 2 | 3 | public class OptimisticConcurrencyException : Exception 4 | { 5 | public OptimisticConcurrencyException() : base($"Couldn't update row because it was updated by another user") 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Application/SendException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Application; 2 | 3 | public class SendException : Exception 4 | { 5 | public SendException() : base($"Couldn't send the message") 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Application/UserUnkownException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Application; 2 | 3 | public class UserUnknownException : Exception 4 | { 5 | public UserUnknownException() : base($"A known user is required for this operation") 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Infrastructure/ConstraintViolationException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Infrastructure; 2 | 3 | public class ConstraintViolationException : Exception 4 | { 5 | public ConstraintViolationException(Exception innerException) : base($"A violation occurred for a database constraint", innerException) 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Infrastructure/ImagePersistenceException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Infrastructure; 2 | 3 | public class ImagePersistenceException : Exception 4 | { 5 | public ImagePersistenceException(Exception innerException) : base("Couldn't persist the image", innerException) 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Exceptions/Infrastructure/PaymentGatewayException.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Exceptions.Infrastructure; 2 | 3 | public class PaymentGatewayException : Exception 4 | { 5 | public PaymentGatewayException(Exception innerException) : base($"Payment gateway threw an exception", innerException) 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/InputModels/Courses/CourseCreateInputModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | using MyCourse.Controllers; 4 | 5 | namespace MyCourse.Models.InputModels.Courses; 6 | 7 | public class CourseCreateInputModel 8 | { 9 | [Required(ErrorMessage = "Il titolo è obbligatorio"), 10 | MinLength(10, ErrorMessage = "Il titolo dev'essere di almeno {1} caratteri"), 11 | MaxLength(100, ErrorMessage = "Il titolo dev'essere di al massimo {1} caratteri"), 12 | RegularExpression(@"^[0-9A-z\u00C0-\u00ff\s\.']+$", ErrorMessage = "Titolo non valido"), //Questa espressione regolare include anche i caratteri accentati 13 | Remote(action: nameof(CoursesController.IsTitleAvailable), controller: "Courses", ErrorMessage = "Il titolo esiste già")] 14 | public string Title { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/MyCourse/Models/InputModels/Courses/CourseDeleteInputModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace MyCourse.Models.InputModels.Courses; 4 | 5 | public class CourseDeleteInputModel 6 | { 7 | [Required] 8 | public int Id { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/MyCourse/Models/InputModels/Courses/CourseListInputModel.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using MyCourse.Customizations.ModelBinders; 3 | 4 | namespace MyCourse.Models.InputModels.Courses; 5 | 6 | [ModelBinder(BinderType = typeof(CourseListInputModelBinder))] 7 | public class CourseListInputModel 8 | { 9 | public CourseListInputModel(string search, int page, string orderby, bool ascending, int limit, CoursesOrderOptions orderOptions) 10 | { 11 | if (!orderOptions.Allow.Contains(orderby)) 12 | { 13 | orderby = orderOptions.By; 14 | ascending = orderOptions.Ascending; 15 | } 16 | 17 | Search = search ?? ""; 18 | Page = Math.Max(1, page); 19 | Limit = Math.Max(1, limit); 20 | OrderBy = orderby; 21 | Ascending = ascending; 22 | 23 | Offset = (Page - 1) * Limit; 24 | } 25 | public string Search { get; } 26 | public int Page { get; } 27 | public string OrderBy { get; } 28 | public bool Ascending { get; } 29 | 30 | public int Limit { get; } 31 | public int Offset { get; } 32 | } 33 | -------------------------------------------------------------------------------- /src/MyCourse/Models/InputModels/Courses/CoursePayInputModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.InputModels.Courses; 2 | 3 | public class CoursePayInputModel 4 | { 5 | public int CourseId { get; set; } 6 | public string UserId { get; set; } 7 | public string Description { get; set; } 8 | public Money Price { get; set; } 9 | public string ReturnUrl { get; set; } 10 | public string CancelUrl { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/MyCourse/Models/InputModels/Courses/CourseSubscribeInputModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace MyCourse.Models.InputModels.Courses; 4 | 5 | public class CourseSubscribeInputModel 6 | { 7 | [Required] 8 | public int CourseId { get; set; } 9 | [Required] 10 | public string UserId { get; set; } 11 | public DateTime PaymentDate { get; set; } 12 | public string PaymentType { get; set; } 13 | public Money Paid { get; set; } 14 | public string TransactionId { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/MyCourse/Models/InputModels/Courses/CourseVoteInputModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace MyCourse.Models.InputModels.Courses; 4 | 5 | public class CourseVoteInputModel 6 | { 7 | [Required] 8 | public int Id { get; set; } 9 | 10 | [Required] 11 | public int Vote { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/MyCourse/Models/InputModels/Lessons/LessonCreateInputModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace MyCourse.Models.InputModels.Lessons; 4 | 5 | public class LessonCreateInputModel 6 | { 7 | [Required] 8 | public int CourseId { get; set; } 9 | 10 | [Required(ErrorMessage = "Il titolo è obbligatorio"), 11 | MinLength(5, ErrorMessage = "Il titolo dev'essere di almeno {1} caratteri"), 12 | MaxLength(100, ErrorMessage = "Il titolo dev'essere di al massimo {1} caratteri"), 13 | RegularExpression(@"^[0-9A-z\u00C0-\u00ff\s\.']+$", ErrorMessage = "Titolo non valido")] //Questa espressione regolare include anche i caratteri accentati 14 | public string Title { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/MyCourse/Models/InputModels/Lessons/LessonDeleteInputModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace MyCourse.Models.InputModels.Lessons; 4 | 5 | public class LessonDeleteInputModel 6 | { 7 | [Required] 8 | public int Id { get; set; } 9 | public int CourseId { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/MyCourse/Models/InputModels/Lessons/LessonEditInputModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Data; 3 | 4 | 5 | namespace MyCourse.Models.InputModels.Lessons; 6 | 7 | public class LessonEditInputModel 8 | { 9 | [Required] 10 | public int Id { get; set; } 11 | 12 | public int CourseId { get; set; } 13 | 14 | [Required(ErrorMessage = "Il titolo è obbligatorio"), 15 | MinLength(5, ErrorMessage = "Il titolo dev'essere di almeno {1} caratteri"), 16 | MaxLength(100, ErrorMessage = "Il titolo dev'essere di al massimo {1} caratteri"), 17 | RegularExpression(@"^[0-9A-z\u00C0-\u00ff\s\.']+$", ErrorMessage = "Titolo non valido"), //Questa espressione regolare include anche le lettere accentate 18 | Display(Name = "Titolo")] 19 | public string Title { get; set; } 20 | 21 | [MinLength(10, ErrorMessage = "La descrizione dev'essere di almeno {1} caratteri"), 22 | MaxLength(4000, ErrorMessage = "La descrizione dev'essere di massimo {1} caratteri"), 23 | Display(Name = "Descrizione")] 24 | public string Description { get; set; } 25 | 26 | [Display(Name = "Durata stimata"), 27 | Required(ErrorMessage = "La durata è richiesta")] 28 | public TimeSpan Duration { get; set; } 29 | 30 | [Display(Name = "Ordine"), 31 | Required(ErrorMessage = "L'ordine è richiesto")] 32 | public int Order { get; set; } 33 | public string RowVersion { get; set; } 34 | 35 | 36 | public static LessonEditInputModel FromDataRow(DataRow courseRow) 37 | { 38 | LessonEditInputModel lessonEditInputModel = new() 39 | { 40 | Id = Convert.ToInt32(courseRow["Id"]), 41 | CourseId = Convert.ToInt32(courseRow["CourseId"]), 42 | Title = Convert.ToString(courseRow["Title"]), 43 | Description = Convert.ToString(courseRow["Description"]), 44 | Duration = TimeSpan.Parse(Convert.ToString(courseRow["Duration"])), 45 | Order = Convert.ToInt32(courseRow["Order"]), 46 | RowVersion = Convert.ToString(courseRow["RowVersion"]) 47 | }; 48 | return lessonEditInputModel; 49 | } 50 | 51 | public static LessonEditInputModel FromEntity(Lesson lesson) 52 | { 53 | return new LessonEditInputModel 54 | { 55 | Id = lesson.Id, 56 | CourseId = lesson.CourseId, 57 | Title = lesson.Title, 58 | Description = lesson.Description, 59 | Duration = lesson.Duration, 60 | Order = lesson.Order, 61 | RowVersion = lesson.RowVersion 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/MyCourse/Models/InputModels/Users/UserRoleInputModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace MyCourse.Models.InputModels.Users; 4 | 5 | public class UserRoleInputModel 6 | { 7 | [Required(ErrorMessage = "L'indirizzo email è obbligatorio"), 8 | EmailAddress(ErrorMessage = "L'indirizzo email digitato non è valido"), 9 | Display(Name = "Indirizzo email")] 10 | public string Email { get; set; } 11 | 12 | [Required(ErrorMessage = "Il ruolo è obbligatorio"), 13 | Display(Name = "Ruolo")] 14 | public Role Role { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Options/ConnectionStringsOptions.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Options; 2 | 3 | public class ConnectionStringsOptions 4 | { 5 | public string Default { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Options/CoursesOptions.cs: -------------------------------------------------------------------------------- 1 | // Generated by https://quicktype.io 2 | namespace MyCourse.Models.Options; 3 | 4 | public partial class CoursesOptions 5 | { 6 | public int PerPage { get; set; } 7 | public int InHome { get; set; } 8 | public CoursesOrderOptions Order { get; set; } 9 | } 10 | 11 | public partial class CoursesOrderOptions 12 | { 13 | public string By { get; set; } 14 | public bool Ascending { get; set; } 15 | public string[] Allow { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Options/PaypalOptions.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Options; 2 | 3 | public class PaypalOptions 4 | { 5 | public string ClientId { get; set; } 6 | public string ClientSecret { get; set; } 7 | public bool IsSandbox { get; set; } 8 | public string BrandName { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Options/SmtpOptions.cs: -------------------------------------------------------------------------------- 1 | using MailKit.Security; 2 | 3 | namespace MyCourse.Models.Options; 4 | 5 | public class SmtpOptions 6 | { 7 | public string Host { get; set; } 8 | public int Port { get; set; } 9 | public SecureSocketOptions Security { get; set; } 10 | public string Username { get; set; } 11 | public string Password { get; set; } 12 | public string Sender { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Options/StripeOptions.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Options; 2 | 3 | public class StripeOptions 4 | { 5 | public string PrivateKey { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Options/UsersOptions.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Options; 2 | 3 | public class UsersOptions 4 | { 5 | public string AssignAdministratorRoleOnRegistration { get; set; } 6 | public string NotificationEmailRecipient { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Application/Courses/CourseService.cs: -------------------------------------------------------------------------------- 1 | using MyCourse.Models.ViewModels.Courses; 2 | using MyCourse.Models.ViewModels.Lessons; 3 | 4 | namespace MyCourse.Models.Services.Application.Courses; 5 | 6 | [Obsolete] 7 | ///Questo è un servizio che abbiamo usato all'inizio del progetto per fornire dati di test. Ora non è usato. 8 | public class CourseService // : ICourseService 9 | { 10 | public List GetCourses() 11 | { 12 | List courseList = new(); 13 | Random rand = new(); 14 | for (int i = 1; i <= 20; i++) 15 | { 16 | decimal price = Convert.ToDecimal(rand.NextDouble() * 10 + 10); 17 | CourseViewModel course = new() 18 | { 19 | Id = i, 20 | Title = $"Corso {i}", 21 | CurrentPrice = new Money(Currency.EUR, price), 22 | FullPrice = new Money(Currency.EUR, rand.NextDouble() > 0.5 ? price : price - 1), 23 | Author = "Nome cognome", 24 | Rating = rand.Next(10, 50) / 10.0, 25 | ImagePath = "/logo.svg" 26 | }; 27 | courseList.Add(course); 28 | } 29 | return courseList; 30 | } 31 | 32 | public CourseDetailViewModel GetCourse(int id) 33 | { 34 | Random rand = new(); 35 | decimal price = Convert.ToDecimal(rand.NextDouble() * 10 + 10); 36 | CourseDetailViewModel course = new() 37 | { 38 | Id = id, 39 | Title = $"Corso {id}", 40 | CurrentPrice = new Money(Currency.EUR, price), 41 | FullPrice = new Money(Currency.EUR, rand.NextDouble() > 0.5 ? price : price - 1), 42 | Author = "Nome cognome", 43 | Rating = rand.Next(10, 50) / 10.0, 44 | ImagePath = "/logo.svg", 45 | Description = $"Descrizione {id}", 46 | Lessons = new List() 47 | }; 48 | 49 | for (var i = 1; i <= 5; i++) 50 | { 51 | LessonViewModel lesson = new() 52 | { 53 | Title = $"Lezione {i}", 54 | Duration = TimeSpan.FromSeconds(rand.Next(40, 90)) 55 | }; 56 | course.Lessons.Add(lesson); 57 | } 58 | 59 | return course; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Application/Courses/ICachedCourseService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Services.Application.Courses; 2 | 3 | public interface ICachedCourseService : ICourseService 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Application/Courses/ICourseService.cs: -------------------------------------------------------------------------------- 1 | using MyCourse.Models.InputModels.Courses; 2 | using MyCourse.Models.ViewModels.Courses; 3 | 4 | namespace MyCourse.Models.Services.Application.Courses 5 | { 6 | public interface ICourseService 7 | { 8 | Task> GetCoursesAsync(CourseListInputModel model); 9 | Task GetCourseAsync(int id); 10 | Task> GetMostRecentCoursesAsync(); 11 | Task> GetBestRatingCoursesAsync(); 12 | Task GetCourseForEditingAsync(int id); 13 | Task CreateCourseAsync(CourseCreateInputModel inputModel); 14 | Task EditCourseAsync(CourseEditInputModel inputModel); 15 | Task DeleteCourseAsync(CourseDeleteInputModel inputModel); 16 | Task IsTitleAvailableAsync(string title, int excludeId); 17 | Task GetCourseAuthorIdAsync(int courseId); 18 | Task SendQuestionToCourseAuthorAsync(int courseId, string question); 19 | Task GetCourseCountByAuthorIdAsync(string authorId); 20 | Task SubscribeCourseAsync(CourseSubscribeInputModel inputModel); 21 | Task IsCourseSubscribedAsync(int courseId, string userId); 22 | Task GetPaymentUrlAsync(int courseId); 23 | Task CapturePaymentAsync(int courseId, string token); 24 | Task GetCourseVoteAsync(int courseId); 25 | Task VoteCourseAsync(CourseVoteInputModel inputModel); 26 | } 27 | } -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Application/Lessons/ICachedLessonService.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Services.Application.Lessons; 2 | 3 | public interface ICachedLessonService : ILessonService 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Application/Lessons/ILessonService.cs: -------------------------------------------------------------------------------- 1 | using MyCourse.Models.InputModels.Lessons; 2 | using MyCourse.Models.ViewModels.Lessons; 3 | 4 | namespace MyCourse.Models.Services.Application.Lessons; 5 | 6 | public interface ILessonService 7 | { 8 | Task GetLessonAsync(int id); 9 | Task GetLessonForEditingAsync(int id); 10 | Task CreateLessonAsync(LessonCreateInputModel inputModel); 11 | Task EditLessonAsync(LessonEditInputModel inputModel); 12 | Task DeleteLessonAsync(LessonDeleteInputModel id); 13 | } 14 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Application/Lessons/MemoryCacheLessonService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Memory; 2 | using MyCourse.Models.InputModels.Lessons; 3 | using MyCourse.Models.ViewModels.Lessons; 4 | 5 | namespace MyCourse.Models.Services.Application.Lessons; 6 | 7 | public class MemoryCacheLessonService : ICachedLessonService 8 | { 9 | private readonly ILessonService lessonService; 10 | private readonly IMemoryCache memoryCache; 11 | 12 | public MemoryCacheLessonService(ILessonService lessonService, IMemoryCache memoryCache) 13 | { 14 | this.lessonService = lessonService; 15 | this.memoryCache = memoryCache; 16 | } 17 | 18 | public Task GetLessonAsync(int id) 19 | { 20 | return memoryCache.GetOrCreateAsync($"Lesson{id}", cacheEntry => 21 | { 22 | cacheEntry.SetAbsoluteExpiration(TimeSpan.FromSeconds(60)); //Esercizio: provate a recuperare il valore 60 usando il servizio di configurazione 23 | return lessonService.GetLessonAsync(id); 24 | }); 25 | } 26 | 27 | public async Task CreateLessonAsync(LessonCreateInputModel inputModel) 28 | { 29 | LessonDetailViewModel viewModel = await lessonService.CreateLessonAsync(inputModel); 30 | memoryCache.Remove($"Course{viewModel.CourseId}"); 31 | return viewModel; 32 | } 33 | 34 | public async Task EditLessonAsync(LessonEditInputModel inputModel) 35 | { 36 | LessonDetailViewModel viewModel = await lessonService.EditLessonAsync(inputModel); 37 | memoryCache.Remove($"Course{viewModel.CourseId}"); 38 | memoryCache.Remove($"Lesson{viewModel.Id}"); 39 | return viewModel; 40 | } 41 | 42 | public Task GetLessonForEditingAsync(int id) 43 | { 44 | return lessonService.GetLessonForEditingAsync(id); 45 | } 46 | 47 | public async Task DeleteLessonAsync(LessonDeleteInputModel inputModel) 48 | { 49 | await lessonService.DeleteLessonAsync(inputModel); 50 | memoryCache.Remove($"Course{inputModel.CourseId}"); 51 | memoryCache.Remove($"Lesson{inputModel.Id}"); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Infrastructure/IDatabaseAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace MyCourse.Models.Services.Infrastructure; 4 | 5 | public interface IDatabaseAccessor 6 | { 7 | Task QueryAsync(FormattableString formattableQuery, CancellationToken token = default(CancellationToken)); 8 | Task QueryScalarAsync(FormattableString formattableQuery, CancellationToken token = default(CancellationToken)); 9 | Task CommandAsync(FormattableString formattableCommand, CancellationToken token = default(CancellationToken)); 10 | } 11 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Infrastructure/IEmailClient.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity.UI.Services; 2 | 3 | namespace MyCourse.Models.Services.Infrastructure; 4 | 5 | public interface IEmailClient : IEmailSender 6 | { 7 | Task SendEmailAsync(string recipientEmail, string replyToEmail, string subject, string htmlMessage); 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Infrastructure/IImagePersister.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Services.Infrastructure; 2 | 3 | public interface IImagePersister 4 | { 5 | /// The image URL e.g. /Courses/1.jpg 6 | Task SaveCourseImageAsync(int courseId, IFormFile formFile); 7 | } 8 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Infrastructure/IPaymentGateway.cs: -------------------------------------------------------------------------------- 1 | using MyCourse.Models.InputModels.Courses; 2 | 3 | namespace MyCourse.Models.Services.Infrastructure; 4 | 5 | public interface IPaymentGateway 6 | { 7 | Task GetPaymentUrlAsync(CoursePayInputModel inputModel); 8 | Task CapturePaymentAsync(string token); 9 | } 10 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Infrastructure/ITransactionLogger.cs: -------------------------------------------------------------------------------- 1 | using MyCourse.Models.InputModels.Courses; 2 | 3 | namespace MyCourse.Models.Services.Infrastructure; 4 | 5 | public interface ITransactionLogger 6 | { 7 | Task LogTransactionAsync(CourseSubscribeInputModel inputModel); 8 | } 9 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Infrastructure/InsecureImagePersister.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.Services.Infrastructure; 2 | 3 | public class InsecureImagePersister : IImagePersister 4 | { 5 | private readonly IWebHostEnvironment env; 6 | 7 | public InsecureImagePersister(IWebHostEnvironment env) 8 | { 9 | this.env = env; 10 | } 11 | 12 | public async Task SaveCourseImageAsync(int courseId, IFormFile formFile) 13 | { 14 | //Salvare il file 15 | string path = $"/Courses/{courseId}.jpg"; 16 | string physicalPath = Path.Combine(env.WebRootPath, "Courses", $"{courseId}.jpg"); 17 | using FileStream fileStream = File.OpenWrite(physicalPath); 18 | await formFile.CopyToAsync(fileStream); 19 | 20 | //Restituire il percorso al file 21 | return path; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Infrastructure/LocalTransactionLogger.cs: -------------------------------------------------------------------------------- 1 | using MyCourse.Models.InputModels.Courses; 2 | 3 | namespace MyCourse.Models.Services.Infrastructure; 4 | 5 | public class LocalTransactionLogger : ITransactionLogger 6 | { 7 | private readonly IHostEnvironment env; 8 | private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); 9 | 10 | public LocalTransactionLogger(IHostEnvironment env) 11 | { 12 | this.env = env; 13 | } 14 | 15 | public async Task LogTransactionAsync(CourseSubscribeInputModel inputModel) 16 | { 17 | string filePath = Path.Combine(env.ContentRootPath, "Data", "transactions.txt"); 18 | string content = $"\r\n{inputModel.TransactionId}\t{inputModel.PaymentDate}\t{inputModel.PaymentType}\t{inputModel.Paid}\t{inputModel.UserId}\t{inputModel.CourseId}"; 19 | try 20 | { 21 | await semaphore.WaitAsync(); 22 | await File.AppendAllTextAsync(filePath, content); 23 | } 24 | finally 25 | { 26 | semaphore.Release(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Infrastructure/MagickNetImagePersister.cs: -------------------------------------------------------------------------------- 1 | using ImageMagick; 2 | using MyCourse.Models.Exceptions.Infrastructure; 3 | 4 | namespace MyCourse.Models.Services.Infrastructure; 5 | 6 | public class MagickNetImagePersister : IImagePersister 7 | { 8 | private readonly IWebHostEnvironment env; 9 | 10 | private readonly SemaphoreSlim semaphore; 11 | 12 | public MagickNetImagePersister(IWebHostEnvironment env) 13 | { 14 | ResourceLimits.Height = 4000; 15 | ResourceLimits.Width = 4000; 16 | semaphore = new SemaphoreSlim(2); 17 | this.env = env; 18 | } 19 | 20 | public async Task SaveCourseImageAsync(int courseId, IFormFile formFile) 21 | { 22 | //Il metodo WaitAsync ha anche un overload che permette di passare un timeout 23 | //Ad esempio, se vogliamo aspettare al massimo 1 secondo: 24 | //await semaphore.AwaitAsync(TimeSpan.FromSeconds(1)); 25 | //Se il timeout scade, il SemaphoreSlim solleverà un'eccezione (così almeno non resta in attesa all'infinito) 26 | await semaphore.WaitAsync(); 27 | try 28 | { 29 | //Salvare il file 30 | string path = $"/Courses/{courseId}.jpg"; 31 | string physicalPath = Path.Combine(env.WebRootPath, "Courses", $"{courseId}.jpg"); 32 | 33 | using Stream inputStream = formFile.OpenReadStream(); 34 | using MagickImage image = new(inputStream); 35 | 36 | //Manipolare l'immagine 37 | int width = 300; //Esercizio: ottenere questi valori dalla configurazione 38 | int height = 300; 39 | MagickGeometry resizeGeometry = new(width, height) 40 | { 41 | FillArea = true 42 | }; 43 | image.Resize(resizeGeometry); 44 | image.Crop(width, width, Gravity.Northwest); 45 | 46 | image.Quality = 70; 47 | image.Write(physicalPath, MagickFormat.Jpg); 48 | 49 | //Restituire il percorso al file 50 | return path; 51 | } 52 | catch (Exception exc) 53 | { 54 | throw new ImagePersistenceException(exc); 55 | } 56 | finally 57 | { 58 | semaphore.Release(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/MyCourse/Models/Services/Infrastructure/MailKitEmailSender.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using MailKit.Net.Smtp; 3 | using MimeKit; 4 | 5 | namespace MyCourse.Models.Services.Infrastructure; 6 | 7 | public class MailKitEmailSender : IEmailClient 8 | { 9 | private readonly IOptionsMonitor smtpOptionsMonitor; 10 | private readonly ILogger logger; 11 | public MailKitEmailSender(IOptionsMonitor smtpOptionsMonitor, ILogger logger) 12 | { 13 | this.logger = logger; 14 | this.smtpOptionsMonitor = smtpOptionsMonitor; 15 | } 16 | 17 | public Task SendEmailAsync(string email, string subject, string htmlMessage) 18 | { 19 | return SendEmailAsync(email, string.Empty, subject, htmlMessage); 20 | } 21 | 22 | public async Task SendEmailAsync(string recipientEmail, string replyToEmail, string subject, string htmlMessage) 23 | { 24 | try 25 | { 26 | var options = this.smtpOptionsMonitor.CurrentValue; 27 | using SmtpClient client = new(); 28 | await client.ConnectAsync(options.Host, options.Port, options.Security); 29 | if (!string.IsNullOrEmpty(options.Username)) 30 | { 31 | await client.AuthenticateAsync(options.Username, options.Password); 32 | } 33 | MimeMessage message = new(); 34 | message.From.Add(MailboxAddress.Parse(options.Sender)); 35 | message.To.Add(MailboxAddress.Parse(recipientEmail)); 36 | 37 | if (replyToEmail is not (null or "")) 38 | { 39 | message.ReplyTo.Add(MailboxAddress.Parse(replyToEmail)); 40 | } 41 | 42 | message.Subject = subject; 43 | message.Body = new TextPart("html") 44 | { 45 | Text = htmlMessage 46 | }; 47 | await client.SendAsync(message); 48 | await client.DisconnectAsync(true); 49 | } 50 | catch (Exception exc) 51 | { 52 | logger.LogError(exc, "Couldn't send email to {email} with message {message}", recipientEmail, htmlMessage); 53 | throw; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/MyCourse/Models/ValueObjects/Money.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.ValueObjects; 2 | 3 | public record Money 4 | { 5 | public Money() : this(Currency.EUR, 0.00m) 6 | { 7 | } 8 | public Money(Currency currency, decimal amount) 9 | { 10 | Amount = amount; 11 | Currency = currency; 12 | } 13 | private decimal amount = 0; 14 | public decimal Amount 15 | { 16 | get 17 | { 18 | return amount; 19 | } 20 | init 21 | { 22 | if (value < 0) 23 | { 24 | throw new InvalidOperationException("The amount cannot be negative"); 25 | } 26 | amount = value; 27 | } 28 | } 29 | public Currency Currency 30 | { 31 | get; init; 32 | } 33 | 34 | public override string ToString() 35 | { 36 | return $"{Currency} {Amount:0.00}"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/MyCourse/Models/ValueObjects/Sql.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.ValueObjects; 2 | 3 | //Questa classe serve unicamente per indicare al servizio infrastrutturale SqliteAccessor 4 | //che un dato parametro non deve essere convertito in SqliteParameter 5 | public class Sql 6 | { 7 | private Sql(string value) 8 | { 9 | Value = value; 10 | } 11 | //Proprietà per conservare il valore originale 12 | public string Value { get; } 13 | 14 | //Conversione da/per il tipo string 15 | public static explicit operator Sql(string value) => new Sql(value); 16 | public override string ToString() 17 | { 18 | return this.Value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/MyCourse/Models/ViewModels/Courses/CourseDetailViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using MyCourse.Models.ViewModels.Lessons; 3 | 4 | namespace MyCourse.Models.ViewModels.Courses; 5 | 6 | public class CourseDetailViewModel 7 | { 8 | public int Id { get; set; } 9 | public string Title { get; set; } 10 | public string ImagePath { get; set; } 11 | public string Author { get; set; } 12 | public double Rating { get; set; } 13 | public Money FullPrice { get; set; } 14 | public Money CurrentPrice { get; set; } 15 | public string Description { get; set; } 16 | public List Lessons { get; set; } = new List(); 17 | public TimeSpan TotalCourseDuration 18 | { 19 | get => TimeSpan.FromSeconds(Lessons?.Sum(l => l.Duration.TotalSeconds) ?? 0); 20 | } 21 | 22 | public static CourseDetailViewModel FromDataRow(DataRow courseRow) 23 | { 24 | var courseDetailViewModel = new CourseDetailViewModel 25 | { 26 | Title = Convert.ToString(courseRow["Title"]), 27 | Description = Convert.ToString(courseRow["Description"]), 28 | ImagePath = Convert.ToString(courseRow["ImagePath"]), 29 | Author = Convert.ToString(courseRow["Author"]), 30 | Rating = Convert.ToDouble(courseRow["Rating"]), 31 | FullPrice = new Money( 32 | Enum.Parse(Convert.ToString(courseRow["FullPrice_Currency"])), 33 | Convert.ToDecimal(courseRow["FullPrice_Amount"]) 34 | ), 35 | CurrentPrice = new Money( 36 | Enum.Parse(Convert.ToString(courseRow["CurrentPrice_Currency"])), 37 | Convert.ToDecimal(courseRow["CurrentPrice_Amount"]) 38 | ), 39 | Id = Convert.ToInt32(courseRow["Id"]), 40 | Lessons = new List() 41 | }; 42 | return courseDetailViewModel; 43 | } 44 | 45 | public static CourseDetailViewModel FromEntity(Course course) 46 | { 47 | return new CourseDetailViewModel 48 | { 49 | Id = course.Id, 50 | Title = course.Title, 51 | Description = course.Description, 52 | Author = course.Author, 53 | ImagePath = course.ImagePath, 54 | Rating = course.Rating, 55 | CurrentPrice = course.CurrentPrice, 56 | FullPrice = course.FullPrice, 57 | Lessons = course.Lessons 58 | .OrderBy(lesson => lesson.Order) 59 | .ThenBy(lesson => lesson.Id) 60 | .Select(lesson => LessonViewModel.FromEntity(lesson)) 61 | .ToList() 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/MyCourse/Models/ViewModels/Courses/CourseListViewModel.cs: -------------------------------------------------------------------------------- 1 | using MyCourse.Models.InputModels.Courses; 2 | 3 | namespace MyCourse.Models.ViewModels.Courses; 4 | 5 | public class CourseListViewModel : IPaginationInfo 6 | { 7 | public ListViewModel Courses { get; set; } 8 | public CourseListInputModel Input { get; set; } 9 | 10 | 11 | #region Implementazione IPaginationInfo 12 | int IPaginationInfo.CurrentPage => Input.Page; 13 | 14 | int IPaginationInfo.TotalResults => Courses.TotalCount; 15 | 16 | int IPaginationInfo.ResultsPerPage => Input.Limit; 17 | 18 | string IPaginationInfo.Search => Input.Search; 19 | 20 | string IPaginationInfo.OrderBy => Input.OrderBy; 21 | 22 | bool IPaginationInfo.Ascending => Input.Ascending; 23 | #endregion 24 | } 25 | -------------------------------------------------------------------------------- /src/MyCourse/Models/ViewModels/Courses/CourseViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace MyCourse.Models.ViewModels.Courses; 4 | 5 | public class CourseViewModel 6 | { 7 | public int Id { get; set; } 8 | public string Title { get; set; } 9 | public string ImagePath { get; set; } 10 | public string Author { get; set; } 11 | public double Rating { get; set; } 12 | public Money FullPrice { get; set; } 13 | public Money CurrentPrice { get; set; } 14 | 15 | public static CourseViewModel FromDataRow(DataRow courseRow) 16 | { 17 | var courseViewModel = new CourseViewModel 18 | { 19 | Title = Convert.ToString(courseRow["Title"]), 20 | ImagePath = Convert.ToString(courseRow["ImagePath"]), 21 | Author = Convert.ToString(courseRow["Author"]), 22 | Rating = Convert.ToDouble(courseRow["Rating"]), 23 | FullPrice = new Money( 24 | Enum.Parse(Convert.ToString(courseRow["FullPrice_Currency"])), 25 | Convert.ToDecimal(courseRow["FullPrice_Amount"]) 26 | ), 27 | CurrentPrice = new Money( 28 | Enum.Parse(Convert.ToString(courseRow["CurrentPrice_Currency"])), 29 | Convert.ToDecimal(courseRow["CurrentPrice_Amount"]) 30 | ), 31 | Id = Convert.ToInt32(courseRow["Id"]) 32 | }; 33 | return courseViewModel; 34 | } 35 | 36 | public static CourseViewModel FromEntity(Course course) 37 | { 38 | return new CourseViewModel 39 | { 40 | Id = course.Id, 41 | Title = course.Title, 42 | ImagePath = course.ImagePath, 43 | Author = course.Author, 44 | Rating = course.Rating, 45 | CurrentPrice = course.CurrentPrice, 46 | FullPrice = course.FullPrice 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/MyCourse/Models/ViewModels/Home/HomeViewModel.cs: -------------------------------------------------------------------------------- 1 | using MyCourse.Models.ViewModels.Courses; 2 | 3 | namespace MyCourse.Models.ViewModels.Home; 4 | 5 | public class HomeViewModel 6 | { 7 | public List MostRecentCourses { get; set; } 8 | public List BestRatingCourses { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/MyCourse/Models/ViewModels/IPaginationInfo.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.ViewModels; 2 | 3 | public interface IPaginationInfo 4 | { 5 | int CurrentPage { get; } 6 | int TotalResults { get; } 7 | int ResultsPerPage { get; } 8 | 9 | string Search { get; } 10 | string OrderBy { get; } 11 | bool Ascending { get; } 12 | } 13 | -------------------------------------------------------------------------------- /src/MyCourse/Models/ViewModels/Lessons/LessonDetailViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace MyCourse.Models.ViewModels.Lessons; 4 | 5 | public class LessonDetailViewModel 6 | { 7 | public int Id { get; set; } 8 | public int CourseId { get; set; } 9 | public string Title { get; set; } 10 | public TimeSpan Duration { get; set; } 11 | public string Description { get; set; } 12 | 13 | public static LessonDetailViewModel FromDataRow(DataRow dataRow) 14 | { 15 | LessonDetailViewModel lessonViewModel = new() 16 | { 17 | Id = Convert.ToInt32(dataRow["Id"]), 18 | CourseId = Convert.ToInt32(dataRow["CourseId"]), 19 | Title = Convert.ToString(dataRow["Title"]), 20 | Duration = TimeSpan.Parse(Convert.ToString(dataRow["Duration"])), 21 | Description = Convert.ToString(dataRow["Description"]) 22 | }; 23 | return lessonViewModel; 24 | } 25 | 26 | public static LessonDetailViewModel FromEntity(Lesson lesson) 27 | { 28 | return new LessonDetailViewModel 29 | { 30 | Id = lesson.Id, 31 | CourseId = lesson.CourseId, 32 | Title = lesson.Title, 33 | Duration = lesson.Duration, 34 | Description = lesson.Description 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/MyCourse/Models/ViewModels/Lessons/LessonViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace MyCourse.Models.ViewModels.Lessons; 4 | 5 | public class LessonViewModel 6 | { 7 | public int Id { get; set; } 8 | public string Title { get; set; } 9 | public TimeSpan Duration { get; set; } 10 | 11 | public static LessonViewModel FromDataRow(DataRow dataRow) 12 | { 13 | LessonViewModel lessonViewModel = new() 14 | { 15 | Id = Convert.ToInt32(dataRow["Id"]), 16 | Title = Convert.ToString(dataRow["Title"]), 17 | Duration = TimeSpan.Parse(Convert.ToString(dataRow["Duration"])), 18 | }; 19 | return lessonViewModel; 20 | } 21 | 22 | public static LessonViewModel FromEntity(Lesson lesson) 23 | { 24 | return new LessonViewModel 25 | { 26 | Id = lesson.Id, 27 | Title = lesson.Title, 28 | Duration = lesson.Duration 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/MyCourse/Models/ViewModels/ListViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse.Models.ViewModels; 2 | 3 | public class ListViewModel 4 | { 5 | public List Results { get; set; } 6 | public int TotalCount { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/MyCourse/MyCourse.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | MyCourse 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | all 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/MyCourse/Pages/Admin/Users.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model MyCourse.Pages.Admin.UsersModel 3 |

@ViewData["Title"]

4 |
5 |
6 |
7 | 8 | 9 |
10 |
11 | 12 | 14 |
15 |
16 | 17 |
18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 | 37 | @foreach (ApplicationUser user in Model.Users) 38 | { 39 |
40 |
@user.FullName
41 |
@user.Email
42 |
43 |
44 | } 45 | @if (Model.Users.Count == 0) 46 | { 47 |

Questo ruolo non è ancora stato assegnato ad alcun utente

48 | } 49 | 50 | @section Scripts 51 | { 52 | 53 | } -------------------------------------------------------------------------------- /src/MyCourse/Pages/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model MyCourse.Pages.ContactModel 3 |

Fai una domanda a @Model.Course.Author, docente di "@Model.Course.Title"

4 |
5 |
6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | @section Scripts 16 | { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/MyCourse/Pages/Contact.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using AspNetCore.ReCaptcha; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | using MyCourse.Models.ViewModels.Courses; 6 | 7 | namespace MyCourse.Pages; 8 | 9 | [ValidateReCaptcha] 10 | public class ContactModel : PageModel 11 | { 12 | public CourseDetailViewModel Course { get; private set; } 13 | 14 | [Required(ErrorMessage = "Il testo della domanda è obbligatorio")] 15 | [Display(Name = "La tua domanda")] 16 | [BindProperty] 17 | public string Question { get; set; } 18 | 19 | public async Task OnGetAsync(int id, [FromServices] ICourseService courseService) 20 | { 21 | try 22 | { 23 | Course = await courseService.GetCourseAsync(id); 24 | ViewData["Title"] = $"Invia una domanda"; 25 | return Page(); 26 | } 27 | catch 28 | { 29 | return RedirectToAction("Index", "Courses"); 30 | } 31 | } 32 | 33 | public async Task OnPostAsync(int id, [FromServices] ICourseService courseService) 34 | { 35 | if (ModelState.IsValid) 36 | { 37 | await courseService.SendQuestionToCourseAuthorAsync(id, Question); 38 | TempData["ConfirmationMessage"] = "La tua domanda è stata inviata"; 39 | return RedirectToAction("Detail", "Courses", new { id = id }); 40 | } 41 | else 42 | { 43 | return await OnGetAsync(id, courseService); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/MyCourse/Pages/Privacy.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 |

Privacy

3 |

Non puoi accedere ai servizi usando tecniche automatizzate come scraping, spider, ecc...

4 |

Lorem ipsum dolor sit amet consectetur adipisicing elit. Quae temporibus ipsam saepe fugiat, minus vero adipisci est quam, perferendis animi eos voluptate doloribus quod, delectus quos nulla ea ratione a!

-------------------------------------------------------------------------------- /src/MyCourse/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 2 | @addTagHelper *, MyCourse 3 | @addTagHelper *, AspNetCore.ReCaptcha 4 | @using Microsoft.AspNetCore.Identity 5 | @using System.Collections.Generic 6 | @using MyCourse.Models.ViewModels 7 | @using MyCourse.Models.ViewModels.Courses 8 | @using MyCourse.Models.ViewModels.Lessons 9 | @using MyCourse.Models.ViewModels.Home 10 | @using MyCourse.Models.Enums 11 | @using MyCourse.Models.InputModels.Courses 12 | @using MyCourse.Models.InputModels.Lessons 13 | @using MyCourse.Models.Entities 14 | @using Microsoft.AspNetCore.Authorization -------------------------------------------------------------------------------- /src/MyCourse/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } -------------------------------------------------------------------------------- /src/MyCourse/Program.cs: -------------------------------------------------------------------------------- 1 | namespace MyCourse; 2 | 3 | public class Program 4 | { 5 | public static void Main(string[] args) 6 | { 7 | // Vari esempi per usare il nuovo builder: https://docs.microsoft.com/en-us/aspnet/core/migration/50-to-60-samples 8 | WebApplicationBuilder builder = WebApplication.CreateBuilder(args); 9 | 10 | Startup startup = new(builder.Configuration); 11 | 12 | // Aggiungere i servizi per la dependency injection (metodo ConfigureServices) 13 | startup.ConfigureServices(builder.Services); 14 | 15 | WebApplication app = builder.Build(); 16 | 17 | // Usiamo i middleware (metodo Configure) 18 | startup.Configure(app); 19 | 20 | app.Run(); 21 | } 22 | } -------------------------------------------------------------------------------- /src/MyCourse/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:17987", 7 | "sslPort": 44394 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development", 16 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" 17 | } 18 | }, 19 | "MyCourse": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development", 25 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/MyCourse/Views/Courses/Create.cshtml: -------------------------------------------------------------------------------- 1 | @model CourseCreateInputModel 2 |

@ViewData["Title"]

3 |
4 |
5 |
6 | 10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 | @section Scripts 24 | { 25 | 26 | } -------------------------------------------------------------------------------- /src/MyCourse/Views/Courses/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model CourseListViewModel 2 | @inject IAuthorizationService authService 3 | @{ 4 | AuthorizationPolicy policy = new AuthorizationPolicyBuilder().RequireRole(nameof(Role.Teacher)).Build(); 5 | AuthorizationResult authorizationResult = await authService.AuthorizeAsync(User, policy); 6 | bool canCreate = authorizationResult.Succeeded; 7 | } 8 |
9 |
10 |

@ViewData["Title"] 11 | @if (canCreate) 12 | { 13 | Crea nuovo 14 | } 15 |

16 |
17 |
18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 |
Titolo
32 |
Valutazione
33 |
Prezzo
34 |
35 |
36 | 37 | @foreach(CourseViewModel course in Model.Courses.Results) 38 | { 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/MyCourse/Views/Courses/Vote.cshtml: -------------------------------------------------------------------------------- 1 | @model CourseVoteInputModel 2 | @{ 3 | Layout = "_LayoutMinimal"; 4 | } 5 |
6 | @for (int i = 1; i <= 5; i++) 7 | {} 10 |
11 | @section Scripts 12 | { 13 | 26 | } -------------------------------------------------------------------------------- /src/MyCourse/Views/Error/CourseNotFound.cshtml: -------------------------------------------------------------------------------- 1 |

@ViewData["Title"]

2 |

Il corso che hai richiesto non esiste.

3 |

Torna al catalogo corsi

-------------------------------------------------------------------------------- /src/MyCourse/Views/Error/Index.cshtml: -------------------------------------------------------------------------------- 1 |

@ViewData["Title"]

2 |

Ci scusiamo, si è verificato un errore.

3 |

Non occorre segnalarlo, ti preghiamo di riprovare più tardi.

-------------------------------------------------------------------------------- /src/MyCourse/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model HomeViewModel 2 |
3 |

@ViewData["Title"]

4 |

Impara in maniera facile e divertente con i nostri corsi online!
5 | Scegli da un vasto catalogo sempre a tua disposizione.

6 |

Sfoglia il catalogo dei corsi

7 |
8 | 9 |
10 |

Aggiunti di recente

11 | @foreach (CourseViewModel course in Model.MostRecentCourses) 12 | { 13 | 14 | } 15 |
16 | 17 | 18 |
19 |

I migliori di sempre

20 | @foreach (CourseViewModel course in Model.BestRatingCourses) 21 | { 22 | 23 | } 24 |
-------------------------------------------------------------------------------- /src/MyCourse/Views/Lessons/Create.cshtml: -------------------------------------------------------------------------------- 1 | @model LessonCreateInputModel 2 |

@ViewData["Title"]

3 |
4 | 5 |
6 |
7 | 11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 | @section Scripts 25 | { 26 | 27 | } -------------------------------------------------------------------------------- /src/MyCourse/Views/Lessons/Detail.cshtml: -------------------------------------------------------------------------------- 1 | @model LessonDetailViewModel 2 |
3 |
4 |
5 |

@ViewData["Title"]

6 |

Durata stimata: @Model.Duration

7 |
8 |
9 | @Model.Description 10 |
11 |
12 | Torna al corso 13 |
14 |
15 |
-------------------------------------------------------------------------------- /src/MyCourse/Views/Shared/Components/PaginationBar/Default.cshtml: -------------------------------------------------------------------------------- 1 | @model IPaginationInfo 2 | @{ 3 | int totalPages = (int) Math.Ceiling(Model.TotalResults / (decimal) Model.ResultsPerPage); 4 | int currentPage = Model.CurrentPage; 5 | } 6 | -------------------------------------------------------------------------------- /src/MyCourse/Views/Shared/Courses/_CourseLine.cshtml: -------------------------------------------------------------------------------- 1 | @model CourseViewModel 2 |
3 |
4 |
5 | @Model.Title 6 |
7 |
8 |

@Model.Title

9 | di @Model.Author 10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 | Dettaglio 19 |
20 |
-------------------------------------------------------------------------------- /src/MyCourse/Views/Shared/_LayoutMinimal.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | @ViewData["Title"] | MyCourse 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | @RenderBody() 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | @RenderSection("Scripts", required: false) 32 | 33 | -------------------------------------------------------------------------------- /src/MyCourse/Views/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @inject SignInManager signInManager 2 | @inject UserManager userManager 3 | 4 | @if (signInManager.IsSignedIn(User)) 5 | { 6 | // Volendo posso recuperare tutti i dati dell'utente grazie allo userManager 7 | // Ma devo essere consapevole che questo mi costerà una query al database 8 | // ApplicationUser applicationUser = await userManager.GetUserAsync(User); 9 | 19 | } 20 | else 21 | { 22 | 30 | } -------------------------------------------------------------------------------- /src/MyCourse/Views/Shared/_Summernote.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/MyCourse/Views/Shared/_Validation.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/MyCourse/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 2 | @addTagHelper *, MyCourse 3 | @using Microsoft.AspNetCore.Identity 4 | @using System.Collections.Generic 5 | @using MyCourse.Models.ViewModels 6 | @using MyCourse.Models.ViewModels.Courses 7 | @using MyCourse.Models.ViewModels.Lessons 8 | @using MyCourse.Models.ViewModels.Home 9 | @using MyCourse.Models.Enums 10 | @using MyCourse.Models.InputModels.Courses 11 | @using MyCourse.Models.InputModels.Lessons 12 | @using MyCourse.Models.Entities 13 | @using Microsoft.AspNetCore.Authorization -------------------------------------------------------------------------------- /src/MyCourse/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } -------------------------------------------------------------------------------- /src/MyCourse/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information" 5 | } 6 | }, 7 | "Courses": { 8 | "PerPage": 10 9 | } 10 | } -------------------------------------------------------------------------------- /src/MyCourse/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Kestrel": { 3 | "Limits": { 4 | "MaxRequestBodySize": 5242880 5 | } 6 | }, 7 | "Logging": { 8 | "LogLevel": { 9 | "Default": "Warning" 10 | } 11 | }, 12 | "ConnectionStrings": { 13 | "Default": "Data Source=Data/MyCourse.db" 14 | }, 15 | "Courses": { 16 | "PerPage": 10, 17 | "InHome": 3, 18 | "Order": { 19 | "By": "Rating", 20 | "Ascending": false, 21 | "Allow": ["Id", "Title", "Rating", "CurrentPrice"] 22 | } 23 | }, 24 | "MemoryCache": { 25 | }, 26 | "ResponseCache": { 27 | "Home": { 28 | "Duration": 60, 29 | "Location": "Client", 30 | "VaryByQueryKeys": ["page"] 31 | } 32 | }, 33 | "Smtp": { 34 | "Host": "smtp.example.org", 35 | "Port": 25, 36 | "Security": "StartTls", 37 | "Username": "Username del server SMTP", 38 | "Password": "Password del server SMTP", 39 | "Sender": "MyCourse " 40 | }, 41 | "ReCaptcha": { 42 | "SiteKey": "SiteKey fornita da Google su https://www.google.com/recaptcha/admin/create", 43 | "SecretKey": "SecretKey fornita da Google su https://www.google.com/recaptcha/admin/create", 44 | "Version": "v2" 45 | }, 46 | "Users": { 47 | "AssignAdministratorRoleOnRegistration": "admin@example.com", 48 | "NotificationEmailRecipient": "boss@example.com" 49 | }, 50 | "Paypal": { 51 | "ClientId": "ClientId fornito da Paypal su https://developer.paypal.com", 52 | "ClientSecret": "ClientSecret fornito da Paypal su https://developer.paypal.com", 53 | "IsSandbox": true, 54 | "BrandName": "MyCourse" 55 | }, 56 | "Stripe": { 57 | "PrivateKey": "Chiave privata fornita da Stripe su https://dashboard.stripe.com/" 58 | } 59 | } -------------------------------------------------------------------------------- /src/MyCourse/libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "defaultProvider": "unpkg", 4 | "libraries": [ 5 | { 6 | "library": "bootstrap@4.4.1", 7 | "destination": "wwwroot\\lib\\bootstrap", 8 | "files": [ 9 | "dist/js/bootstrap.min.js", 10 | "dist/css/bootstrap.min.css" 11 | ] 12 | }, 13 | { 14 | "library": "@fortawesome/fontawesome-free@5.12.1", 15 | "destination": "wwwroot\\lib\\font-awesome", 16 | "files": [ 17 | "css/all.min.css", 18 | "webfonts/fa-brands-400.eot", 19 | "webfonts/fa-brands-400.svg", 20 | "webfonts/fa-brands-400.ttf", 21 | "webfonts/fa-brands-400.woff", 22 | "webfonts/fa-brands-400.woff2", 23 | "webfonts/fa-regular-400.eot", 24 | "webfonts/fa-regular-400.svg", 25 | "webfonts/fa-regular-400.ttf", 26 | "webfonts/fa-regular-400.woff", 27 | "webfonts/fa-regular-400.woff2", 28 | "webfonts/fa-solid-900.eot", 29 | "webfonts/fa-solid-900.svg", 30 | "webfonts/fa-solid-900.ttf", 31 | "webfonts/fa-solid-900.woff", 32 | "webfonts/fa-solid-900.woff2" 33 | ] 34 | }, 35 | { 36 | "library": "popper.js@1.16.0", 37 | "destination": "wwwroot\\lib\\popper", 38 | "files": [ 39 | "dist/umd/popper.min.js" 40 | ] 41 | }, 42 | { 43 | "library": "jquery-validation@1.19.1", 44 | "destination": "wwwroot/lib/jquery-validation", 45 | "files": [ 46 | "dist/jquery.validate.min.js", 47 | "dist/localization/messages_it.min.js", 48 | "dist/additional-methods.min.js" 49 | ] 50 | }, 51 | { 52 | "library": "jquery-validation-unobtrusive@3.2.11", 53 | "destination": "wwwroot/lib/jquery-validation-unobtrusive", 54 | "files": [ 55 | "dist/jquery.validate.unobtrusive.min.js" 56 | ] 57 | }, 58 | { 59 | "library": "jquery@3.4.1", 60 | "destination": "wwwroot/lib/jquery", 61 | "files": [ 62 | "dist/jquery.min.js", 63 | "dist/jquery.slim.min.js" 64 | ] 65 | }, 66 | { 67 | "library": "summernote@0.8.15", 68 | "destination": "wwwroot/lib/summernote", 69 | "files": [ 70 | "dist/font/summernote.eot", 71 | "dist/font/summernote.ttf", 72 | "dist/font/summernote.woff", 73 | "dist/font/summernote.woff2", 74 | "dist/summernote-bs4.min.css", 75 | "dist/summernote-bs4.min.js", 76 | "dist/lang/summernote-it-IT.js" 77 | ] 78 | }, 79 | { 80 | "library": "qrcodejs@1.0.0", 81 | "destination": "wwwroot\\lib\\qrcodejs", 82 | "files": [ 83 | "qrcode.min.js" 84 | ] 85 | } 86 | ] 87 | } -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/1.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/10.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/11.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/12.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/13.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/14.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/15.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/16.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/17.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/18.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/19.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/2.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/20.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/21.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/22.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/23.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/24.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/25.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/26.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/26.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/27.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/27.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/28.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/29.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/29.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/3.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/30.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/31.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/31.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/32.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/4.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/5.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/6.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/7.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/8.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/9.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/Courses/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/Courses/default.png -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/images/hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/images/hero.jpg -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/font-awesome/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/jquery-validation/dist/localization/messages_it.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery Validation Plugin - v1.19.1 - 6/15/2019 2 | * https://jqueryvalidation.org/ 3 | * Copyright (c) 2019 Jörn Zaefferer; Licensed MIT */ 4 | !function(a){"function"==typeof define&&define.amd?define(["jquery","../jquery.validate.min"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){return a.extend(a.validator.messages,{required:"Campo obbligatorio",remote:"Controlla questo campo",email:"Inserisci un indirizzo email valido",url:"Inserisci un indirizzo web valido",date:"Inserisci una data valida",dateISO:"Inserisci una data valida (ISO)",number:"Inserisci un numero valido",digits:"Inserisci solo numeri",creditcard:"Inserisci un numero di carta di credito valido",equalTo:"Il valore non corrisponde",extension:"Inserisci un valore con un'estensione valida",maxlength:a.validator.format("Non inserire più di {0} caratteri"),minlength:a.validator.format("Inserisci almeno {0} caratteri"),rangelength:a.validator.format("Inserisci un valore compreso tra {0} e {1} caratteri"),range:a.validator.format("Inserisci un valore compreso tra {0} e {1}"),max:a.validator.format("Inserisci un valore minore o uguale a {0}"),min:a.validator.format("Inserisci un valore maggiore o uguale a {0}"),nifES:"Inserisci un NIF valido",nieES:"Inserisci un NIE valido",cifES:"Inserisci un CIF valido",currency:"Inserisci una valuta valida"}),a}); -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/summernote/dist/font/summernote.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/summernote/dist/font/summernote.eot -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/summernote/dist/font/summernote.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/summernote/dist/font/summernote.ttf -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/summernote/dist/font/summernote.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/summernote/dist/font/summernote.woff -------------------------------------------------------------------------------- /src/MyCourse/wwwroot/lib/summernote/dist/font/summernote.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyDotNet/MyCourse/adbcf92a7bc9f189322c47c78d4d4e5b28b9d28d/src/MyCourse/wwwroot/lib/summernote/dist/font/summernote.woff2 --------------------------------------------------------------------------------