From 1f92f82b2c379cb334aaa132a1ca1b0d257b63dd Mon Sep 17 00:00:00 2001 From: snow flurry Date: Tue, 17 Oct 2023 21:55:10 -0700 Subject: [PATCH] Initial commit --- .gitignore | 454 ++++++++++++++++++ PhoneToolMX.Models/AlwaysIncludeAttribute.cs | 7 + .../CheckExtensionsAttribute.cs | 25 + PhoneToolMX.Models/Data/PTMXContext.cs | 126 +++++ PhoneToolMX.Models/HeaderAttribute.cs | 16 + .../20231015015926_InitialCreate.Designer.cs | 358 ++++++++++++++ .../20231015015926_InitialCreate.cs | 288 +++++++++++ .../Migrations/PTMXContextModelSnapshot.cs | 356 ++++++++++++++ PhoneToolMX.Models/Models/CustomData.cs | 21 + PhoneToolMX.Models/Models/ErrorViewModel.cs | 8 + PhoneToolMX.Models/Models/Extension.cs | 25 + PhoneToolMX.Models/Models/IModel.cs | 19 + PhoneToolMX.Models/Models/IOwnedModel.cs | 10 + PhoneToolMX.Models/Models/OwnedBase.cs | 17 + PhoneToolMX.Models/Models/Phone.cs | 28 ++ PhoneToolMX.Models/Models/PhoneModel.cs | 30 ++ PhoneToolMX.Models/Models/Role.cs | 17 + PhoneToolMX.Models/Models/User.cs | 11 + PhoneToolMX.Models/PhoneToolMX.Models.csproj | 19 + PhoneToolMX.Models/ViewModels/ExtensionVM.cs | 57 +++ PhoneToolMX.Models/ViewModels/IViewModel.cs | 15 + PhoneToolMX.Models/ViewModels/PhoneVM.cs | 64 +++ PhoneToolMX/.vscode/launch.json | 36 ++ PhoneToolMX/.vscode/tasks.json | 41 ++ PhoneToolMX/Controllers/BaseController.cs | 225 +++++++++ .../Controllers/ExtensionController.cs | 23 + PhoneToolMX/Controllers/HomeController.cs | 27 ++ PhoneToolMX/Controllers/ModelController.cs | 13 + PhoneToolMX/Controllers/PhoneController.cs | 26 + PhoneToolMX/Folder.DotSettings | 2 + PhoneToolMX/Helpers/FormMessage.cs | 18 + PhoneToolMX/Helpers/HtmlTable.cs | 91 ++++ PhoneToolMX/PhoneToolMX.csproj | 35 ++ PhoneToolMX/Program.cs | 131 +++++ PhoneToolMX/Properties/launchSettings.json | 28 ++ .../Views/Extension/DeleteConfirm.cshtml | 10 + PhoneToolMX/Views/Extension/Index.cshtml | 6 + PhoneToolMX/Views/Extension/_Form.cshtml | 33 ++ PhoneToolMX/Views/Home/Index.cshtml | 24 + PhoneToolMX/Views/Home/Privacy.cshtml | 6 + PhoneToolMX/Views/Model/Index.cshtml | 7 + PhoneToolMX/Views/Phone/DeleteConfirm.cshtml | 13 + PhoneToolMX/Views/Phone/Index.cshtml | 6 + PhoneToolMX/Views/Phone/_Form.cshtml | 34 ++ PhoneToolMX/Views/Shared/Error.cshtml | 14 + PhoneToolMX/Views/Shared/_Layout.cshtml | 89 ++++ PhoneToolMX/Views/Shared/_Layout.cshtml.css | 49 ++ PhoneToolMX/Views/_ViewImports.cshtml | 3 + PhoneToolMX/Views/_ViewStart.cshtml | 3 + PhoneToolMX/appsettings.Development.json | 18 + PhoneToolMX/appsettings.json | 9 + PhoneToolMX/wwwroot/favicon.ico | Bin 0 -> 5430 bytes PhoneToolMX/wwwroot/images/banner.gif | Bin 0 -> 19220 bytes PhoneToolMX/wwwroot/images/del_x16.gif | Bin 0 -> 164 bytes PhoneToolMX/wwwroot/images/edit_x16.gif | Bin 0 -> 220 bytes PhoneToolMX/wwwroot/main.css | 182 +++++++ global.json | 7 + ptmx-asp.sln | 28 ++ ptmx-asp.sln.DotSettings | 3 + 59 files changed, 3211 insertions(+) create mode 100644 .gitignore create mode 100644 PhoneToolMX.Models/AlwaysIncludeAttribute.cs create mode 100644 PhoneToolMX.Models/CheckExtensionsAttribute.cs create mode 100644 PhoneToolMX.Models/Data/PTMXContext.cs create mode 100644 PhoneToolMX.Models/HeaderAttribute.cs create mode 100644 PhoneToolMX.Models/Migrations/20231015015926_InitialCreate.Designer.cs create mode 100644 PhoneToolMX.Models/Migrations/20231015015926_InitialCreate.cs create mode 100644 PhoneToolMX.Models/Migrations/PTMXContextModelSnapshot.cs create mode 100644 PhoneToolMX.Models/Models/CustomData.cs create mode 100644 PhoneToolMX.Models/Models/ErrorViewModel.cs create mode 100644 PhoneToolMX.Models/Models/Extension.cs create mode 100644 PhoneToolMX.Models/Models/IModel.cs create mode 100644 PhoneToolMX.Models/Models/IOwnedModel.cs create mode 100644 PhoneToolMX.Models/Models/OwnedBase.cs create mode 100644 PhoneToolMX.Models/Models/Phone.cs create mode 100644 PhoneToolMX.Models/Models/PhoneModel.cs create mode 100644 PhoneToolMX.Models/Models/Role.cs create mode 100644 PhoneToolMX.Models/Models/User.cs create mode 100644 PhoneToolMX.Models/PhoneToolMX.Models.csproj create mode 100644 PhoneToolMX.Models/ViewModels/ExtensionVM.cs create mode 100644 PhoneToolMX.Models/ViewModels/IViewModel.cs create mode 100644 PhoneToolMX.Models/ViewModels/PhoneVM.cs create mode 100644 PhoneToolMX/.vscode/launch.json create mode 100644 PhoneToolMX/.vscode/tasks.json create mode 100644 PhoneToolMX/Controllers/BaseController.cs create mode 100644 PhoneToolMX/Controllers/ExtensionController.cs create mode 100644 PhoneToolMX/Controllers/HomeController.cs create mode 100644 PhoneToolMX/Controllers/ModelController.cs create mode 100644 PhoneToolMX/Controllers/PhoneController.cs create mode 100644 PhoneToolMX/Folder.DotSettings create mode 100644 PhoneToolMX/Helpers/FormMessage.cs create mode 100644 PhoneToolMX/Helpers/HtmlTable.cs create mode 100644 PhoneToolMX/PhoneToolMX.csproj create mode 100644 PhoneToolMX/Program.cs create mode 100644 PhoneToolMX/Properties/launchSettings.json create mode 100644 PhoneToolMX/Views/Extension/DeleteConfirm.cshtml create mode 100644 PhoneToolMX/Views/Extension/Index.cshtml create mode 100644 PhoneToolMX/Views/Extension/_Form.cshtml create mode 100644 PhoneToolMX/Views/Home/Index.cshtml create mode 100644 PhoneToolMX/Views/Home/Privacy.cshtml create mode 100644 PhoneToolMX/Views/Model/Index.cshtml create mode 100644 PhoneToolMX/Views/Phone/DeleteConfirm.cshtml create mode 100644 PhoneToolMX/Views/Phone/Index.cshtml create mode 100644 PhoneToolMX/Views/Phone/_Form.cshtml create mode 100644 PhoneToolMX/Views/Shared/Error.cshtml create mode 100644 PhoneToolMX/Views/Shared/_Layout.cshtml create mode 100644 PhoneToolMX/Views/Shared/_Layout.cshtml.css create mode 100644 PhoneToolMX/Views/_ViewImports.cshtml create mode 100644 PhoneToolMX/Views/_ViewStart.cshtml create mode 100644 PhoneToolMX/appsettings.Development.json create mode 100644 PhoneToolMX/appsettings.json create mode 100644 PhoneToolMX/wwwroot/favicon.ico create mode 100644 PhoneToolMX/wwwroot/images/banner.gif create mode 100644 PhoneToolMX/wwwroot/images/del_x16.gif create mode 100644 PhoneToolMX/wwwroot/images/edit_x16.gif create mode 100644 PhoneToolMX/wwwroot/main.css create mode 100644 global.json create mode 100644 ptmx-asp.sln create mode 100644 ptmx-asp.sln.DotSettings diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a72f3dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,454 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/PhoneToolMX.Models/AlwaysIncludeAttribute.cs b/PhoneToolMX.Models/AlwaysIncludeAttribute.cs new file mode 100644 index 0000000..57a3123 --- /dev/null +++ b/PhoneToolMX.Models/AlwaysIncludeAttribute.cs @@ -0,0 +1,7 @@ +namespace PhoneToolMX.Models +{ + [System.AttributeUsage(AttributeTargets.Property)] + public class AlwaysIncludeAttribute : System.Attribute + { + } +} diff --git a/PhoneToolMX.Models/CheckExtensionsAttribute.cs b/PhoneToolMX.Models/CheckExtensionsAttribute.cs new file mode 100644 index 0000000..49dd61a --- /dev/null +++ b/PhoneToolMX.Models/CheckExtensionsAttribute.cs @@ -0,0 +1,25 @@ +using PhoneToolMX.Models.ViewModels; +using System.ComponentModel.DataAnnotations; +using System.Data; + +namespace PhoneToolMX.Models +{ + public class CheckExtensionsAttribute : ValidationAttribute + { + private int? MaxExtensions; + + public override string FormatErrorMessage(string name) + { + return MaxExtensions != null + ? $"Up to {MaxExtensions} extensions can be assigned to this phone" + : "Too many extensions assigned to this phone"; + } + public override bool IsValid(object value) + { + if (value is not PhoneVM phone) throw new ConstraintException($"{GetType().Name} is only allowed for PhoneVM"); + if (phone.MaxExtensions <= 0 || phone.Extensions == null) return true; + MaxExtensions = phone.MaxExtensions; + return !(phone.Extensions.Count > phone.MaxExtensions); + } + } +} diff --git a/PhoneToolMX.Models/Data/PTMXContext.cs b/PhoneToolMX.Models/Data/PTMXContext.cs new file mode 100644 index 0000000..0db822b --- /dev/null +++ b/PhoneToolMX.Models/Data/PTMXContext.cs @@ -0,0 +1,126 @@ +// using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using System.Security.Claims; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using PhoneToolMX.Models; +using System.Net.NetworkInformation; + +// ReSharper disable CheckNamespace +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace PhoneToolMX.Data { + public class PTMXContext : DbContext + { + public DbSet Phones { get; set; } + public DbSet PhoneModels { get; set; } + public DbSet Extensions { get; set; } + public DbSet CustomData { get; set; } + public DbSet Users { get; set; } + + public PTMXContext(DbContextOptions options) : base(options) + { + } + + #region Public helpers + + /// + /// Adds an entity to the database, marking the given as its owner. + /// + /// The that owns this entity. + /// The entity to be created. + /// A model conforming to . + /// The entity entry for the created entity. + public async Task> AddOwnable(User owner, TEntity entity) where TEntity: OwnedBase + { + var set = Set(); + entity.Owners ??= new List(); + entity.Owners.Add(owner); + var entry = await AddAsync(entity); + return entry; + } + + /// + /// Gets all entities of a certain model owned by the . + /// + /// The object to be considered the owner. + /// A model conforming to + /// All entities of the model owned by the given user + public ICollection GetOwned(User owner) where TEntity : class, IOwnedModel + { + if (owner == null) return null; + var entity = Set().Where(x => x.Owners.Any(o => o.Id == owner.Id)); + // eager load all w/ AlwaysInclude + entity = typeof(TEntity).GetProperties() + .Where(p => p.GetCustomAttributes(typeof(AlwaysIncludeAttribute), true) + .Length != 0) + .Aggregate(entity, (current, prop) => current.Include(prop.Name)); + return entity.ToList(); + } + + /// + /// Gets a model entity by its Id + /// + /// Id of the model, or null if you're like that + /// A model confirming to that has a given DbSet + /// The model defined by the Id, or null if none exist. + /// If null is provided as the id, null will be returned. id is nullable solely for compatibility. + /// + public TEntity GetEntityById(int? id) where TEntity : class, IModel + { + return id == null ? null : Set().FirstOrDefault(o => o.Id == id); + } + + #endregion + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Core of PTMX: Phones and extensions + var ext = modelBuilder.Entity(); + ext.HasKey(x => x.Id); + ext.Property(x => x.Id).UseIdentityColumn(); + ext.Property(x => x.ExtId).HasComputedColumnSql("\"Id\" + 1000", stored: true); + + var phone = modelBuilder.Entity(); + phone + .HasMany(p => p.Extensions) + .WithMany(x => x.Phones); + phone.HasKey(p => p.Id); + phone.Property(p => p.Id).UseIdentityColumn(); + // Randomly generates a 24-char password + phone + .Property(p => p.Password) + .HasDefaultValueSql("encode(gen_random_bytes(18), 'base64')"); + + // Wallpapers, ringtones, etc + var cd = modelBuilder.Entity(); + cd.HasKey(c => c.Id); + cd.Property(c => c.Id).UseIdentityColumn(); + + // Phone models, for custom tests + var pm = modelBuilder.Entity(); + pm.HasKey(p => p.Id); + pm.Property(p => p.Id).UseIdentityColumn(); + pm.HasData(new PhoneModel + { + Id = 0, + ModelName = "Polycom VVX300/310", + MaxExtensions = 6, + PreVvxPolycom = false, + }); + + // Authz/RBAC + var userEnt = modelBuilder.Entity(); + userEnt + .HasMany(u => u.Phones) + .WithMany(p => p.Owners); + userEnt + .HasMany(u => u.Extensions) + .WithMany(x => x.Owners); + userEnt + .HasKey(u => u.Id); + + modelBuilder.Entity() + .HasKey(r => r.Id); + } + } +} \ No newline at end of file diff --git a/PhoneToolMX.Models/HeaderAttribute.cs b/PhoneToolMX.Models/HeaderAttribute.cs new file mode 100644 index 0000000..34a34ab --- /dev/null +++ b/PhoneToolMX.Models/HeaderAttribute.cs @@ -0,0 +1,16 @@ +namespace PhoneToolMX.Models +{ + [System.AttributeUsage(AttributeTargets.Property)] + public class HeaderAttribute : System.Attribute + { + public string Title { get; set; } + public bool Primary { get; set; } + public bool Small { get; set; } + public HeaderAttribute(string title) + { + Title = title; + } + + public HeaderAttribute() {} + } +} diff --git a/PhoneToolMX.Models/Migrations/20231015015926_InitialCreate.Designer.cs b/PhoneToolMX.Models/Migrations/20231015015926_InitialCreate.Designer.cs new file mode 100644 index 0000000..499cd23 --- /dev/null +++ b/PhoneToolMX.Models/Migrations/20231015015926_InitialCreate.Designer.cs @@ -0,0 +1,358 @@ +// +using System; +using System.Net.NetworkInformation; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PhoneToolMX.Data; + +#nullable disable + +namespace PhoneToolMX.Models.Migrations +{ + [DbContext(typeof(PTMXContext))] + [Migration("20231015015926_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.16") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ExtensionPhone", b => + { + b.Property("ExtensionsId") + .HasColumnType("integer"); + + b.Property("PhonesId") + .HasColumnType("integer"); + + b.HasKey("ExtensionsId", "PhonesId"); + + b.HasIndex("PhonesId"); + + b.ToTable("ExtensionPhone"); + }); + + modelBuilder.Entity("ExtensionUser", b => + { + b.Property("ExtensionsId") + .HasColumnType("integer"); + + b.Property("OwnersId") + .HasColumnType("text"); + + b.HasKey("ExtensionsId", "OwnersId"); + + b.HasIndex("OwnersId"); + + b.ToTable("ExtensionUser"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.CustomData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Data") + .HasColumnType("bytea"); + + b.Property("DataType") + .HasColumnType("integer"); + + b.Property("FriendlyName") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("PhoneId") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PhoneId"); + + b.ToTable("CustomData"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Extension", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtId") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("integer") + .HasComputedColumnSql("\"Id\" + 1000", true); + + b.Property("HoldMusicId") + .HasColumnType("integer"); + + b.Property("Listed") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("HoldMusicId"); + + b.ToTable("Extensions"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BackgroundId") + .HasColumnType("integer"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("text"); + + b.Property("MacAddress") + .IsRequired() + .HasColumnType("macaddr"); + + b.Property("ModelId") + .HasColumnType("integer"); + + b.Property("Password") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValueSql("encode(gen_random_bytes(18), 'base64')"); + + b.HasKey("Id"); + + b.HasIndex("BackgroundId"); + + b.HasIndex("ModelId"); + + b.ToTable("Phones"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.PhoneModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MaxExtensions") + .HasColumnType("bigint"); + + b.Property("ModelName") + .HasColumnType("text"); + + b.Property("PreVvxPolycom") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("PhoneModels"); + + b.HasData( + new + { + Id = 0, + MaxExtensions = 6L, + ModelName = "Polycom VVX300/310", + PreVvxPolycom = false + }); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NormalizedName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Role"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PhoneUser", b => + { + b.Property("OwnersId") + .HasColumnType("text"); + + b.Property("PhonesId") + .HasColumnType("integer"); + + b.HasKey("OwnersId", "PhonesId"); + + b.HasIndex("PhonesId"); + + b.ToTable("PhoneUser"); + }); + + modelBuilder.Entity("ExtensionPhone", b => + { + b.HasOne("PhoneToolMX.Models.Extension", null) + .WithMany() + .HasForeignKey("ExtensionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany() + .HasForeignKey("PhonesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExtensionUser", b => + { + b.HasOne("PhoneToolMX.Models.Extension", null) + .WithMany() + .HasForeignKey("ExtensionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.User", null) + .WithMany() + .HasForeignKey("OwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PhoneToolMX.Models.CustomData", b => + { + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany("Ringtones") + .HasForeignKey("PhoneId"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Extension", b => + { + b.HasOne("PhoneToolMX.Models.CustomData", "HoldMusic") + .WithMany() + .HasForeignKey("HoldMusicId"); + + b.Navigation("HoldMusic"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.HasOne("PhoneToolMX.Models.CustomData", "Background") + .WithMany() + .HasForeignKey("BackgroundId"); + + b.HasOne("PhoneToolMX.Models.PhoneModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Background"); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("PhoneUser", b => + { + b.HasOne("PhoneToolMX.Models.User", null) + .WithMany() + .HasForeignKey("OwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany() + .HasForeignKey("PhonesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.Navigation("Ringtones"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PhoneToolMX.Models/Migrations/20231015015926_InitialCreate.cs b/PhoneToolMX.Models/Migrations/20231015015926_InitialCreate.cs new file mode 100644 index 0000000..a7727eb --- /dev/null +++ b/PhoneToolMX.Models/Migrations/20231015015926_InitialCreate.cs @@ -0,0 +1,288 @@ +using System; +using System.Net.NetworkInformation; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace PhoneToolMX.Models.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PhoneModels", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ModelName = table.Column(type: "text", nullable: true), + MaxExtensions = table.Column(type: "bigint", nullable: false), + PreVvxPolycom = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PhoneModels", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Role", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: true), + NormalizedName = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Role", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + UserName = table.Column(type: "text", nullable: true), + NormalizedUserName = table.Column(type: "text", nullable: true), + Email = table.Column(type: "text", nullable: true), + NormalizedEmail = table.Column(type: "text", nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CustomData", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FriendlyName = table.Column(type: "character varying(16)", maxLength: 16, nullable: true), + DataType = table.Column(type: "integer", nullable: false), + Size = table.Column(type: "bigint", nullable: false), + Data = table.Column(type: "bytea", nullable: true), + PhoneId = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CustomData", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Extensions", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ExtId = table.Column(type: "integer", nullable: false, computedColumnSql: "\"Id\" + 1000", stored: true), + DirectoryName = table.Column(type: "text", nullable: false), + Listed = table.Column(type: "boolean", nullable: false), + HoldMusicId = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Extensions", x => x.Id); + table.ForeignKey( + name: "FK_Extensions_CustomData_HoldMusicId", + column: x => x.HoldMusicId, + principalTable: "CustomData", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Phones", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MacAddress = table.Column(type: "macaddr", nullable: false), + FriendlyName = table.Column(type: "text", nullable: false), + ModelId = table.Column(type: "integer", nullable: false), + BackgroundId = table.Column(type: "integer", nullable: true), + Password = table.Column(type: "text", nullable: true, defaultValueSql: "encode(gen_random_bytes(18), 'base64')") + }, + constraints: table => + { + table.PrimaryKey("PK_Phones", x => x.Id); + table.ForeignKey( + name: "FK_Phones_CustomData_BackgroundId", + column: x => x.BackgroundId, + principalTable: "CustomData", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Phones_PhoneModels_ModelId", + column: x => x.ModelId, + principalTable: "PhoneModels", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ExtensionUser", + columns: table => new + { + ExtensionsId = table.Column(type: "integer", nullable: false), + OwnersId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExtensionUser", x => new { x.ExtensionsId, x.OwnersId }); + table.ForeignKey( + name: "FK_ExtensionUser_Extensions_ExtensionsId", + column: x => x.ExtensionsId, + principalTable: "Extensions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ExtensionUser_Users_OwnersId", + column: x => x.OwnersId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ExtensionPhone", + columns: table => new + { + ExtensionsId = table.Column(type: "integer", nullable: false), + PhonesId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExtensionPhone", x => new { x.ExtensionsId, x.PhonesId }); + table.ForeignKey( + name: "FK_ExtensionPhone_Extensions_ExtensionsId", + column: x => x.ExtensionsId, + principalTable: "Extensions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ExtensionPhone_Phones_PhonesId", + column: x => x.PhonesId, + principalTable: "Phones", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PhoneUser", + columns: table => new + { + OwnersId = table.Column(type: "text", nullable: false), + PhonesId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PhoneUser", x => new { x.OwnersId, x.PhonesId }); + table.ForeignKey( + name: "FK_PhoneUser_Phones_PhonesId", + column: x => x.PhonesId, + principalTable: "Phones", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PhoneUser_Users_OwnersId", + column: x => x.OwnersId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "PhoneModels", + columns: new[] { "Id", "MaxExtensions", "ModelName", "PreVvxPolycom" }, + values: new object[] { 0, 6L, "Polycom VVX300/310", false }); + + migrationBuilder.CreateIndex( + name: "IX_CustomData_PhoneId", + table: "CustomData", + column: "PhoneId"); + + migrationBuilder.CreateIndex( + name: "IX_ExtensionPhone_PhonesId", + table: "ExtensionPhone", + column: "PhonesId"); + + migrationBuilder.CreateIndex( + name: "IX_Extensions_HoldMusicId", + table: "Extensions", + column: "HoldMusicId"); + + migrationBuilder.CreateIndex( + name: "IX_ExtensionUser_OwnersId", + table: "ExtensionUser", + column: "OwnersId"); + + migrationBuilder.CreateIndex( + name: "IX_Phones_BackgroundId", + table: "Phones", + column: "BackgroundId"); + + migrationBuilder.CreateIndex( + name: "IX_Phones_ModelId", + table: "Phones", + column: "ModelId"); + + migrationBuilder.CreateIndex( + name: "IX_PhoneUser_PhonesId", + table: "PhoneUser", + column: "PhonesId"); + + migrationBuilder.AddForeignKey( + name: "FK_CustomData_Phones_PhoneId", + table: "CustomData", + column: "PhoneId", + principalTable: "Phones", + principalColumn: "Id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_CustomData_Phones_PhoneId", + table: "CustomData"); + + migrationBuilder.DropTable( + name: "ExtensionPhone"); + + migrationBuilder.DropTable( + name: "ExtensionUser"); + + migrationBuilder.DropTable( + name: "PhoneUser"); + + migrationBuilder.DropTable( + name: "Role"); + + migrationBuilder.DropTable( + name: "Extensions"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Phones"); + + migrationBuilder.DropTable( + name: "CustomData"); + + migrationBuilder.DropTable( + name: "PhoneModels"); + } + } +} diff --git a/PhoneToolMX.Models/Migrations/PTMXContextModelSnapshot.cs b/PhoneToolMX.Models/Migrations/PTMXContextModelSnapshot.cs new file mode 100644 index 0000000..5bcb161 --- /dev/null +++ b/PhoneToolMX.Models/Migrations/PTMXContextModelSnapshot.cs @@ -0,0 +1,356 @@ +// +using System; +using System.Net.NetworkInformation; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PhoneToolMX.Data; + +#nullable disable + +namespace PhoneToolMX.Models.Migrations +{ + [DbContext(typeof(PTMXContext))] + partial class PTMXContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.16") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ExtensionPhone", b => + { + b.Property("ExtensionsId") + .HasColumnType("integer"); + + b.Property("PhonesId") + .HasColumnType("integer"); + + b.HasKey("ExtensionsId", "PhonesId"); + + b.HasIndex("PhonesId"); + + b.ToTable("ExtensionPhone", (string)null); + }); + + modelBuilder.Entity("ExtensionUser", b => + { + b.Property("ExtensionsId") + .HasColumnType("integer"); + + b.Property("OwnersId") + .HasColumnType("text"); + + b.HasKey("ExtensionsId", "OwnersId"); + + b.HasIndex("OwnersId"); + + b.ToTable("ExtensionUser", (string)null); + }); + + modelBuilder.Entity("PhoneToolMX.Models.CustomData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Data") + .HasColumnType("bytea"); + + b.Property("DataType") + .HasColumnType("integer"); + + b.Property("FriendlyName") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("PhoneId") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PhoneId"); + + b.ToTable("CustomData", (string)null); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Extension", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtId") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("integer") + .HasComputedColumnSql("\"Id\" + 1000", true); + + b.Property("HoldMusicId") + .HasColumnType("integer"); + + b.Property("Listed") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("HoldMusicId"); + + b.ToTable("Extensions", (string)null); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BackgroundId") + .HasColumnType("integer"); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("text"); + + b.Property("MacAddress") + .IsRequired() + .HasColumnType("macaddr"); + + b.Property("ModelId") + .HasColumnType("integer"); + + b.Property("Password") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValueSql("encode(gen_random_bytes(18), 'base64')"); + + b.HasKey("Id"); + + b.HasIndex("BackgroundId"); + + b.HasIndex("ModelId"); + + b.ToTable("Phones", (string)null); + }); + + modelBuilder.Entity("PhoneToolMX.Models.PhoneModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MaxExtensions") + .HasColumnType("bigint"); + + b.Property("ModelName") + .HasColumnType("text"); + + b.Property("PreVvxPolycom") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("PhoneModels", (string)null); + + b.HasData( + new + { + Id = 0, + MaxExtensions = 6L, + ModelName = "Polycom VVX300/310", + PreVvxPolycom = false + }); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NormalizedName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Role", (string)null); + }); + + modelBuilder.Entity("PhoneToolMX.Models.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("PhoneUser", b => + { + b.Property("OwnersId") + .HasColumnType("text"); + + b.Property("PhonesId") + .HasColumnType("integer"); + + b.HasKey("OwnersId", "PhonesId"); + + b.HasIndex("PhonesId"); + + b.ToTable("PhoneUser", (string)null); + }); + + modelBuilder.Entity("ExtensionPhone", b => + { + b.HasOne("PhoneToolMX.Models.Extension", null) + .WithMany() + .HasForeignKey("ExtensionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany() + .HasForeignKey("PhonesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExtensionUser", b => + { + b.HasOne("PhoneToolMX.Models.Extension", null) + .WithMany() + .HasForeignKey("ExtensionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.User", null) + .WithMany() + .HasForeignKey("OwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PhoneToolMX.Models.CustomData", b => + { + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany("Ringtones") + .HasForeignKey("PhoneId"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Extension", b => + { + b.HasOne("PhoneToolMX.Models.CustomData", "HoldMusic") + .WithMany() + .HasForeignKey("HoldMusicId"); + + b.Navigation("HoldMusic"); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.HasOne("PhoneToolMX.Models.CustomData", "Background") + .WithMany() + .HasForeignKey("BackgroundId"); + + b.HasOne("PhoneToolMX.Models.PhoneModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Background"); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("PhoneUser", b => + { + b.HasOne("PhoneToolMX.Models.User", null) + .WithMany() + .HasForeignKey("OwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PhoneToolMX.Models.Phone", null) + .WithMany() + .HasForeignKey("PhonesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PhoneToolMX.Models.Phone", b => + { + b.Navigation("Ringtones"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PhoneToolMX.Models/Models/CustomData.cs b/PhoneToolMX.Models/Models/CustomData.cs new file mode 100644 index 0000000..0b48983 --- /dev/null +++ b/PhoneToolMX.Models/Models/CustomData.cs @@ -0,0 +1,21 @@ +using PhoneToolMX.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace PhoneToolMX.Models { + public enum CustomDataType { + Background, + MusicTone, + } + public class CustomData : IModel { + public int? Id { get; set; } + + [MaxLength(16)] + [Header("Name")] + public string FriendlyName { get; set; } + public CustomDataType DataType { get; set; } + [Header("Size")] + public uint Size { get; set; } + public byte[] Data { get; set; } + } +} \ No newline at end of file diff --git a/PhoneToolMX.Models/Models/ErrorViewModel.cs b/PhoneToolMX.Models/Models/ErrorViewModel.cs new file mode 100644 index 0000000..e7a1460 --- /dev/null +++ b/PhoneToolMX.Models/Models/ErrorViewModel.cs @@ -0,0 +1,8 @@ +namespace PhoneToolMX.Models; + +public class ErrorViewModel +{ + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); +} diff --git a/PhoneToolMX.Models/Models/Extension.cs b/PhoneToolMX.Models/Models/Extension.cs new file mode 100644 index 0000000..822d042 --- /dev/null +++ b/PhoneToolMX.Models/Models/Extension.cs @@ -0,0 +1,25 @@ +using PhoneToolMX.Data; +using PhoneToolMX.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace PhoneToolMX.Models { + public class Extension: OwnedBase { + public ICollection Phones { get; set; } + + [Header("Ext.", Primary = true, Small = true)] + public int ExtId { get; set; } + + [Header("Directory Name")] + [Required] + public string DirectoryName { get; set; } + + [Header("Listed?", Small = true)] + public bool Listed { get; set; } + + public CustomData HoldMusic { get; set; } + + [NotMapped] + public string ListViewName => $"{DirectoryName} ({ExtId})"; + } +} \ No newline at end of file diff --git a/PhoneToolMX.Models/Models/IModel.cs b/PhoneToolMX.Models/Models/IModel.cs new file mode 100644 index 0000000..49335f8 --- /dev/null +++ b/PhoneToolMX.Models/Models/IModel.cs @@ -0,0 +1,19 @@ +using System.Reflection; + +namespace PhoneToolMX.Models +{ + public interface IModel + { + public int? Id { get; set; } + + public void Commit(IModel obj) + { + if (GetType() != obj.GetType()) throw new ArgumentException("Input object must be the same model"); + foreach (var prop in GetType().GetProperties().Where(p => p.CanWrite)) { + if (prop.GetValue(obj, null) is {} propVal) { + prop.SetValue(this, propVal, null); + } + } + } + } +} diff --git a/PhoneToolMX.Models/Models/IOwnedModel.cs b/PhoneToolMX.Models/Models/IOwnedModel.cs new file mode 100644 index 0000000..0a8ac81 --- /dev/null +++ b/PhoneToolMX.Models/Models/IOwnedModel.cs @@ -0,0 +1,10 @@ +using PhoneToolMX.Models.ViewModels; + +namespace PhoneToolMX.Models +{ + public interface IOwnedModel : IModel + { + public ICollection Owners { get; set; } + public bool IsOwnedBy(User user); + } +} diff --git a/PhoneToolMX.Models/Models/OwnedBase.cs b/PhoneToolMX.Models/Models/OwnedBase.cs new file mode 100644 index 0000000..3d2c86c --- /dev/null +++ b/PhoneToolMX.Models/Models/OwnedBase.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using PhoneToolMX.Models.ViewModels; +using System.ComponentModel.DataAnnotations; + +namespace PhoneToolMX.Models +{ + public class OwnedBase : IOwnedModel + { + public int? Id { get; set; } + public ICollection Owners { get; set; } + + public bool IsOwnedBy(User user) + { + return (Owners != null && Owners.Any(o => o.Id == user.Id)); + } + } +} diff --git a/PhoneToolMX.Models/Models/Phone.cs b/PhoneToolMX.Models/Models/Phone.cs new file mode 100644 index 0000000..abc5fad --- /dev/null +++ b/PhoneToolMX.Models/Models/Phone.cs @@ -0,0 +1,28 @@ +using PhoneToolMX.Data; +using PhoneToolMX.Models; +using PhoneToolMX.Models.ViewModels; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Net.NetworkInformation; +using System.Security.Cryptography; + +namespace PhoneToolMX.Models { + public class Phone: OwnedBase { + [Required] + public PhysicalAddress MacAddress { get; set; } + + [Header("Name", Primary = true)] + [Required] + public string FriendlyName { get; set; } + + [AlwaysInclude] + [Required] + public PhoneModel Model { get; set; } + + [AlwaysInclude] + public ICollection Extensions { get; set; } + public CustomData Background { get; set; } + public ICollection Ringtones { get; set; } + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/PhoneToolMX.Models/Models/PhoneModel.cs b/PhoneToolMX.Models/Models/PhoneModel.cs new file mode 100644 index 0000000..1403dd0 --- /dev/null +++ b/PhoneToolMX.Models/Models/PhoneModel.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace PhoneToolMX.Models +{ + public class PhoneModel + { + public int? Id { get; set; } + + /// + /// The friendly name of the phone (e.g., "Polycom VVX300/310") + /// + public string ModelName { get; set; } + + /// + /// The maximum lines the phone can support. + /// + public uint MaxExtensions { get; set; } + + /// + /// This setting impacts whether the model is pre-VVX era. + /// All Polycom phones that can be provisioned by Polycom UC + /// before the VVX era (mainly, SPIP phones) have a limit of + /// 300KiB per ringtone. + /// + /// All Polycom phones, VVX or otherwise, have a limit of + /// 600KiB total across all ringtones. + /// + public bool PreVvxPolycom { get; set; } + } +} diff --git a/PhoneToolMX.Models/Models/Role.cs b/PhoneToolMX.Models/Models/Role.cs new file mode 100644 index 0000000..e257cc4 --- /dev/null +++ b/PhoneToolMX.Models/Models/Role.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations.Schema; + +namespace PhoneToolMX.Models +{ + public class Role : IdentityRole + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public override string Id { get; set; } + + public Role() { } + public Role(string name) + { + Name = name; + } + } +} diff --git a/PhoneToolMX.Models/Models/User.cs b/PhoneToolMX.Models/Models/User.cs new file mode 100644 index 0000000..f0cc500 --- /dev/null +++ b/PhoneToolMX.Models/Models/User.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace PhoneToolMX.Models { + public class User : IdentityUser + { + public ICollection Phones; + public ICollection Extensions; + } +} \ No newline at end of file diff --git a/PhoneToolMX.Models/PhoneToolMX.Models.csproj b/PhoneToolMX.Models/PhoneToolMX.Models.csproj new file mode 100644 index 0000000..62e3384 --- /dev/null +++ b/PhoneToolMX.Models/PhoneToolMX.Models.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + disable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/PhoneToolMX.Models/ViewModels/ExtensionVM.cs b/PhoneToolMX.Models/ViewModels/ExtensionVM.cs new file mode 100644 index 0000000..35067d9 --- /dev/null +++ b/PhoneToolMX.Models/ViewModels/ExtensionVM.cs @@ -0,0 +1,57 @@ +using PhoneToolMX.Data; +using System.ComponentModel.DataAnnotations; + +namespace PhoneToolMX.Models.ViewModels +{ + public class ExtensionVM: IViewModel + { + + public int? Id { get; set; } + + [Header("Ext.", Primary = true, Small = true)] + public int ExtId { get; set; } + + [Header("Directory Name")] + [Required] + [MaxLength(15)] + public string DirectoryName { get; set; } + + [Header("Listed?", Small = true)] + [Required] + public bool Listed { get; set; } + + public int? HoldMusic { get; set; } + + public IOwnedModel ToEntity(PTMXContext ctx) + { + return new Extension + { + Id = Id, + ExtId = ExtId, + DirectoryName = DirectoryName, + Listed = Listed, + HoldMusic = HoldMusic == null ? null : ctx.CustomData.FirstOrDefault(c => c.Id == HoldMusic), + Owners = new List(), + }; + } + public IOwnedModel ToEntity(PTMXContext ctx, IOwnedModel current) + { + var ent = ToEntity(ctx); + ent.Owners = current.Owners; + return ent; + } + + public IViewModel FromEntity(IOwnedModel entity) + { + if (entity is not Extension extEnt) throw new ArgumentException("entity must be of type Extension"); + return new ExtensionVM + { + Id = extEnt.Id, + ExtId = extEnt.ExtId, + DirectoryName = extEnt.DirectoryName, + Listed = extEnt.Listed, + HoldMusic = extEnt.HoldMusic?.Id, + }; + } + } +} diff --git a/PhoneToolMX.Models/ViewModels/IViewModel.cs b/PhoneToolMX.Models/ViewModels/IViewModel.cs new file mode 100644 index 0000000..0909369 --- /dev/null +++ b/PhoneToolMX.Models/ViewModels/IViewModel.cs @@ -0,0 +1,15 @@ +using PhoneToolMX.Data; +using PhoneToolMX.Models; + +namespace PhoneToolMX.Models.ViewModels +{ + public interface IViewModel + { + public int? Id { get; set; } + + public IOwnedModel ToEntity(PTMXContext ctx); + public IOwnedModel ToEntity(PTMXContext ctx, IOwnedModel current); + + public IViewModel FromEntity(IOwnedModel entity); + } +} diff --git a/PhoneToolMX.Models/ViewModels/PhoneVM.cs b/PhoneToolMX.Models/ViewModels/PhoneVM.cs new file mode 100644 index 0000000..68db836 --- /dev/null +++ b/PhoneToolMX.Models/ViewModels/PhoneVM.cs @@ -0,0 +1,64 @@ +using PhoneToolMX.Data; +using PhoneToolMX.Models; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Net.NetworkInformation; + +namespace PhoneToolMX.Models.ViewModels +{ + [CheckExtensions] + public class PhoneVM : IViewModel + { + public int? Id { get; set; } + + [Header("MAC Address")] + [Required] + [RegularExpression("(?:[0-9a-fA-F]{2}[-:]?){6}", ErrorMessage = "Must be of the form XX:XX:XX:XX:XX:XX, XX-XX-XX-XX-XX-XX, or XXXXXXXXXXXX")] + public string MacAddress { get; set; } + + [Header("Name", Primary = true)] + [Required] + public string FriendlyName { get; set; } + + [Required] + public int? Model { get; set; } + + public ICollection Extensions { get; set; } + + public int MaxExtensions { get; set; } + + public IOwnedModel ToEntity(PTMXContext ctx) + { + return new Phone + { + Id = Id, + MacAddress = PhysicalAddress.Parse(MacAddress), + FriendlyName = FriendlyName, + Model = ctx.PhoneModels.FirstOrDefault(p => p.Id == Model)!, + Extensions = Extensions?.Select(x => ctx.Extensions.FirstOrDefault(e => e.Id == x)).ToList(), + Owners = new List(), + }; + } + + public IOwnedModel ToEntity(PTMXContext ctx, IOwnedModel current) + { + var ent = ToEntity(ctx); + ent.Owners = current.Owners; + return ent; + } + + public IViewModel FromEntity(IOwnedModel entity) + { + if (entity is not Phone phoneEnt) throw new ArgumentException("entity must be of type Phone"); + return new PhoneVM + { + Id = entity.Id, + MacAddress = phoneEnt.MacAddress.ToString(), + FriendlyName = phoneEnt.FriendlyName, + Model = phoneEnt.Model!.Id, + Extensions = phoneEnt.Extensions?.Select(x => x.Id).ToList(), + MaxExtensions = (int)phoneEnt.Model!.MaxExtensions, + }; + } + } +} diff --git a/PhoneToolMX/.vscode/launch.json b/PhoneToolMX/.vscode/launch.json new file mode 100644 index 0000000..517fc54 --- /dev/null +++ b/PhoneToolMX/.vscode/launch.json @@ -0,0 +1,36 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/net6.0/PhoneToolMX.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://localhost:5001" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/PhoneToolMX/.vscode/tasks.json b/PhoneToolMX/.vscode/tasks.json new file mode 100644 index 0000000..d0b7e92 --- /dev/null +++ b/PhoneToolMX/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/PhoneToolMX.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/PhoneToolMX.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/PhoneToolMX.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/PhoneToolMX/Controllers/BaseController.cs b/PhoneToolMX/Controllers/BaseController.cs new file mode 100644 index 0000000..09555d7 --- /dev/null +++ b/PhoneToolMX/Controllers/BaseController.cs @@ -0,0 +1,225 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using PhoneToolMX.Data; +using PhoneToolMX.Helpers; +using PhoneToolMX.Models; +using PhoneToolMX.Models.ViewModels; +using System.Security.Authentication; +using System.Security.Claims; + +namespace PhoneToolMX.Controllers +{ + [Authorize] + [Route("[controller]")] + public abstract class BaseController : Controller + where T: OwnedBase, IModel + where TViewModel : IViewModel, new() + { + protected private readonly PTMXContext _context; + private readonly UserManager _userManager; + + protected BaseController(UserManager mgr, PTMXContext ctx) + { + _context = ctx; + _userManager = mgr; + } + + protected private virtual Task PreForm(TViewModel vm) + { + throw new NotImplementedException(); + } + + #region Helper methods + + /// + /// Gets the "friendly" name for an owned model object. + /// + /// The model object to inspect + /// The friendly name, or its ID if none was defined + private static string GetFriendlyName(IViewModel vm) + { + if (vm is not TViewModel model) return vm.Id.ToString(); + return typeof(TViewModel).GetProperties().Where(pi => + { + var headers = pi.GetCustomAttributes(typeof(HeaderAttribute), false); + return headers.Length != 0 && ((HeaderAttribute)headers[0]).Primary; + }).Select(pi => pi.GetValue(model)?.ToString()).FirstOrDefault() ?? model.Id.ToString(); + } + + private static string GetFriendlyName(IOwnedModel model) + { + var vm = new TViewModel().FromEntity(model); + return GetFriendlyName(vm); + } + + protected private async Task CurrentUser() + { + var claim = HttpContext.User.Claims.FirstOrDefault(c => c.Type.Equals(ClaimTypes.NameIdentifier)); + if (claim == null) { + throw new InvalidCredentialException("fuck this noise, nameid claim missing"); + } + var user = await _userManager.FindByIdAsync(claim.Value); + return user; + } + + private async Task FormView(TViewModel vm) + { + await PreForm(vm); + return View("_Form", vm); + } + + private void SetMessage(FormMessage msg) + { + TempData["Message"] = msg.ToString(); + } + + private void SetMessage(FormMessageType type, string msg) + { + SetMessage(new FormMessage + { + Type = type, + Message = msg, + }); + } + + private async Task ValidationError(TViewModel vm) + { + var plural = ModelState.ErrorCount > 1 ? "s" : string.Empty; + SetMessage( + FormMessageType.Error, + $"Error{plural} occurred in validation. Make the changes required and click Submit to try again." + ); + return await FormView(vm); + } + + #endregion + + #region Create + + [HttpGet("New")] + public async Task New() + { + ViewData["Title"] = $"Add new {typeof(T).Name.ToLower()}"; + return await FormView(default); + } + + [HttpPost("New")] + public async Task NewPost(TViewModel vm) + { + ViewData["Title"] = $"New {typeof(T).Name}"; + + if (!ModelState.IsValid) return await ValidationError(vm); + + if (vm.ToEntity(_context) is not T model) throw new InvalidOperationException($"{typeof(TViewModel).FullName}.ToEntity() somehow produced something other than a {typeof(T).FullName}"); + var entity = await _context.AddOwnable(await CurrentUser(), model); + await _context.SaveChangesAsync(); + + SetMessage( + FormMessageType.Success, + $"{typeof(T).Name} {GetFriendlyName(vm)} was created." + ); + + return RedirectToAction("Edit", new { id = entity.Entity.Id }); + } + + #endregion + + #region Read + + [HttpGet] + public async Task Index() + { + ViewData["Title"] = $"My {typeof(T).Name}s"; + return View("Index", _context + .GetOwned(await CurrentUser()) + .Select(m => (TViewModel)new TViewModel().FromEntity(m)) + .ToList()); + } + + #endregion + + #region Update + + [HttpGet("Edit")] + public async Task Edit(int id) + { + var model = _context.GetOwned(await CurrentUser()).FirstOrDefault(o => o.Id == id); + if (model == null) return NotFound(); + ViewData["Title"] = $"Editing {typeof(T).Name.ToLower()} {GetFriendlyName(new TViewModel().FromEntity(model))}"; + return await FormView((TViewModel)new TViewModel().FromEntity(model)); + } + + [HttpPost("Edit")] + public async Task EditPost(TViewModel vm) + { + if (vm?.Id == null) { + return BadRequest(); + } + + if (!ModelState.IsValid) return await ValidationError(vm); + + // merge VM's changes with DbModel + var currentModel = _context.GetEntityById(vm.Id); + if (vm.ToEntity(_context, currentModel) is not T entity) throw new InvalidOperationException($"{typeof(TViewModel).FullName}.ToEntity() somehow produced something other than a {typeof(T).FullName}"); + currentModel.Commit(entity); + + // and commit back + _context.Set().Update(currentModel); + await _context.SaveChangesAsync(); + + SetMessage( + FormMessageType.Success, + $"{typeof(T).Name} {GetFriendlyName(vm)} was updated." + ); + + return RedirectToAction("Edit", new { id = entity.Id }); + } + + #endregion + + #region Delete + + [HttpGet("Delete")] + public async Task Delete(int id) + { + var model = _context.GetEntityById(id); + + if (model?.IsOwnedBy(await CurrentUser()) != true) { + return NotFound(); + } + + ViewData["Title"] = $"Delete {GetFriendlyName(model)}"; + TempData["ModelDelete"] = model.Id; + return View("DeleteConfirm", model); + } + + [HttpPost("Delete")] + public async Task PostDelete(int id) + { + if ((int?)TempData["ModelDelete"] != id) { + return BadRequest(); + } + + var model = _context.GetEntityById(id); + + if (model?.IsOwnedBy(await CurrentUser()) != true) { + return NotFound(); + } + + _context.Set().Remove(model); + await _context.SaveChangesAsync(); + SetMessage( + FormMessageType.Success, + $"{typeof(T).Name} {GetFriendlyName(new TViewModel().FromEntity(model))} was successfully deleted." + ); + + return RedirectToAction("Index"); + } + + + + #endregion + + } +} diff --git a/PhoneToolMX/Controllers/ExtensionController.cs b/PhoneToolMX/Controllers/ExtensionController.cs new file mode 100644 index 0000000..16651dc --- /dev/null +++ b/PhoneToolMX/Controllers/ExtensionController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using PhoneToolMX.Models; +using PhoneToolMX.Data; +using PhoneToolMX.Helpers; +using PhoneToolMX.Models.ViewModels; + +namespace PhoneToolMX.Controllers { + [Authorize] + public class ExtensionController : BaseController + { + public ExtensionController(UserManager mgr, PTMXContext ctx) : base(mgr, ctx) {} + + protected override private Task PreForm(ExtensionVM e) + { + var holdMusics = _context.CustomData.Where(d => d.DataType == CustomDataType.MusicTone).ToList(); + ViewBag.HoldMusics = holdMusics; + ViewBag.SelectedMusic = e?.HoldMusic == null ? null : holdMusics.FirstOrDefault(d => d.Id == e.HoldMusic); + return Task.CompletedTask; + } + } +} diff --git a/PhoneToolMX/Controllers/HomeController.cs b/PhoneToolMX/Controllers/HomeController.cs new file mode 100644 index 0000000..2e458cc --- /dev/null +++ b/PhoneToolMX/Controllers/HomeController.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; +using PhoneToolMX.Models; + +namespace PhoneToolMX.Controllers; + +public class HomeController : Controller +{ + private readonly ILogger _logger; + + public HomeController(ILogger logger) + { + _logger = logger; + } + + public IActionResult Index() + { + return View(); + } + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } +} diff --git a/PhoneToolMX/Controllers/ModelController.cs b/PhoneToolMX/Controllers/ModelController.cs new file mode 100644 index 0000000..3ec8879 --- /dev/null +++ b/PhoneToolMX/Controllers/ModelController.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; + +namespace PhoneToolMX.Controllers +{ + public class ModelController : Controller + { + // GET + public IActionResult Index() + { + return View(); + } + } +} diff --git a/PhoneToolMX/Controllers/PhoneController.cs b/PhoneToolMX/Controllers/PhoneController.cs new file mode 100644 index 0000000..70b0fd0 --- /dev/null +++ b/PhoneToolMX/Controllers/PhoneController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using PhoneToolMX.Data; +using PhoneToolMX.Helpers; +using PhoneToolMX.Models; +using PhoneToolMX.Models.ViewModels; + +namespace PhoneToolMX.Controllers +{ + [Authorize] + public class PhoneController : BaseController + { + public PhoneController(UserManager mgr, PTMXContext ctx) : base(mgr, ctx) {} + + protected override private async Task PreForm(PhoneVM pvm) + { + var myExts = _context.GetOwned(await CurrentUser()); + var phoneModels = _context.PhoneModels.ToList(); + ViewBag.MyExtensions = myExts; + ViewBag.SelectedExtensions = pvm?.Extensions == null ? null : myExts.Where(x => pvm.Extensions.Contains(x.Id)).ToList(); + ViewBag.ModelNumbers = phoneModels; + ViewBag.CurrentModel = pvm?.Model == null ? null : phoneModels.Where(m => m.Id == pvm.Model); + } + } +} diff --git a/PhoneToolMX/Folder.DotSettings b/PhoneToolMX/Folder.DotSettings new file mode 100644 index 0000000..33461e4 --- /dev/null +++ b/PhoneToolMX/Folder.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/PhoneToolMX/Helpers/FormMessage.cs b/PhoneToolMX/Helpers/FormMessage.cs new file mode 100644 index 0000000..520ceac --- /dev/null +++ b/PhoneToolMX/Helpers/FormMessage.cs @@ -0,0 +1,18 @@ +using System.Text.Json; + +namespace PhoneToolMX.Helpers { + public enum FormMessageType { + Success, + Info, + Warning, + Error, + } + + public class FormMessage { + public FormMessageType Type { get; set; } + public string Message { get; set; } + + public string CssClassName() => $"msg-{Type.ToString().ToLower()}"; + public override string ToString() => JsonSerializer.Serialize(this); + } +} diff --git a/PhoneToolMX/Helpers/HtmlTable.cs b/PhoneToolMX/Helpers/HtmlTable.cs new file mode 100644 index 0000000..5297fe5 --- /dev/null +++ b/PhoneToolMX/Helpers/HtmlTable.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using PhoneToolMX.Models; +using System.Linq.Expressions; +using Microsoft.AspNetCore.Mvc; + +namespace PhoneToolMX.Helpers +{ + public static class HtmlTable + { + /// + /// Creates an HTML table for a given collection of model objects. + /// + /// HtmlHelper instance in the view + /// List of table headers + /// Function used to determine the structure of each row. All objects will be converted to strings with ToString(). + /// + /// An HTML table representing the list. + /// + /// https://xkcd.com/1319/ + /// + public static IHtmlContent MakeTable(this IHtmlHelper> helper) + { + var builder = new HtmlContentBuilder(); + var urlFactory = helper.ViewContext.HttpContext.RequestServices.GetRequiredService(); + var url = urlFactory.GetUrlHelper(helper.ViewContext); + var props = typeof(TModel).GetProperties().Where(pi => pi.GetCustomAttributes(typeof(HeaderAttribute), false).Length != 0).ToList(); + var idProp = typeof(TModel).GetProperty("Id"); + var headerList = props.Select(pi => + { + var attr = (HeaderAttribute)pi.GetCustomAttributes(typeof(HeaderAttribute), false)[0]; + return attr; + }).ToList(); + headerList.Add(new HeaderAttribute + { + Title = "Actions", + Small = true, + }); + + builder.AppendHtmlLine("") + .AppendHtmlLine(""); + foreach (var header in headerList) { + builder.AppendFormat("{1}", + (header.Small) ? " width=2" : string.Empty, + header.Title); + } + builder.AppendHtmlLine("") + .AppendHtmlLine(""); + + // build body + if (helper.ViewData.Model is {} rows) { + foreach (var row in rows) { + var cells = props + .Select(pi => + { + var val = pi.GetValue(row, null); + if (val is bool someBool) { + // idk, yes/no feels better to me than true/false for user-facing stuff + return someBool ? "Yes" : "No"; + } + return val?.ToString() ?? "(null)"; + }) + .ToList(); + var cellId = idProp?.GetValue(row, null)?.ToString() ?? "INVALID_ID"; + builder.AppendHtmlLine(""); + foreach (var cell in cells) { + builder.AppendFormat("", cell); + } + // Actions cell + builder.AppendHtml("") + .AppendHtmlLine(""); + } + if (rows.Count == 0) { + builder.AppendFormat("", headerList.Count); + } + } else { + builder.AppendFormat("", headerList.Count); + } + + // close remaining table tags + return builder.AppendHtmlLine("") + .AppendHtmlLine("
{0}") + // ReSharper disable Mvc.ActionNotResolved + .AppendHtml($"\"Edit\"") + .AppendHtml($"\"Delete\"") + .AppendHtmlLine("
No entries found
Invalid model found
"); + } + } +} diff --git a/PhoneToolMX/PhoneToolMX.csproj b/PhoneToolMX/PhoneToolMX.csproj new file mode 100644 index 0000000..d509b90 --- /dev/null +++ b/PhoneToolMX/PhoneToolMX.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + <_ContentIncludedByDefault Remove="wwwroot\js\site.js" /> + + + diff --git a/PhoneToolMX/Program.cs b/PhoneToolMX/Program.cs new file mode 100644 index 0000000..db12cc8 --- /dev/null +++ b/PhoneToolMX/Program.cs @@ -0,0 +1,131 @@ +using PhoneToolMX.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using PhoneToolMX.Models; +using System.Security.Authentication; +using System.Security.Claims; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddControllersWithViews(); +builder.Services.AddDbContext( +options => options.UseNpgsql(builder.Configuration.GetConnectionString("DbConnection"), + b => b.MigrationsAssembly("PhoneToolMX.Models"))); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + +builder.Services.AddIdentityCore(opts => + { + opts.ClaimsIdentity.UserIdClaimType = "sub"; + opts.ClaimsIdentity.UserNameClaimType = "preferred_username"; + opts.ClaimsIdentity.EmailClaimType = "email"; + }) + .AddRoles() + .AddRoleManager>() + .AddSignInManager>() + .AddUserManager>() + .AddEntityFrameworkStores(); + +// Using OIDC +builder.Services.AddAuthentication(opts => +{ + opts.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + opts.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; +}) + .AddCookie() + .AddOpenIdConnect((OpenIdConnectOptions opts) => { + // pull from appsettings + var oidcConfig = builder.Configuration.GetRequiredSection("OpenIdConnect"); + opts.ClientId = oidcConfig.GetValue("ClientId"); + opts.ClientSecret = oidcConfig.GetValue("ClientSecret"); + opts.MetadataAddress = oidcConfig.GetValue("MetadataUrl"); + opts.Authority = oidcConfig.GetValue("Authority"); + + opts.GetClaimsFromUserInfoEndpoint = true; + opts.SaveTokens = false; + opts.ResponseType = "code"; + opts.Scope.Add("openid"); + + opts.Events = new OpenIdConnectEvents + { + OnUserInformationReceived = async ctx => + { + // Create the user in UserManager for future authz + var _userManager = ctx.HttpContext.RequestServices.GetRequiredService>(); + var curUser = ctx.Principal; + var user = new User + { + Id = curUser?.Claims.FirstOrDefault(c => c.Type.Equals(ClaimTypes.NameIdentifier)) + ?.Value, + UserName = curUser?.Claims.FirstOrDefault(c => c.Type.Equals("preferred_username"))?.Value, + Email = curUser?.Claims.FirstOrDefault(c => c.Type.Equals("email")) + ?.Value, + EmailConfirmed = true, // always confirmed (or rather, expect the IdP to confirm it) + Phones = null, // no phones or extensions by default + Extensions = null, + }; + if (user.Id == null) { + throw new InvalidCredentialException($"Missing required OIDC claim \"{ClaimTypes.NameIdentifier}\""); + } + + if (await _userManager.FindByIdAsync(user.Id) == null) { + var res = await _userManager.CreateAsync(user); + if (res.Succeeded == false) { + throw new InvalidCredentialException($"Creating identity failed: {res.Errors.FirstOrDefault()!.Description}"); + } + } + } + }; + + // if dev, disable secure + if (!builder.Environment.IsDevelopment()) return; + opts.NonceCookie.SecurePolicy = CookieSecurePolicy.None; + opts.NonceCookie.SameSite = SameSiteMode.Unspecified; + opts.CorrelationCookie.SecurePolicy = CookieSecurePolicy.None; + opts.CorrelationCookie.SameSite = SameSiteMode.Unspecified; + }); +builder.Services.AddAuthorization(opts => +{ + opts.AddPolicy("RequireAdmin", + policy => policy.RequireRole("Administrator")); +}); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Home/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} else { + app.UseDeveloperExceptionPage(); + app.UseMigrationsEndPoint(); +} + +using (var scope = app.Services.CreateScope()) { + var services = scope.ServiceProvider; + + var context = services.GetRequiredService(); + context.Database.EnsureCreated(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}") + .RequireAuthorization(); + +app.Run(); diff --git a/PhoneToolMX/Properties/launchSettings.json b/PhoneToolMX/Properties/launchSettings.json new file mode 100644 index 0000000..1ecff00 --- /dev/null +++ b/PhoneToolMX/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5205", + "sslPort": 44330 + } + }, + "profiles": { + "PhoneToolMX": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/PhoneToolMX/Views/Extension/DeleteConfirm.cshtml b/PhoneToolMX/Views/Extension/DeleteConfirm.cshtml new file mode 100644 index 0000000..2a7948b --- /dev/null +++ b/PhoneToolMX/Views/Extension/DeleteConfirm.cshtml @@ -0,0 +1,10 @@ +@model PhoneToolMX.Models.ViewModels.ExtensionVM + +@using (Html.BeginForm(FormMethod.Post)) { +

Are you sure you want to delete extension @(Model?.ExtId)?

+ +
+ + +
+} \ No newline at end of file diff --git a/PhoneToolMX/Views/Extension/Index.cshtml b/PhoneToolMX/Views/Extension/Index.cshtml new file mode 100644 index 0000000..86ad953 --- /dev/null +++ b/PhoneToolMX/Views/Extension/Index.cshtml @@ -0,0 +1,6 @@ +@using PhoneToolMX.Helpers +@model ICollection + +

This is a list of all extensions you can edit.

+ +@Html.MakeTable() \ No newline at end of file diff --git a/PhoneToolMX/Views/Extension/_Form.cshtml b/PhoneToolMX/Views/Extension/_Form.cshtml new file mode 100644 index 0000000..dce71b2 --- /dev/null +++ b/PhoneToolMX/Views/Extension/_Form.cshtml @@ -0,0 +1,33 @@ +@model PhoneToolMX.Models.ViewModels.ExtensionVM + + +

Fill out the fields below, then click "Submit" to @ViewData["Action"] the line.

+ +@using (Html.BeginForm(FormMethod.Post)) { + + @if (Model != null) { + @Html.HiddenFor(m => m.Id) + } + + + + + + + + + + + + + + +
@Html.LabelFor(m => m.DirectoryName, "Name in Directory:")@Html.TextBoxFor(m => m.DirectoryName)@Html.ValidationMessageFor(m => m.DirectoryName)
@Html.LabelFor(m => m.HoldMusic, "Hold Music:")@if (ViewBag.HoldMusics == null || ((ICollection)ViewBag.HoldMusics).Count == 0) { + @Html.DropDownListFor(m => m.HoldMusic, new List(), "-- No music available --", new { @disabled=true }) + } else { + @Html.DropDownListFor(m => m.HoldMusic, new SelectList(ViewBag.HoldMusics, "Id", "FriendlyName")) + }
@Html.CheckBoxFor(m => m.Listed) @Html.LabelFor(m => m.Listed, "Show extension in global directory")
+ + +
+} diff --git a/PhoneToolMX/Views/Home/Index.cshtml b/PhoneToolMX/Views/Home/Index.cshtml new file mode 100644 index 0000000..5b9ec34 --- /dev/null +++ b/PhoneToolMX/Views/Home/Index.cshtml @@ -0,0 +1,24 @@ +@{ + ViewData["Title"] = "Home Page"; +} + +

Welcome to PhoneToolMX! Select one of the options from the sidebar to begin.

+ +
+ +

News

+ +

Oct 17, 2023 :: Version 1.0

+ +

Finally, initial release! As far as user-facing features go, it's still pretty light. + But the scaffolding is there for new features like:

+ +
    +
  • Online Voicemail
  • +
  • Custom Media (Wallpapers, Hold Music, Ringtones)
  • +
  • Call Groups (Multiple Extensions -> One Number)
  • +
+ +

Even though this is v1.0, expect bugs, and please + report them if you see them.

+ @this.Url.Action("Error") \ No newline at end of file diff --git a/PhoneToolMX/Views/Home/Privacy.cshtml b/PhoneToolMX/Views/Home/Privacy.cshtml new file mode 100644 index 0000000..2479fb7 --- /dev/null +++ b/PhoneToolMX/Views/Home/Privacy.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/PhoneToolMX/Views/Model/Index.cshtml b/PhoneToolMX/Views/Model/Index.cshtml new file mode 100644 index 0000000..6836435 --- /dev/null +++ b/PhoneToolMX/Views/Model/Index.cshtml @@ -0,0 +1,7 @@ +@using PhoneToolMX.Helpers +@model ICollection +@{ + ViewData["Title"] = "Phone Models"; +} + +@Html.MakeTable() \ No newline at end of file diff --git a/PhoneToolMX/Views/Phone/DeleteConfirm.cshtml b/PhoneToolMX/Views/Phone/DeleteConfirm.cshtml new file mode 100644 index 0000000..e526ba2 --- /dev/null +++ b/PhoneToolMX/Views/Phone/DeleteConfirm.cshtml @@ -0,0 +1,13 @@ +@model PhoneToolMX.Models.ViewModels.PhoneVM +@{ + ViewData["Title"] = $"Deactivate {Model?.FriendlyName}"; +} + +@using (Html.BeginForm(FormMethod.Post)) { +

Are you sure you want to deactivate phone @(Model?.FriendlyName)? This will not delete any associated extensions.

+ +
+ + +
+} \ No newline at end of file diff --git a/PhoneToolMX/Views/Phone/Index.cshtml b/PhoneToolMX/Views/Phone/Index.cshtml new file mode 100644 index 0000000..9858f00 --- /dev/null +++ b/PhoneToolMX/Views/Phone/Index.cshtml @@ -0,0 +1,6 @@ +@using PhoneToolMX.Helpers +@model ICollection + +

This is a list of all phones you manage.

+ +@Html.MakeTable() \ No newline at end of file diff --git a/PhoneToolMX/Views/Phone/_Form.cshtml b/PhoneToolMX/Views/Phone/_Form.cshtml new file mode 100644 index 0000000..aa5e92c --- /dev/null +++ b/PhoneToolMX/Views/Phone/_Form.cshtml @@ -0,0 +1,34 @@ +@model PhoneToolMX.Models.ViewModels.PhoneVM + + +

Fill out the fields below, then click "Submit" to @ViewData["Action"] the phone.

+ +@using (Html.BeginForm(FormMethod.Post)) { + + @if (Model != null) { + @Html.HiddenFor(m => m.Id) + } + + + + + + + + + + + + + + + + + + +
@Html.LabelFor(m => m.MacAddress, "MAC Address:")@Html.TextBoxFor(m => m.MacAddress)@Html.ValidationMessageFor(m => m.MacAddress)
@Html.LabelFor(m => m.FriendlyName, "Phone Name:")@Html.TextBoxFor(m => m.FriendlyName)@Html.ValidationMessageFor(m => m.FriendlyName)
@Html.LabelFor(m => m.Model, "Phone Model:")@Html.DropDownListFor(m => m.Model, new SelectList(ViewBag.ModelNumbers, "Id", "ModelName", ViewBag.CurrentModel))@Html.ValidationMessageFor(m => m.Model)
@Html.LabelFor(m => m.Extensions, "Extensions:")@Html.ListBoxFor(m => m.Extensions, new MultiSelectList(ViewBag.MyExtensions, "Id", "ListViewName", ViewBag.SelectedExtensions)) +
+ + +
+} diff --git a/PhoneToolMX/Views/Shared/Error.cshtml b/PhoneToolMX/Views/Shared/Error.cshtml new file mode 100644 index 0000000..654a41a --- /dev/null +++ b/PhoneToolMX/Views/Shared/Error.cshtml @@ -0,0 +1,14 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Something went wrong!"; +} + +

An unknown error occurred while processing your request.

+ +@if (Model?.ShowRequestId == true) { +

Provide the following request ID to the application administrator, or include it in a bug report:

+ +
    +
  • @Model.RequestId
  • +
+} \ No newline at end of file diff --git a/PhoneToolMX/Views/Shared/_Layout.cshtml b/PhoneToolMX/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..bd985df --- /dev/null +++ b/PhoneToolMX/Views/Shared/_Layout.cshtml @@ -0,0 +1,89 @@ +@using PhoneToolMX.Helpers +@using System.Text.Json + +@{ + var userIdent = (System.Security.Claims.ClaimsIdentity)User.Identity; +} + + +@* ReSharper disable once MissingTitleTag *@ + + + + + @ViewData["Title"] - PhoneToolMX + + + + + + + + + + + + + + + + + + + + +
+ @await RenderSectionAsync("CtxOptions", false) + + + @if (userIdent != null) { + Hello, @userIdent.Name! @("[")Logout@("]") + } else { + Login + } +
+

@ViewData["Title"]

+ @if (TempData["Message"] != null) { + var msg = JsonSerializer.Deserialize((string)TempData["Message"]); +
+ @msg.Message +
+ } + @RenderBody() +
+ + diff --git a/PhoneToolMX/Views/Shared/_Layout.cshtml.css b/PhoneToolMX/Views/Shared/_Layout.cshtml.css new file mode 100644 index 0000000..dc7c1ee --- /dev/null +++ b/PhoneToolMX/Views/Shared/_Layout.cshtml.css @@ -0,0 +1,49 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +a { + color: #0077cc; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; +} + diff --git a/PhoneToolMX/Views/_ViewImports.cshtml b/PhoneToolMX/Views/_ViewImports.cshtml new file mode 100644 index 0000000..336102d --- /dev/null +++ b/PhoneToolMX/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using PhoneToolMX +@using PhoneToolMX.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/PhoneToolMX/Views/_ViewStart.cshtml b/PhoneToolMX/Views/_ViewStart.cshtml new file mode 100644 index 0000000..6e88aa3 --- /dev/null +++ b/PhoneToolMX/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/PhoneToolMX/appsettings.Development.json b/PhoneToolMX/appsettings.Development.json new file mode 100644 index 0000000..b7cc1a1 --- /dev/null +++ b/PhoneToolMX/appsettings.Development.json @@ -0,0 +1,18 @@ +{ + "Urls": "http://localhost:5001", + "ConnectionStrings": { + "DbConnection": "Host=yuuka.i.2ki.xyz; Database=ptmx_dev; Username=flurry; Password=PeE4PKpMUE2qPja4FyGrvB8FVArZKWa5YReFbfRW649chx38" + }, + "OpenIdConnect": { + "ClientId": "3f2b1348-6d6b-4332-8c43-0a7a5707ccbf", + "ClientSecret": "gto_ljwf22pbzaop7tv6nv2upf6binv7siybye7xu5ytecfrdlils6ra", + "MetadataUrl": "https://git.2ki.xyz/.well-known/openid-configuration", + "Authority": "https://git.2ki.xyz" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/PhoneToolMX/appsettings.json b/PhoneToolMX/appsettings.json new file mode 100644 index 0000000..4d56694 --- /dev/null +++ b/PhoneToolMX/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/PhoneToolMX/wwwroot/favicon.ico b/PhoneToolMX/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..63e859b476eff5055e0e557aaa151ca8223fbeef GIT binary patch literal 5430 zcmc&&Yj2xp8Fqnv;>&(QB_ve7>^E#o2mu=cO~A%R>DU-_hfbSRv1t;m7zJ_AMrntN zy0+^f&8be>q&YYzH%(88lQ?#KwiCzaCO*ZEo%j&v;<}&Lj_stKTKK>#U3nin@AF>w zb3ONSAFR{u(S1d?cdw53y}Gt1b-Hirbh;;bm(Rcbnoc*%@jiaXM|4jU^1WO~`TYZ~ zC-~jh9~b-f?fX`DmwvcguQzn*uV}c^Vd&~?H|RUs4Epv~gTAfR(B0lT&?RWQOtduM z^1vUD9{HQsW!{a9|0crA34m7Z6lpG^}f6f?={zD+ zXAzk^i^aKN_}s2$eX81wjSMONE#WVdzf|MT)Ap*}Vsn!XbvsI#6o&ij{87^d%$|A{ z=F{KB%)g%@z76yBzbb7seW**Ju8r4e*Z3PWNX3_tTDgzZatz7)Q6ytwB%@&@A|XT; zecM`Snxx5po$C)%yCP!KEtos~eOS)@2=kX-RIm)4glMCoagTEFxrBeSX%Euz734Fk z%7)x(k~T!@Hbg_37NSQL!vlTBXoURSzt~I**Zw`&F24fH*&kx=%nvZv|49SC*daD( zIw<~%#=lk8{2-l(BcIjy^Q$Q&m#KlWL9?UG{b8@qhlD z;umc+6p%|NsAT~0@DgV4-NKgQuWPWrmPIK&&XhV&n%`{l zOl^bbWYjQNuVXTXESO)@|iUKVmErPUDfz2Wh`4dF@OFiaCW|d`3paV^@|r^8T_ZxM)Z+$p5qx# z#K=z@%;aBPO=C4JNNGqVv6@UGolIz;KZsAro``Rz8X%vq_gpi^qEV&evgHb_=Y9-l z`)imdx0UC>GWZYj)3+3aKh?zVb}=@%oNzg7a8%kfVl)SV-Amp1Okw&+hEZ3|v(k8vRjXW9?ih`&FFM zV$~{j3IzhtcXk?Mu_!12;=+I7XK-IR2>Yd%VB^?oI9c^E&Chb&&je$NV0P-R;ujkP z;cbLCCPEF6|22NDj=S`F^2e~XwT1ZnRX8ra0#DaFa9-X|8(xNW_+JhD75WnSd7cxo z2>I_J5{c|WPfrgl7E2R)^c}F7ry()Z>$Jhk9CzZxiPKL#_0%`&{MX>P_%b~Dx0D^S z7xP1(DQ!d_Icpk!RN3I1w@~|O1ru#CO==h#9M~S4Chx*@?=EKUPGBv$tmU+7Zs_al z`!jR?6T&Z7(%uVq>#yLu`abWk!FBlnY{RFNHlj~6zh*;@u}+}viRKsD`IIxN#R-X3 z@vxu#EA_m}I503U(8Qmx^}u;)KfGP`O9E1H1Q|xeeksX8jC%@!{YT1)!lWgO=+Y3*jr=iSxvOW1}^HSy=y){tOMQJ@an>sOl4FYniE z;GOxd7AqxZNbYFNqobpv&HVO$c-w!Y*6r;$2oJ~h(a#(Bp<-)dg*mNigX~9rPqcHv z^;c*|Md?tD)$y?6FO$DWl$jUGV`F1G_^E&E>sY*YnA~ruv3=z9F8&&~Xpm<<75?N3 z>x~`I&M9q)O1=zWZHN9hZWx>RQ}zLP+iL57Q)%&_^$Sme^^G7;e-P~CR?kqU#Io#( z(nH1Wn*Ig)|M>WLGrxoU?FZrS`4GO&w;+39A3f8w{{Q7eg|$+dIlNFPAe+tN=FOYU z{A&Fg|H73+w1IK(W=j*L>JQgz$g0 z7JpKXLHIh}#$wm|N`s}o-@|L_`>*(gTQ~)wr3Eap7g%PVNisKw82im;Gdv#85x#s+ zoqqtnwu4ycd>cOQgRh-=aEJbnvVK`}ja%+FZx}&ehtX)n(9nVfe4{mn0bgijUbNr7Tf5X^$*{qh2%`?--%+sbSrjE^;1e3>% zqa%jdY16{Y)a1hSy*mr0JGU05Z%=qlx5vGvTjSpTt6k%nR06q}1DU`SQh_ZAeJ}A@`hL~xvv05U?0%=spP`R>dk?cOWM9^KNb7B?xjex>OZo%JMQQ1Q zB|q@}8RiP@DWn-(fB;phPaIOP2Yp)XN3-Fsn)S3w($4&+p8f5W_f%gac}QvmkHfCj$2=!t`boCvQ zCW;&Dto=f8v##}dy^wg3VNaBy&kCe3N;1|@n@pUaMPT?(aJ9b*(gJ28$}(2qFt$H~u5z94xcIQkcOI++)*exzbrk?WOOOf*|%k5#KV zL=&ky3)Eirv$wbRJ2F2s_ILQY--D~~7>^f}W|Aw^e7inXr#WLI{@h`0|jHud2Y~cI~Yn{r_kU^Vo{1gjabsw!WqDu1dfpQ=?q zRaIYARbQ%Af2x0fs(+uVs-LQ=zpB*V|IEz)0Dr0gpQpq^ED|%vqj7mWG&2W-(;$#S57=zf0JH=Ey;g4w3zN+}5&8^ne=Qgb9QX^; zgPA}^h*A2PMS%u`goTEKQHfGgT8$J3j}(!Ul#r2-n3t25mXe;IprM(gSdXKgovD(X zqM@#eFnJ5O9Ux7Mr+X)MF_8%(zFSc!-lR{ zX*xxbp)4&6EjrB9kmIS2u`V@2v?vgfK|=ih5hBL2rOTHvW6G>i$JP)(%x>yTGNqXl zA_u7r5isrAu{d4A^;ic^UDGrozAZh+uFX_7Siy}Ur>!QCJ^ua`5i9oONkV6j%{pk% zV4`cKJU)cT>f@`s5hq@ZJJA%yk9FsM3d~6hT0&a{5}e7nvE#>(BclL9XNb*YJ99ci zvLp&oM9`=VA_~nYm#Sg9kzmSnOgXBs(X>YAj@-O?HSy&M%e$;fvxf;9VvD$K$%G2| zLaYRfQY+?tE0PkaT>8eo3pqmX#5^vxwZ(-amQ239`Sa*wysNA^3A6Xld~ya_s7f;{ zaJSq7;*?A{tx%EiO*&N-_@99Nodetd8@SnVOAx*J!_RL4;i4EpY$b*eNyjmQQFU{1 zw_S6Ck$9I}viQZ9bX(R^pk zlFd*c+9N2LbHX?OiSPwDQc=~z8&-7*4}w$05f5(j+|h>~dPwM7N)(QTQgFkywIO)< z*`-R1CW0bji4{es7mI$j=qHJHUWbWc-^uCWjXXB$=%bLT=R*K04Vl?Zn9YX_PXi_L zL}}Js0aujta7kqvR89rPRaYtX8fsBFWvZ!K;zP$Bw^XX@N&UR3l1XtUrYLd4CP%1X zc}morvU$09s9%9b_v}U3Id|RvMn=8G$f8(8>g~7SHd>Azec;5$9uvZ+DHDm^mjxp} zX%Vksx1>gbf2-X~8-HMm^IAG^B=}l9amd4!tzdlx;ljGI8KkdlrnTX)9Lo8jc7*A!dS=NY_c&J%qK-#Fc5dCEdsS<3C zfpcgh2Rr~L7*DO1=i}i2!dh#+E5rrOLUF~&eVF#iW(y_}U?5Ew_w6j+4f5>_AyBt- zguTsj-^3Shyn2-WBP6E)J_#?zJfU!j>R!!z5I{2*xtB#4cQQoQo)-N92}@FKE5LCZTM`-{qGXCe_X%UKLL zo&+aIL2h+T|gheWoyO>QJ38v>=cK#2H`Zg-N4Mqp9F zE_!iqLZcbF#5l(P-w-Y*?D|7brpLG?m1a?v@q#;=LZ!)_<5oYI!_lY)Kkf<4Ra#KNQ0KQo^-EXvlxXd5f|7#J~ogXl*UhT@<1iG6XJ=ci8cr#_|HZ7=7`S zsEpKlEJL&9*&-RyQ>ulQe;>M_?_=IucDzFJ{N!% z(nE)$G>Bq7RHxM-YpNu@mJ zsiSG0%Tmp&R5Hr76X9&;a7@C_6skahcpYVY?0cFGpC*S%J*b#oXcoy+tW`H=(etJ_jf`vX;7aD* z%w|SX%zcJ>IMKw*K1TV0(3DB5RuO1(#z4~!gWxZUiitl2(h^47=c8!m<3SN(B-T)O znLvb%h>|p?6scH1csh|fpHvwXahI)&S!+*Bgpn38k<-5Vl`%flAP4`^LEvDfGn+x` z5vtJyCsd&dD8Yvu4Rks7RRv%*;V49v>J>e1u4xJ#rsdqH(PHLGs$n&wuXt66v{=kI zbh7IdTbetKph#qKJty2g$yN%qbAU_K>m>e4+~NuYeY&|Px-7#%3Au@u)BC4T8f(xf z*rA#KH}u!$wyPypmFZW~%#DM5#GrjKbE^lNnpdSdvqh?eQe#btYi*>hZq|vDo)pPi z-72Wu`gcX>tRlvsQ!Tf0(YOdsunL&+0->o1gZu;%KL$sT&D>;-M7`y^@b}DudA48$ zYv%YU+PPc(MuwCL3uW2|V)IfoqsT3-nSusV`nhRB;Pt0A>wDHCb`qwu?W8zuN7w(F zly`9@+s1$k!P+VK$xwEKffnqB3GvgF&x>V+(^yMq;PMKrJfBvLd55ID;a}SQ3KzD9 z$2;heUw3pz3)y$DGUMYkGNUmU5UDFl?dB|ODnvrInW+EDjkmbNEp-l!s79yNoBF-~ z+t^Ab&S(ue5>d`Hak(On6}MnLJl#V-hGbZPs0+g*bgYL6aZD0>VY6dtbBGB>6$dN9 z&Ki~4s{@8++YHjzvObrI`K+}b-ng~FIwBk&ZRFgx4ZqEHOIm&WP5*9DiNPc1BQ(uz zoYq5!TY`rQ!mUr{HP>pUhTfwb#n1)8BF4)odX9&)G$8_(&M;^1JHsaC?4 zDT1Ga8jVyW!a&bxEXCwBX75mo7rHQf9mZkOwwuR&2bwr*{!?Yq4c5+4tH1>x;g^EqWoR$15>n`_u*6i&LeT+&g* zx80hvDL;e|`E8mhVq^5#=i!bsr^PgnTK!zSQEgs|A%v#*uGZ;LclcQ)xoSi=yz<+W zHpby9S0O8~$aB?#*hgQy0hKS_UP+1=*h6aPy^;xUkM7IqLxfw^z2@-s(9aBEJ`&Fn z)+#sOijn!|qe=99Q?qlK_tU)arrdP$6%Q@a+P28^xbmH+E7f^Cd1hO@b}junZvRXA z^7Bm_W-5C!Q*c@3I(Jk3)Uu`k`awKn6{>Zp&rsy$7mhxt?|H;`Rh47^UQsH7r(IX| zZ7|k*n?)`@lsxz2UKTQ6JCQ_v#!G2Q1Tn|P#8}uH?pfh>bd%FfvMSu+_G!)mPI{#;Uz{XiY5>c8XW}K!+AclJk z2v+fPCWqvJrxSf=aTbpRX~_X~*q419w?I;Nb(5rVv{ika1Xm`Q1u3|O-a$r(l@G6$4knN#`3GSbpVx>Z+3_H3UAhV@k}r3g3Rq#~U6ip@5Q_Lv@^#)5AM zQFH`@tS2}}K!dQf4A4b2eOM2b0f?z~J&(05ZvsCS)Q89zSc-&v4dr_xws!bF?E5;R%l07eOp*{+VXG}_eAiRKyx*ZM&=>+n3LpTZ{TJRx2S%i zrc?^T0>9`}HS-&>l$1MDd#&b&=(a5l8I@GvGp4bJ7TIC{F64Js36VazDjrsJ#?*Va z!hAB5S&hdvX%P(^Vv>=kfi5YNT9<~Drz9O{U%C^I9+5ySMI+rO1v>ecje(FqiHqvB zk7D9Wl+z`K1SC3fGq0DFc=&&429`EgLe}FKq9QC6c}(c@hzvt2SQTg6MF$OXjRm+_ z+565Yk(FA8CS2R}Q zFEVH|9dw7$z=xn^p1XkvB!rzvf(6Y84W>X4GB-$fREcScXYH4K`-oy4>5;@HMA@WG z{KIhU2p-e-nv-;a=xCc2xOx5gB1A@Y#vxK>L7+w|3}?n-mAFUbfSl?lVzB~)UJ#Hq zgHMGM2u&%8QX`&J*$f|Q1iL< z(K!&vrw(MtH6fHej3cUAz*wFkDz$JLjfjB%b3lQl=>^NDj|6v`JhUHK11Flteat3` zHS$P;)~DozihwG0=9rrU#$SIrpU+?;joPd_;BTjwpi+|$KFaD^&1rvw+41Lsk2N-k>_eU6l?GWygei;}J>D5` zkLY)!fds1dWv7Zg4Oxm{rgA~UnZGtoh9H^oDyuD;o;;*aA*HK;>aR7sr}X7{Ad*A` z+pmpAtdtf`4-_Jn)UX;jif<9IRQm$|*_A$41&8W|vC$NiLo-Jxpj6LEkn=$qh4(70 znkG|VOC9Ps(tsa6L8=?7uF^zz;e(7}(JQRxe+Jip3@DcPW>1CGbajfO=Xi5v)uYeS zitwmX1uG{>0*d=}TTJ^g>4c8lazs_TxnX;I|H2N3IVPp~4wGtTs$p5?vMw{YjJ3izw8#Y>B|R-rJ9ul|H<-Vz-DPNF4784iI}$P~ERR-~-SWNO>tS4l4nEnF>Xo3AN~sLypq~0lb|^ly@rbUg7UbG9 z0+pSp+6Wt35{lQND3+nX(WX^Xz<}FP(nO~{=#KeSf)1H)ds>P$MK>ICxtNQz9bvtH zLXY_AHTz1viz33DDAa@R|s(!1QA32uGtg*t&fSFiLC3V3qsX#R%F~Krm3rk9TvZJ3UTV?ZB$g45{ zigV9bG0S@xQGm)2%Leukso=`WpnIIvTAceBlmTd|lTlipiT!4iF$ndPDBIV3r6$mrUs8`E5d$cO%8x@quYd>kvY~03@^=feJJtL3~SK^DvXgKrn z%a)0qa`#5ID=+D~(0vxC&wOXO5z+X%nJh{=n+U=C*uleeMKXk)|)(+ z)r%(qbZLG{aq0YCuPdXP<%xN1q?2h`pOZBFQVuZmljWnrJ*{GAhjv7h8e!m+hrw<) zT)!%tuGIi4Eqk5a$XLP#1T_~^?*nfMwPMOS3^gZA9E!)X@ zL{1CRveVZ8_t~8l86g?^HTnT3L=fBrsy=+p*hCm`0Juk#Xa~#P(=MngFc_tCBtCBU z6BlOPsn$luOqH0u!1I!wd+W@&cCU)8bbKIk^8-Zk_SN5U z^=)l)b0<75T$k)DyYzn<+Tx-{9~%a*D}msEc~u7p4=v^+L;X2*pk1S@pk?$ziLDxn znUL`(AJ)@l`U@|Zt(_*cDA}pmizHQWzMk%daKihMo1L#vY!LZtxYC=e)DavXO~tVN z3WI({4ePuM3%%hCq_vZSb`G-@nR})YCr}=oSv5Ia;u^}tgo53KJLRN?>BcXn&ls-Q zg9Lm3J@EwB{c;1Pd+$Psx-4KZ6;3(%50_PpMW=wfHy={`zc0elP(RlZ( z)-ve$UFbbd0STZdNQ)8A3k$Jr=ns+T*B+wrY9STle8&#Jz**_{m`s_DLV3aIFfDV8V1$aXzPbd`51c3N`IFfl5H4HyV3*VVor z?EE@{#qKrChU3nm$*+CivOVZ-^T;ZF$sw&aba4ufPO}_LjsW&_GA-`2SVJ?Mq-G2{ zIv0u8xJRxW%Xzz07QO-q=^h)j454zv)e|Z|*hYOE>-)aux>WMM&dj;u%niS{1t{kK zXR&hPxvZ7lI?1cq1kTZX$}yiXeE#SXwB ztn#vmAO;e@87^mNycAQzJ%bJE_q;H?$O2@*9%KvIJF(RJ{xi^^Sf$F0isL$(Ns(vi zoZPOi!A1-AGZ0sJoBR2nt!eCn!Q){+-(oKbV{hmbcUvRU+Q?Gm*}et9chOkCXbauF zcE9o$>d*b=sb|JMh39Lai=cv^u|+&X4eE~%Z0|}bAGIVsJ+a+zJM>Af(52eM^%q%* zmnPuYgYN=prX_vCA>Q&86xSzUa9lQMBrmPpltF@{WL&-eH79SHsr4z?WL2<#=kWVB=~9!A2zWY|>B zXuhbBr09_tfcAmnRE~(I>aFzc8GO2?x<0xfa%RSCV&1y+_S(MC%+L@c5YreJD<>z= zG*=|^#vBi83SV0zS18v|cYjw?sL-L5hMu!n>jvXOxxd55%g@u-+b2oo17w|LVUg+J zH>Dq%eQiqIp#vum95^WCWbs$U2pvFvW(5kwDA1r;gCdD=WXZrH0WD1b!XT*@R*7x;r4FmN z8riN+?J72$FjXgL;&_FSh1a%6@4<~LcP`z!cD3N;LNLr23}eQaFv!NAUkfJk`E97! z4T{4PecaK37!1vfmNWYKIH?x~NS;GNlJV$~fIcN_Jv#Y|Mqgfv=5k`(0#ZY7aM(ox2x zgad4@(BxZBEEIPGdO^uz8&IWaR8)~WdbO2ONr6)pH^yK|6;;qhr6pF_Y4=onFDcW^ zRNygJB{)Bo16q`w1b~8@Qf>u>1R6AmpPn2a!BEii2HolMjn`!*pOor>TpM7QcM(4VxhEn5h0^(1PLvkMG^>&qltD}N=Kn& z(k{EHl8SG?#iCnCMW*$WkHD7V5wSb+xK4OarDBQ`A7x6Fbz51)vWH7Qh90W1+?sm99aerw^au=*6Vs zg|Qca*T#p7F5d7FjI^XCV}@{20b~7>+m@c>smpQCS%R6jC4Ag)huzM-~n2J^;i$#rK)-H+{tzispXi%fTHb!VPqwy$A zK#LMrq=Nu6X(?P&Q(nxJ2A-hEX&Ph7M)Ww4fhAl30{0P6+N2l0Bi=;;@j{{!L}fmM z*-d(uD$Ly4peci-;Z%d882K8d4^s)oi(w4^07GQ*IHH7XHy^8!<~G=f1Rlk6MnWBt z(qbFnnBpj>>z}be_cAzcE{@LnnOK@*G^iz`gw!zx?x>7S zD;8~pMuO~F?8fsPX^GHEhwM@{*!esUx+{3p@lw==N5h-(3}>m(VI;fJLsOnoZ9lk| zD;r7=CB~9%T(F-JKvkg&0RT{X8=pk~j;RJPzCmyrD_;*tby0-4;V^{53t=jwDrm|= za(&ng0of>$YpPR^M%rc|r4d0>JaU}ij1y37mcdixRGeHBWpns>$SlPZhV*2onx5yp zZal+w{Ok(3c9N`L1O+yo#H6?0MM@5A&k6O>00e+gN{7NV9^Z>-zDQ+PCqR*4=(CUd z{?XBbNahRh(<1*0Qn$h(##A?fh+pV{#eq-;d98R6WjCTT0@ieZm7s-50NEo4VrOen z;>AygwWH7h>723Ph-qAR)!%i+cCsbmR;Q)aa0#-kV%lLhUPyriVCx!}R9@0vvB|Uq z>5&B`OAlGugazEEUaGW@bK`3N-6lYk18}K>R3lG7#W>-5-(`3xxp%I^3_hZv#bY*URs?X0lqF2 ztq8p?kBtCJ?Fx*$kC`Yf4VRFNQukmMITtjeq&Q|f%PU)3}#IkA%`hhx}OL;9I&u-o$v zr_{-20N1xs(QqNZv`th0TsX!QI4Bg##(@pwm;*cRF{gu&fh8`gLR}=ENt>w4z+m8B z7b1ultne_DrWBZUNL)S`XClU!h!3!FGDh*%<)Ju^wY%BxoegMB8L5mo#o`2&+NBg_ zThPEdMJ+les}+!-EVoU|hCgTf;n_0UEzU}@xD6cxN@mrmjdtf!h9fRf&W^^_Wx|25 z=Yv&tI^RV2@!Ev!#8G>9qEeVtq-tH>CT~F^RX$&&b^t|%jN;ZNij#>%F+VaYv(*Zr z^O2xg2%1u}rZZ9r0+;n>&=_E;+i5pyrQMd6$*ec4z2+@kV$VGb4UrlCb8Th#l9b=m zs-xY-xqoZ)efCrTTEr#pm?F*9NuO2IP=KfgGQDYiV?Dcr67~J=cVvqV!HI~O!rh)Y zzJL5p4P_)_{q$FdN<~&Na+v$AYf_jc1CBE3IU^y&9CAcxRwZVB^l+T5a-+c9z)x!6 z=Y+J{B9+8-&32bR`&=|Wb7{muf4ZST48q^K@WW00-E!XDXi3xf>PMI>kf&Q?tiyf> ztj`zMNsTeEA7Si`x=#*nsqilJg_uot7(dQFWIT964!2hR@|VAdE>E$28an|-qPZ&m z`u$Z2v>dcQXIgt2Uh_U}`N~F{_=&&5Hnb2t43e#9ZX06;Q04Qq`aJsg`%c83k0VtQ z5FJFT8}SJLVE&PnZVgX!O&zFBmk-5W1L}a*6^M}Wz(h%);h364lpPnK-4|3*-*|y} zfgMK~21+p>u1y@>$;teDput5}|FFoISr)4-!Zcx)9zBq>IZtb_P!6)pPCQy@H5i=r zkvaL7j<`VNHB0ynV9)WKw}pq)2pzY)j+{*cbpRml+*X&!5Nw^E?9da`Il)^!0l(dw zjsd_0C;-{WN4#iA1Z=A4g4!{D$A)`zn50u&kFu()U;eFuY12DiO4go(TjC}#b@6pfxtbs2* z3Q5`jl0(FS#Lb-?%$F>#!N!pyN^x9%eH_S)*&&=+jer$6%~a{=AP;WaQV1b~#aWvb z*v&iy4&nj>bskyFA6SUq{PA2dLXr|D$KYYriMf!6ZIz07A7TQan2}&rbtGEoS>p-KP?daM7Jeb6oh6>w+XC80pY%rtOrqG74I6@@ z5M%%#W@mr&KpI|TAnGM{CP*Hd(%1mxzigxt6eaE`TBSrxDDa5^$-P;-?VZp~WcX1e#3*)KAxK0iJ4zovNh|K~ehv#4V+r7cJz6 zI_iQvz-O*O#~DBa97+PP3ThGu9oa?!9LfWjTQ@e_v>oO5y^c4f1!#5u6H-iJIAG(V zHR15wQ?y(dZ?#4q(Lj<`001r$tkw?w)!&AV3j#)6oUSAF#7kNV!CG-ZoB{!K1){y! z#|Efr4}2P)DC?QVOI#Ac1(0X8`oMo40eL#>4-7!E`T*jb!34z1MkeO{>`S)#Kmgo} zlKIsxHP#ARL2Ev0zuJcb{F*cIK?C$9f)v0Wl!lw}%F7r)p%g4yH9$9#n7FxxR?w)a zjukfG#(7{Y2MuLa1}nHR-8QbA{vDp2mClyvs*?8VJB_7=IpLYufRsAHfmB_Ny`G%r z(h%%Rxjw6v3V|3V!H@YYegf^dW@ke#qJiw}5kM;kH~>ZUz`5H0ho2PA4%}6vJZ(a{ z-T^3T7~N|}irRttYuReRjM0HihS?+VYkR6$1>q43w5`9yU$zzFv%OYP!r9y6M&2?- z;&p5iA_wq%n~zG8RnE>iMjD-jlWzjx;~t%_PGz8ZWw44LbvD7ax)pU1CZ34t5NNIv z2x|6l?h@eX=MKR|UgzqGmqIF_5rl3MT;$Dqox|B7w<_qrJ<9A#tJJL`+TrU5&Kdwb zZP^+xpNybP{?Q|lCxZ|GX=#MZxIl+4hy+lO%?(nF;w@OI8TbCoX$9lkHf4_z&#zW4 z6mnjSDQ+ean44%rSFY^NiIC5EC3h%?yg9+?da2MN!3CKAB@ozb=2mUcn&A@6RhkX~ z>k(=Y+~ex{*wPX~05d_nL~RgY>4qpyB^J{}#0%H%KrZz~Wy05YNdcZ7@A1lkq@snj zk*$I}?>4nqD0GUY-}CFRd;eZ%G;yR*-Kh zr_o(umiQls&9CJZDGXq)61=4cA1e|7q6Y&2AmVNk+@rjHun{rA7Y~7@wUq(K%L0e% z>k@$&H$fQ>493v$sEJy4fx&2+@frJ2t>Hy`0fy9?r~@E!A}jJDBXaX12n0NU14Qy9 zOL8So@&lN#f;d1-uEppSLW$xfB=pLHS*oGDDCZ3Sm}~Xu_S!-a7oKriZ1?i8Qgp)QrF|USR;ffxg(aDa`?9h_u7`q<0`oMv-@elYfS~;^1C~y(w zEE3#fGy_4qHh~%=!JHPsG$Sw*upW^aZRsi~)>71l-Usd#>JNQgDc<76hzf&XPZpJk zKQI7;$X)a4+9TjIeEpjGiGTwfN(1!DuYfXxfNHL=YPJm>4%_eGaqlaaTi+H35ZBQw zr;s~Qo>h*^5-RZ#&rUAm>bF!MHU;@gfLHSy72t zzyb8-E21Dkk3vIdC&F2oZ)nC08;U~bi~*?iLxO5F>PY)V^u!t`;wf7zgCA(Ln>6^K zUkfg`f%J2pbaBeq;WEv;H8JpPLsns7hglupn_27ecxRxno2=U?5@Eb##AC5S#V zgcX$$0Q^{j{Bzx*U|mPR2$w(>1&bie?R`9eajn93YmZQ}giGj%G>WP*>TrVXtxojd zdh0|id-RTiEK`-^)T}S!n%=lTnxEnS4rS|{aGtDEiC-}E5NFcMn5r{UyJHVf=~=?| zO=DM5^MIv^wqTle0!ug$G`Lao2@n(jo>(nA=fwt_RJ}>EW96;0j&kWrbkBleIYKF5~TS?sb8qqWiO1Sd6l2Xlw0|- zV7WGi5=jTEtTuK*IX1hEqd4r;=V_tt9Fnc3H*8u%4+$c>;EspSZ*)#e=T_2<=r*g@pvPmS{t@3qm?%#UHvU`8L$EqAU zlKeZBv&~~fxC62t?)!)v&X3jq9w2LJ(_<~cqm`4f0UL;j-(WyLFG%va;zP&;2GlhHu$Qa> zyh1Fr!l&e2_ZO41A1arNYs|^WdtTqpviF9(W?PsN7kjom=U6nh6y{%kf6y;O=@%}Q z`0b#c)%-kUyPRgYN1l5Sq`TBNK~V#BHD_8iUp`h>{!S}>&?h3n^fmx60H{TMaV5S%Ydyo0wzH8;C+^y1-Lc= zw%!vq-iv&#*LUD2@vuvpITn8J43~PNi3~AXX5($+t9dflgU>%O(5wH?k3NdW`TDHIvKd{{|d-Z!WHI7Ix>Ji z4Mup39y1tf_4;6PGzCK@@Dz$gPXPG*Oa}}at-FahWDfF!L13WF@q9Y{PA5P|VQ@fU zXi(_qci3oX=yW z&>Cy&_&69WOdQ;}gRJwDk=*P|^TPa`qtM2IygZGZVU7KAqm5Gy#eu*T$w1q=(3fH8AK3!YC#Ik|p8UwHS&31iM8G7ClCwsXezE(ybQlL-9gSl>O8F#rN!JcET9XzA2uHp7^SiZKpJMnrMo z%@m7KtYD;)g$Wlg8zWk{JwizC%?uY>2ml;IOAQk0%!z4eP6j?VJrxq_T>)1Nj6BIsX$-Aq%@evH-CSuz3P67<)994vqgKbpmGnO+>0RHs{-1rQn#CSh0x5-cd8T5GJin9+|w z6!2PYz(G+26f)@c8%u(5(10AxDR*Qbb^Ia7m58vph?_o{he}YIY?obp2CN}$v2Bw7 z*&df;mf2;s=81(YRZ?|1RZ}?;JBgduO*!RIbQWkR4jwZ4preHf8ZUtyEUFDV1KNvk zyE`oSR)u#hP_GH^?!ZrEDNsZyhX-o|;z>)manfh`fYDWRpXvz3LKzUS#jPAfKnJfw0cqtL#_U8FjQq#P*~JQ{AQs zb+%APWfgp8ikDTGQ8xQ#e9ErbtO4uV;4gt>2Z$&RZJcdj4c@R#(4c^p{fykrtPQDR zhH0>aHS-p9=?aYoZov zY$`$q637h11JD`wD<#Xt^o2%t2-36Cs|OucEn_YVc2V9XBD<4>SUDfn*G4@xw@-C- ze3)09X=9V5wa%HXmn)MjKe zs)84&P#edcz%o#n80vsf93w5_b-@zSvrtAXpiw7xSzC{reCNBbJ&RnDz|ys(hBfCA z&01P1-qgZlwduLXOxQzLlky{{abbc2WBXeb-O?!jZQ*`y8Q}i#gg|HiWKoM6;G+Df zk+x*?PcdG^!3@@jFoos7i=R=!2p$%WlW^({WN1hdB3B|(xxsN6Q6Q?0u(>UC;Ucs- z!*A@!8vrD7MyE3eafT!ZBK@jHEu@3&fR_nOc1=q*G~RWL_QXmsNjg|Ei!O|zJm_r; zdf3uh^JtkwBid4lO+1VvXT`^ zY;}$Y6ar@F&_Dx_Q=~%_=?~A*XN?vC9q81eT1CX3cdYl7rKyttO;;)&MWym3Y6&qs z$}{EkvVziDE)R;#Dwp%d7RCl;3T@FOfj{`B7;7BPY{dLm8clFhYVKf+%?N5vx3~j_ z$#u}y#+2X^|hiU4WjOpNCB=iLb8h8 zl2S?viD6RHoMIGT2`=!1gvrGk7br(e=jYk)JxZw5NUaVqaKO9htF$_pOBnfKnGwuo z2KNZyK;W?(3#bzY4^g0ROryZza^Qles;6sY#DtQ;utsVBC7p0J&{ifChc>#JBoiK> z(3pSaHDr^FfEPR$g|x~Fy5=B&u#{r}?*d^1&^b;gloPCn z0s^8nA>x3=l|9~JY0K~Z!xwDSWp7>OX^JINni9)j{#N$COSCd%(;LL4aPPFm1RpdL zEX^(4#TsUG7rP4MTAThr#2KcaHU@}Of?S}Iea^H021*M~69hyvM_{xG>KN5OL(GoK z#SLYOt0O6NS|u!k0*i?J1L>OV4E~Z;I z#BOCOrDc<*nRJ%SGxKGQn9FRdf-98e<(hH-NyMG|M6(9g^fB?A=hcRTB8^sUPjz6; zr0P4~f!4tQrGDriR6XibWvV_bKy^vWOEDu5?i=(t8p=p;3U6;5Yc6-{k5pj|YEUZ% zCV*W_GT?%;_L;7fGjcaz7Xe@9?s#X1A3^;3JYyL)m`{EsrKz$>dhHJKNFgo1TK46n zw+~5s!3)Ytmgu733N1kj`=pcp5is4Xq9|Nb3mZ-w6d_DA!X3AElOV+$F!vdW`V4}? zkM}?D{SHK|eDj_7(*39bh$&+^XlS792o_DtDt6p?2EeNvC`rabC2F#2f#PUwCqwjQ z4hc{pB~SR7A~_McrGU|PA1-HDB2{`% zlUS`cbdd#MtME#T6?@qBLp?DFOb2dG_Y8)1H$MOY_?80~fNtsbdv@j*T~`9iCjs2o z3|EI2GMH~aumRIo0vRAziFQsBU=0wEFh@vyk47mJ(l2rsMn5N7~GiSSSv@K8$j0}s$Tc)&@K2Y~}9Z0LnMSn^P1 zu~C))dfXu_OtA|w7kU#oft43v7^sLvhk>fs6g(jcneY@Im^?rSg5Tynz!whk_FBp} zGXoG83NQfrM*wuDiJK^VB*23IK)_%N&>t6&0irkn5Ku`ahKjkDK%N)@1CRkCb^!}8 ziU?7Pa-oa7cn=pKia|I5ebIIl(jOM%KSQN>p#l*o5-M6qenAE}q{b1j;Z|KBWDjHk zZukNZFcKIChHLOMNWq3l))L@Fb8@02Q}!LxGG@nubEv>&rZ+tHr*nU(Psr9vK5+}e zR*%nih>2){tapK_XA6|2kGkMP$_9zeqh6p_9aL0j-P3d&88oVTL8Cq)F(PL_CFaCW7P;jrlv;|Q7S=}eusns zUC03?gE5V>5ah@K4!~;v7Z(v}xQ+{j04n(c+ogu$RTHaYdIOk|_NXQ~@qk=rLzr-Y zO#=(A2PehyYd$w^OH_2M$B2#CW~Z=hp67XPre7Xt7MMskr??KOh!-t*4jX`jGvy~J z>2;nM7zD5ZbY=pas33}I4veWcn)wD101lD%HiuJ3va%bF5fE3kTqkjcfz&c7Glu1- zV>;GjzJVJkAenA;P_B}Wpz{Q@@qZzgc(OqpEhG~-lN58A9y5eQ&cY>qD2ZK>oNYoi zLX%2vww!`^mx}lc4(W&sX$q~hkJv^^7dd+xsTktqeDWhs5^xub#$bvDj1fSDUAc=3 zpn}(=V);1@7XUN=W7iB5F^Zn30TB=Z5HJ9)_?|FBli~1-qgbCDAOZKupUA+U4@#dd zv5Q=!lgQAXI6!HB#{>H?TpG}JtcgxUkRw`;rvQm{cw=>`x$8fL(i5_)*Y28Q*g zj^b%o0uZ4jcQcy=fPQd05=EWs@gxLUE$ISGSF(=FS%8xVm!tG@mZ&wW$DQDrms?tN z!vcu|*%iswb7zK+8HT25s-|nore{>4o)L|}1xX>Q8=t`s126#w8UY0m0aU=6D0+8| z;{Xwmr+G>M4Dh00l>rbysEyh=w#g8Tnx}j^sS{8D4{$OxClbAx2AH={HSz!jFaZQG zss%u*1YoNFUU>UqxAY*T8h7Kj~bi4^!niKU>W4cVP~iI*10 zojc(OOk!oVYOCE+3T+Cl(JHOeO0CcLP3>YCb)*@SQV?p_jJZ)&yCD&zmNDF@VV@_bWmoB&_Nyc(_t!fnMbiLu{$61XbVy;^#1*C?v8ssscb6gL!BM_l9EdsCm z^f?=W20z=Qj`yiKN^7ihB!uva55U40x-&%gemd>Vw&bI4lN@+qxM6Q3Nn+T?nTT zHX8*Eael19jhJJ!W3a6xL4O`3v}TpQpoEla@MTKFBz2`6Y}h)g`UiRdEPx=j^x5yQ>v|xT_VF6E)OdKheA& z48kGIyezA-j^TtMsxsNvw;014S>=9_v#Fg5Rw<#qq9X?Lv`-@AP?j5yy@`ghW-^$- zD@gXa3AIUtuvZ4jhZosHK@m%AjV zAU42E(G_jc(F;{9=C{~azAE!?$2_8GKn8u3$kYtJwsDO!HqurZ5-p;>jJ&=CEbtNQ7vd*}3kb=oOZ^;Vj@_}SqriMVpkyQl2 z<5FvBrkE|z1w$CFTuKd6T&BaqK%e*TJ5IoQSXo*W%y%kb?khM&E zv`ydz9>OpiIMhcO)T<+bTR;IMLUkHk*ld;KyR+?-ao{>*W98D%&4$+<*p>Upm#fHD ztP#j|94WqXyaIBfdw&bqHKUxYwJU7!3@&tO9@}ZGs0}$<3vC+Pdbtb61Z}JoEWu0E zhv-e-m9UQJAw3DH)bBwD22SNwekW#~N7;8FeLHEq9kb*cxEs>t44xw-9l0{hBNq-U zWH7EReGviW3L5op{y}M=z9JrvJ5<48Ez$!t*D8-J>^pSg*KOj!c^o%0#5%3C zEoBo62ECrt6^3@7VL~n=hCfD;cVi@W`Ao4 zmYv%=RRZXp{cL=h@!sL-p>Afr;GJz7(IJZhlIYp+Z5~2C>AoxJ4lK)&bQN!ud-)y)upwD&${y9q08Q=; zdG1vGwVUS@87si7-DX97y25%4L9h6W-yj_t8Iyv{pVq=Ev&SGRM-6`TPRO%9s~bBa z!z_aE^Cx7P3ezDz;?pg@znSb&RPkQ6&a0a)kG=8MA=IA!-)4&Y^1S0E59{)A=n**S zQC-lv`|_Dx)ujyhN(!BffYj8Zz@hM+CeQfOPyI{f;9joLh^$toS-p=BI4S&8W{1pD zucG9CzE$t*A@SS~vG6jz^*l`CI(h{9gx%)6wC0R1|7w^e18k2Va8RN+Ql@c~C%dl5 z3B#>O=YAqveS4mJRxfqnfbW#x&WmI)YakW|l7@P^4SiF?iJ51_e z2OvBypVRC1J3g=9^ZWikV4&b2VId#}93qfn9HT`d0OBJeWe~tmCB`G%rW|A3Bv9w3 zWaMR}g(gOTszvH5MyxDGMr=kvt!!-uFDvdCFD{v0R#Fioppo|$Vqf(b@qo!gSHiXCwa`W;ITs?XQj+7}o zLj(_q>HxB(w?)+0hG5H{%V*^-L?&3KAYj6G-@t2M4kjF9#2f;9?;g@US z4WbzXk8nb0+rIXg`>n*hZzkM~YYcH=ng-V4h#Z=E zW=biDq$J^KB7t-#D4%^c$x0hq_!^+LQSysVv<0(UZ@i62l~Jo0r?M=?iC`g}eDVn?sbITIwin9sgKFpkWvHS_ z^-PU#V5n;$E6UtrBGSrr5e=sT$Xl7^C{JOUm#_w2Z%6tX5@ zYpv_Wa7BCLbn${($k|v<##bVi@x*V4yf@!{oB1-H%5KJpgoPJq5lgA9a!PPDyGC;^ zjKk95Fy8u1uA)@MVzi`C|80(i0G|)COjbuu@{F6s4zk$8AV7u zvpts8=@2pO4CBx@zFAyiTOP8Up@f0CA(qP;$#BLklDLegND3HEnM$UZ@(%w&t%@}W z;M~Lm1?k+41+;P=|63e_kW1u8N!?*ir=~-NhONzlYztv`mL?bv28lb*JBNLYSFaTE zv5$W2!}YvpIF;29Bb$iUBvPTn9me7n!1>`WXyzhu5fOe#(cGh);fzM1Xj1K}$`{7K zzy6UDYFDh!HUbDHBEZodQJNUQYyb~s7;G*D{GS-TGQnM{?SL6P!tvyozo=c%I*!@e zF{}8;Wipd*EUY2m98?Jh(U3vev&ctgk{U@y0#FpXMK>cEPM-)WIJ&u95s|@*64hua z%b?#og)=&6NQ0eK93>S^DFQU?Ycc&2+c}PDEA8O%2-qmvE%}8@e~NK!PQr^m{_>A5 z{x2&VtQ{;1|9VU%Nf&u#)Z`!3C`SvI7_@q5^@THBq{NlMX2H@xFFw8 z(j}Ckd<%2xR0>s^K{O;5Eh?AFRE(BFFR1aaX{5ZO1GRX7Vv6H}Rs~GPmNZL8h38)X zTTJijQ=VB#X?PH68#)9dA1>w*mK(jQGDRxaxnAa(gLsKFPdb`HW<)}viD}JBf;00` zjweH51~6Q-8}|VNXeqc9Jc-r}qg_mYS1A!SmfEzZX_f?01!&YBXf-dkGDx#r257z7 z6|kZOc&_b-UfwfNSk_~$bm15X;mD4%_TZy*{Vj0)V^?a9L^FwtNz4v65yFW^G%nSl zBV*A`|5!kRCpb-)hI(?-aQ*asKSi2QBda1}M8$MgIf!veSxPE4&1q57A{|}O+OxXj zA6V+fiCCqhu&yqb7SqofEqDSceK9@1BFjJVhCq$t(H&0NN5JtkU#DV9gYMLbn=&W|0+}#lXOz1H?cP0fuG}gMq=!%*?^T!PnO}GAt-EGBPbK zEi)^txVX5hs|%t4PO=0FobX)D`g}(8`8zU-eNLHzUbEJmbmS1-rSwH&j&t0!?W!pb z%?rZ?Re0Q89y%%SOWlK C^)3Ve literal 0 HcmV?d00001 diff --git a/PhoneToolMX/wwwroot/images/edit_x16.gif b/PhoneToolMX/wwwroot/images/edit_x16.gif new file mode 100644 index 0000000000000000000000000000000000000000..94026ad9621e12479887b355ea7e963958df8104 GIT binary patch literal 220 zcmZ?wbh9u|6krfwC}&_${K>+}#lXOz1H?cP0frVJZDL{)85We8l~q_+*dJ!ym2dE3 zxAE^!W}7qST(91>a8lFkwcFnvo^|NZp*L^deEat8|Ns9W?Zg4rNCkzyl+1Yzi+Wsc z^L=38@jB#IA;)<_VL}Us^9czhzP2C%p+<{-Pt}VZHoUXWxh?)+9NXP<@X$rw*tve} kH&=Dr$TUuF=&auJc$wvqbH}H%G%z+bYqYktDKc0C055h*Bme*a literal 0 HcmV?d00001 diff --git a/PhoneToolMX/wwwroot/main.css b/PhoneToolMX/wwwroot/main.css new file mode 100644 index 0000000..317e7ad --- /dev/null +++ b/PhoneToolMX/wwwroot/main.css @@ -0,0 +1,182 @@ +body { +background-color:#333333; +} + +table#main { +margin-left:auto; +margin-right:auto; +width:722px; +color:#000000; +font-family:Geneva, Arial, Helvetica, sans-serif; +font-size:12px; +border:none; +} + +table#main td { +background-color:#CCCCCC; +padding:0.45em; +border: 1px solid #000000; +} + +/* +table#main table td { +padding:unset; +border:none; +} +*/ + +form table input[type="text"], +form table select { +width:100%; + box-sizing: border-box; + -moz-box-sizing: border-box; +} + +table#main td#header { +background-color:#ffffff; +height:150px; +padding: 0; +margin:0; +} + +table#main td#header img { +width:720px; +height:150px; +padding:0; +display:block; +margin:1px; +} + +table#main td#nav { +background-color:#ccccff; +} + +table#main td#nav, table#main td#content { +vertical-align:top; +} + +table#main td#nav ul { +list-style-type:square; +margin: 0.5em 0 0.5em 1em; +padding: 0; +} + +table#main td#nav ul li { +margin:0 0 0.25em 0.5em; +padding: 0; +} + +table#main td#footer { +background-color:#000000; +color:#ffffff; +font-size:10px; +} + +table#main td#footer a:link { +color:#00CCFF; +} + +table#main td#context, +table#main td#userinfo { +background-color:#000000; +color: #ffffff; +height:1.5em; +vertical-align:middle; +} + +table#main td#userinfo a:link, +table#main td#userinfo a:visited, +table#main td#userinfo a:active, +table#main td#footer a:link, +table#main td#footer a:visited, +table#main td#footer a:active { + color: #ffffff; +} + +table#main td#context form label, +table#main td#context form select, +table#main td#context form input { +display:table-cell; +vertical-align:middle; +} + +table#main td#context form select, +table#main td#context form input { +height:1.5em; +font-size:1em; +padding:0; +} + +table#main td#userinfo { +text-align:right; +} + +table#main td#content { +padding:1em; +} + +td#content h1, +td#content h2, +td#content h3 { +margin-top:0.25em; +margin-bottom:0.25em; +} + +table#listview { + width: 100%; + border: 1px outset; + border-spacing: 1px; + background-color: #666; +} + +table#listview td, +table#listview th { + padding: 0.25em; + border: 1px inset; +} + +table#listview thead { + background-color: #aaa; + border: 1px #aaa inset; +} + +table#main form table td { + border: unset; +} + +div.status { + border: 2px ridge; + margin: 0.25em 0; + padding: 0.5em; +} + +div.status.msg-success { + border-color: #4a4; + color: #161; + background-color: #bfb; +} + +div.status.msg-warning { + border-color: #aa4; + color: #661; + background-color: #ffb; +} + +div.status.msg-error { + border-color: #a44; + color: #611; + background-color: #fbb; +} + +div.status.msg-info { + border-color: #444; + color: #111; + background-color: #bbb; +} + +span.field-validation-error { + width: 100%; + display: block; + font-size: 10px; + color: #c00; +} diff --git a/global.json b/global.json new file mode 100644 index 0000000..9e5e1fd --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/ptmx-asp.sln b/ptmx-asp.sln new file mode 100644 index 0000000..95b2a66 --- /dev/null +++ b/ptmx-asp.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhoneToolMX", "PhoneToolMX\PhoneToolMX.csproj", "{130DD8E2-5E99-47A9-8B30-2FF54A0DA89D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhoneToolMX.Models", "PhoneToolMX.Models\PhoneToolMX.Models.csproj", "{84FB1CE2-A4B6-4251-9427-8AEB175D6874}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {130DD8E2-5E99-47A9-8B30-2FF54A0DA89D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {130DD8E2-5E99-47A9-8B30-2FF54A0DA89D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {130DD8E2-5E99-47A9-8B30-2FF54A0DA89D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {130DD8E2-5E99-47A9-8B30-2FF54A0DA89D}.Release|Any CPU.Build.0 = Release|Any CPU + {84FB1CE2-A4B6-4251-9427-8AEB175D6874}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84FB1CE2-A4B6-4251-9427-8AEB175D6874}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84FB1CE2-A4B6-4251-9427-8AEB175D6874}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84FB1CE2-A4B6-4251-9427-8AEB175D6874}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ptmx-asp.sln.DotSettings b/ptmx-asp.sln.DotSettings new file mode 100644 index 0000000..14592c7 --- /dev/null +++ b/ptmx-asp.sln.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file