├── .gitattributes ├── .gitignore ├── README.md └── src ├── App.razor ├── BlazorLoginDiscord.csproj ├── BlazorLoginDiscord.sln ├── Data ├── AccountController.cs └── UserService.cs ├── Discord.OAuth2 ├── Discord.OAuth2.csproj ├── DiscordDefaults.cs ├── DiscordExtensions.cs ├── DiscordHandler.cs └── DiscordOptions.cs ├── Pages ├── AccountManage.razor ├── Error.razor ├── Guild.razor ├── Index.razor ├── RedirectToLogin.razor └── _Host.cshtml ├── Program.cs ├── Properties └── launchSettings.json ├── Shared ├── MainLayout.razor └── NavMenu.razor ├── Startup.cs ├── _Imports.razor ├── appsettings.json.template └── wwwroot ├── css ├── bootstrap │ ├── bootstrap.min.css │ └── bootstrap.min.css.map ├── open-iconic │ ├── FONT-LICENSE │ ├── ICON-LICENSE │ ├── README.md │ └── font │ │ ├── css │ │ └── open-iconic-bootstrap.min.css │ │ └── fonts │ │ ├── open-iconic.eot │ │ ├── open-iconic.otf │ │ ├── open-iconic.svg │ │ ├── open-iconic.ttf │ │ └── open-iconic.woff └── site.css └── favicon.ico /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | *.vscode/* 10 | appsettings.*json 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # DNX 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | 50 | *_i.c 51 | *_p.c 52 | *_i.h 53 | *.ilk 54 | *.meta 55 | *.obj 56 | *.pch 57 | *.pdb 58 | *.pgc 59 | *.pgd 60 | *.rsp 61 | *.sbr 62 | *.tlb 63 | *.tli 64 | *.tlh 65 | *.tmp 66 | *.tmp_proj 67 | *.log 68 | *.vspscc 69 | *.vssscc 70 | .builds 71 | *.pidb 72 | *.svclog 73 | *.scc 74 | 75 | # Chutzpah Test files 76 | _Chutzpah* 77 | 78 | # Visual C++ cache files 79 | ipch/ 80 | *.aps 81 | *.ncb 82 | *.opendb 83 | *.opensdf 84 | *.sdf 85 | *.cachefile 86 | *.VC.db 87 | *.VC.VC.opendb 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | *.sap 94 | 95 | # TFS 2012 Local Workspace 96 | $tf/ 97 | 98 | # Guidance Automation Toolkit 99 | *.gpState 100 | 101 | # ReSharper is a .NET coding add-in 102 | _ReSharper*/ 103 | *.[Rr]e[Ss]harper 104 | *.DotSettings.user 105 | 106 | # JustCode is a .NET coding add-in 107 | .JustCode 108 | 109 | # TeamCity is a build add-in 110 | _TeamCity* 111 | 112 | # DotCover is a Code Coverage Tool 113 | *.dotCover 114 | 115 | # NCrunch 116 | _NCrunch_* 117 | .*crunch*.local.xml 118 | nCrunchTemp_* 119 | 120 | # MightyMoose 121 | *.mm.* 122 | AutoTest.Net/ 123 | 124 | # Web workbench (sass) 125 | .sass-cache/ 126 | 127 | # Installshield output folder 128 | [Ee]xpress/ 129 | 130 | # DocProject is a documentation generator add-in 131 | DocProject/buildhelp/ 132 | DocProject/Help/*.HxT 133 | DocProject/Help/*.HxC 134 | DocProject/Help/*.hhc 135 | DocProject/Help/*.hhk 136 | DocProject/Help/*.hhp 137 | DocProject/Help/Html2 138 | DocProject/Help/html 139 | 140 | # Click-Once directory 141 | publish/ 142 | 143 | # Publish Web Output 144 | *.[Pp]ublish.xml 145 | *.azurePubxml 146 | # TODO: Comment the next line if you want to checkin your web deploy settings 147 | # but database connection strings (with potential passwords) will be unencrypted 148 | #*.pubxml 149 | *.publishproj 150 | 151 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 152 | # checkin your Azure Web App publish settings, but sensitive information contained 153 | # in these scripts will be unencrypted 154 | PublishScripts/ 155 | 156 | # NuGet Packages 157 | *.nupkg 158 | # The packages folder can be ignored because of Package Restore 159 | **/packages/* 160 | # except build/, which is used as an MSBuild target. 161 | !**/packages/build/ 162 | # Uncomment if necessary however generally it will be regenerated when needed 163 | #!**/packages/repositories.config 164 | # NuGet v3's project.json files produces more ignoreable files 165 | *.nuget.props 166 | *.nuget.targets 167 | 168 | # Microsoft Azure Build Output 169 | csx/ 170 | *.build.csdef 171 | 172 | # Microsoft Azure Emulator 173 | ecf/ 174 | rcf/ 175 | 176 | # Windows Store app package directories and files 177 | AppPackages/ 178 | BundleArtifacts/ 179 | Package.StoreAssociation.xml 180 | _pkginfo.txt 181 | 182 | # Visual Studio cache files 183 | # files ending in .cache can be ignored 184 | *.[Cc]ache 185 | # but keep track of directories ending in .cache 186 | !*.[Cc]ache/ 187 | 188 | # Others 189 | ClientBin/ 190 | ~$* 191 | *~ 192 | *.dbmdl 193 | *.dbproj.schemaview 194 | *.jfm 195 | *.pfx 196 | *.publishsettings 197 | node_modules/ 198 | orleans.codegen.cs 199 | 200 | # Since there are multiple workflows, uncomment next line to ignore bower_components 201 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 202 | #bower_components/ 203 | 204 | # RIA/Silverlight projects 205 | Generated_Code/ 206 | 207 | # Backup & report files from converting an old project file 208 | # to a newer Visual Studio version. Backup files are not needed, 209 | # because we have git ;-) 210 | _UpgradeReport_Files/ 211 | Backup*/ 212 | UpgradeLog*.XML 213 | UpgradeLog*.htm 214 | 215 | # SQL Server files 216 | *.mdf 217 | *.ldf 218 | 219 | # Business Intelligence projects 220 | *.rdl.data 221 | *.bim.layout 222 | *.bim_*.settings 223 | 224 | # Microsoft Fakes 225 | FakesAssemblies/ 226 | 227 | # GhostDoc plugin setting file 228 | *.GhostDoc.xml 229 | 230 | # Node.js Tools for Visual Studio 231 | .ntvs_analysis.dat 232 | 233 | # Visual Studio 6 build log 234 | *.plg 235 | 236 | # Visual Studio 6 workspace options file 237 | *.opt 238 | 239 | # Visual Studio LightSwitch build output 240 | **/*.HTMLClient/GeneratedArtifacts 241 | **/*.DesktopClient/GeneratedArtifacts 242 | **/*.DesktopClient/ModelManifest.xml 243 | **/*.Server/GeneratedArtifacts 244 | **/*.Server/ModelManifest.xml 245 | _Pvt_Extensions 246 | 247 | # Paket dependency manager 248 | .paket/paket.exe 249 | paket-files/ 250 | 251 | # FAKE - F# Make 252 | .fake/ 253 | 254 | # JetBrains Rider 255 | .idea/ 256 | *.sln.iml 257 | 258 | # CodeRush 259 | .cr/ 260 | 261 | # Python Tools for Visual Studio (PTVS) 262 | __pycache__/ 263 | *.pyc 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlazorDiscordOAuth2 2 | Example implementation of OAuth2 for Discord using Blazor 3 | 4 | ## Setup 5 | 1. Edit `appsettings.json.template` to include your app id and secret found at [https://discordapp.com/developers/applications/](https://discordapp.com/developers/applications/) then rename the file to `appsettings.json` 6 | 2. (optional) Edit `launchsettings.json` and set `sslPort` to your desired port, this will be used in step 3 7 | 3. Go to your application at [https://discordapp.com/developers/applications/[APP_ID]/oauth](https://discordapp.com/developers/applications/) and add a redirect to `https://localhost:PORT/signin-discord`, replacing `PORT` with the one you chose in step 2 (or 44393 if you skipped step 2) 8 | 4. Add any necessary scopes in `startup.cs` NOTE: Currently email, identify and guilds are supported. Others may not work without editing `DiscordHandler.cs` and adding requesting/parsing data yourself. 9 | 10 | ## Authorizing users 11 | - use AuthorizeView for content that can be seen but changes based on whether a user is authorized or not 12 | Example: `MainLayour.razor` 13 | ``` 14 | 15 | 16 | Hello, @context.User.Identity.Name! 17 | LogOut 18 | 19 | 20 | Log in 21 | 22 | 23 | ``` 24 | - use `@attribute [Authorize]` as a page header to ensure a user is authorized before accessing the page. (demonstrated in `AccountManage.razor`) 25 | NOTE: `App.razor` has been configured via `AuthorizeRouteView` to automatically redirect a user to the discord auth page if they access an authorized page and have not authorized themselves -------------------------------------------------------------------------------- /src/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Sorry, there's nothing at this address.

14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/BlazorLoginDiscord.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | all 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/BlazorLoginDiscord.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29324.140 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorLoginDiscord", "BlazorLoginDiscord.csproj", "{7F217B5B-5F4A-4F10-95BB-42C701296FAF}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {7F217B5B-5F4A-4F10-95BB-42C701296FAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {7F217B5B-5F4A-4F10-95BB-42C701296FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {7F217B5B-5F4A-4F10-95BB-42C701296FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {7F217B5B-5F4A-4F10-95BB-42C701296FAF}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {F0913C4F-05C5-4013-BDA1-9513973D011A} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/Data/AccountController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System.Threading.Tasks; 5 | using System.Linq; 6 | using System.Security.Claims; 7 | using Microsoft.AspNetCore.DataProtection; 8 | using System.IO; 9 | using System.Text; 10 | using Newtonsoft.Json; 11 | 12 | namespace BlazorLoginDiscord.Data 13 | { 14 | [Route("[controller]/[action]")] 15 | public class AccountController : ControllerBase 16 | { 17 | public IDataProtectionProvider Provider { get; } 18 | 19 | public AccountController(IDataProtectionProvider provider) 20 | { 21 | Provider = provider; 22 | } 23 | 24 | [HttpGet] 25 | public IActionResult Login(string returnUrl = "/") 26 | { 27 | return Challenge(new AuthenticationProperties { RedirectUri = returnUrl }, "Discord"); 28 | } 29 | 30 | [HttpGet] 31 | public async Task LogOut(string returnUrl = "/") 32 | { 33 | //This removes the cookie assigned to the user login. 34 | await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); 35 | return LocalRedirect(returnUrl); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Data/UserService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.DataProtection; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Options; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Converters; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Globalization; 13 | using System.Linq; 14 | using System.Net.Http; 15 | using System.Net.Http.Headers; 16 | using System.Security.Claims; 17 | using System.Threading.Tasks; 18 | 19 | namespace BlazorLoginDiscord.Data 20 | { 21 | public class UserService 22 | { 23 | private static HttpClient client = new HttpClient(); 24 | 25 | /// 26 | /// Parses the user's discord claim for their `identify` information 27 | /// 28 | /// 29 | /// 30 | public DiscordUserClaim GetInfo(HttpContext httpContext) 31 | { 32 | if (!httpContext.User.Identity.IsAuthenticated) 33 | { 34 | return null; 35 | } 36 | 37 | var claims = httpContext.User.Claims; 38 | bool? verified; 39 | if (bool.TryParse(claims.FirstOrDefault(x => x.Type == "urn:discord:verified")?.Value, out var _verified)) 40 | { 41 | verified = _verified; 42 | } 43 | else 44 | { 45 | verified = null; 46 | } 47 | 48 | var userClaim = new DiscordUserClaim 49 | { 50 | UserId = ulong.Parse(claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value), 51 | Name = claims.First(x => x.Type == ClaimTypes.Name).Value, 52 | Discriminator = claims.First(x => x.Type == "urn:discord:discriminator").Value, 53 | Avatar = claims.First(x => x.Type == "urn:discord:avatar").Value, 54 | Email = claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value, 55 | Verified = verified 56 | }; 57 | 58 | return userClaim; 59 | } 60 | 61 | /// 62 | /// Gets the user's discord oauth2 access token 63 | /// 64 | /// 65 | /// 66 | public async Task GetTokenAsync(HttpContext httpContext) 67 | { 68 | if (!httpContext.User.Identity.IsAuthenticated) 69 | { 70 | return null; 71 | } 72 | 73 | var tk = await httpContext.GetTokenAsync("Discord", "access_token"); 74 | return tk; 75 | } 76 | 77 | /// 78 | /// Gets a list of the user's guilds, Requires `Guilds` scope 79 | /// 80 | /// 81 | /// 82 | public async Task> GetUserGuildsAsync(HttpContext httpContext) 83 | { 84 | if (!httpContext.User.Identity.IsAuthenticated) 85 | { 86 | return null; 87 | } 88 | 89 | var token = await GetTokenAsync(httpContext); 90 | 91 | var guildEndpoint = Discord.OAuth2.DiscordDefaults.UserInformationEndpoint + "/guilds"; 92 | 93 | using (var request = new HttpRequestMessage(HttpMethod.Get, guildEndpoint)) 94 | { 95 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); 96 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 97 | 98 | var response = await client.SendAsync(request); 99 | if (!response.IsSuccessStatusCode) 100 | { 101 | return null; 102 | } 103 | 104 | var content = await response.Content.ReadAsStringAsync(); 105 | try 106 | { 107 | var guilds = Guild.ListFromJson(content); 108 | return guilds; 109 | } 110 | catch 111 | { 112 | return null; 113 | } 114 | } 115 | } 116 | 117 | public class DiscordUserClaim 118 | { 119 | public ulong UserId { get; set; } 120 | public string Name { get; set; } 121 | public string Discriminator { get; set; } 122 | public string Avatar { get; set; } 123 | 124 | /// 125 | /// Will be null if the email scope is not provided 126 | /// 127 | public string Email { get; set; } = null; 128 | 129 | /// 130 | /// Whether the email on this account has been verified, can be null 131 | /// 132 | public bool? Verified { get; set; } = null; 133 | } 134 | 135 | public class Guild 136 | { 137 | [JsonProperty("id")] 138 | public string Id { get; set; } 139 | 140 | [JsonProperty("name")] 141 | public string Name { get; set; } 142 | 143 | [JsonProperty("icon")] 144 | public string Icon { get; set; } 145 | 146 | [JsonProperty("owner")] 147 | public bool Owner { get; set; } 148 | 149 | [JsonProperty("permissions")] 150 | public long Permissions { get; set; } 151 | 152 | [JsonProperty("features")] 153 | public List Features { get; set; } 154 | 155 | public static List ListFromJson(string json) => JsonConvert.DeserializeObject>(json, Settings); 156 | private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings 157 | { 158 | MetadataPropertyHandling = MetadataPropertyHandling.Ignore, 159 | DateParseHandling = DateParseHandling.None, 160 | Converters = 161 | { 162 | new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } 163 | }, 164 | }; 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Discord.OAuth2/Discord.OAuth2.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ASP.Net Core middleware that enables an application to support Discord's OAuth 2.0 authentication workflow. 5 | Discord.OAuth2 6 | Discord.OAuth2 7 | netstandard2.0 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Discord.OAuth2/DiscordDefaults.cs: -------------------------------------------------------------------------------- 1 | namespace Discord.OAuth2 2 | { 3 | public static class DiscordDefaults 4 | { 5 | public const string AuthenticationScheme = "Discord"; 6 | public const string DisplayName = "Discord"; 7 | 8 | public static readonly string AuthorizationEndpoint = "https://discordapp.com/api/oauth2/authorize"; 9 | public static readonly string TokenEndpoint = "https://discordapp.com/api/oauth2/token"; 10 | public static readonly string UserInformationEndpoint = "https://discordapp.com/api/users/@me"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Discord.OAuth2/DiscordExtensions.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using Microsoft.AspNetCore.Authentication; 4 | using Discord.OAuth2; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | public static class DiscordAuthenticationOptionsExtensions 9 | { 10 | public static AuthenticationBuilder AddDiscord(this AuthenticationBuilder builder) 11 | => builder.AddDiscord(DiscordDefaults.AuthenticationScheme, _ => { }); 12 | 13 | public static AuthenticationBuilder AddDiscord(this AuthenticationBuilder builder, Action configureOptions) 14 | => builder.AddDiscord(DiscordDefaults.AuthenticationScheme, configureOptions); 15 | 16 | public static AuthenticationBuilder AddDiscord(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) 17 | => builder.AddDiscord(authenticationScheme, DiscordDefaults.DisplayName, configureOptions); 18 | 19 | public static AuthenticationBuilder AddDiscord(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) 20 | => builder.AddOAuth(authenticationScheme, displayName, configureOptions); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Discord.OAuth2/DiscordHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.OAuth; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Options; 5 | using Newtonsoft.Json.Linq; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Security.Claims; 9 | using System.Text.Encodings.Web; 10 | using System.Text.Json; 11 | using System.Threading.Tasks; 12 | 13 | namespace Discord.OAuth2 14 | { 15 | internal class DiscordHandler : OAuthHandler 16 | { 17 | public DiscordHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) 18 | : base(options, logger, encoder, clock) 19 | { 20 | } 21 | 22 | protected override async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) 23 | { 24 | var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); 25 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); 26 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 27 | 28 | var response = await Backchannel.SendAsync(request, Context.RequestAborted); 29 | if (!response.IsSuccessStatusCode) 30 | throw new HttpRequestException($"Failed to retrieve Discord user information ({response.StatusCode})."); 31 | 32 | //This has been modified to support net core 2 33 | var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); 34 | var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement); 35 | 36 | context.RunClaimActions(); 37 | 38 | await Events.CreatingTicket(context); 39 | 40 | return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Discord.OAuth2/DiscordOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.OAuth; 3 | using Microsoft.AspNetCore.Http; 4 | using System.Security.Claims; 5 | 6 | namespace Discord.OAuth2 7 | { 8 | /// Configuration options for . 9 | public class DiscordOptions : OAuthOptions 10 | { 11 | /// Initializes a new . 12 | public DiscordOptions() 13 | { 14 | CallbackPath = new PathString("/signin-discord"); 15 | AuthorizationEndpoint = DiscordDefaults.AuthorizationEndpoint; 16 | TokenEndpoint = DiscordDefaults.TokenEndpoint; 17 | UserInformationEndpoint = DiscordDefaults.UserInformationEndpoint; 18 | Scope.Add("identify"); 19 | 20 | ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id", ClaimValueTypes.UInteger64); 21 | ClaimActions.MapJsonKey(ClaimTypes.Name, "username", ClaimValueTypes.String); 22 | ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email); 23 | ClaimActions.MapJsonKey("urn:discord:discriminator", "discriminator", ClaimValueTypes.UInteger32); 24 | ClaimActions.MapJsonKey("urn:discord:avatar", "avatar", ClaimValueTypes.String); 25 | ClaimActions.MapJsonKey("urn:discord:verified", "verified", ClaimValueTypes.Boolean); 26 | } 27 | 28 | /// Gets or sets the Discord-assigned appId. 29 | public string AppId { get => ClientId; set => ClientId = value; } 30 | /// Gets or sets the Discord-assigned app secret. 31 | public string AppSecret { get => ClientSecret; set => ClientSecret = value; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Pages/AccountManage.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage" 2 | @attribute [Authorize] 3 | @inherits LayoutComponentBase 4 | @using Microsoft.AspNetCore.Http 5 | @inject IHttpContextAccessor httpContextAccessor 6 | @inject BlazorLoginDiscord.Data.UserService usrSvc 7 | 8 | 9 |

Hello, world 2!

10 | 11 | Welcome to your new app. 12 | 13 |

14 | Hello, @httpContextAccessor.HttpContext.User.Identity.Name! 15 | 16 | Name: @claim.Name#@claim.Discriminator 17 | ID: @claim.UserId 18 | OAuth Token: @token 19 |

20 | 21 | 22 | @if (guilds == null) 23 | { 24 |

Loading Guilds...

25 | } 26 | else 27 | { 28 |

Guilds

29 |
30 | @foreach (var guild in guilds) 31 | { 32 |
33 |

@guild.Name

34 |
35 | } 36 | } 37 | 38 | @code { 39 | private BlazorLoginDiscord.Data.UserService.DiscordUserClaim claim; 40 | private string token; 41 | private List guilds; 42 | 43 | protected override async Task OnInitializedAsync() 44 | { 45 | claim = usrSvc.GetInfo(httpContextAccessor.HttpContext); 46 | token = await usrSvc.GetTokenAsync(httpContextAccessor.HttpContext); 47 | guilds = await usrSvc.GetUserGuildsAsync(httpContextAccessor.HttpContext); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/error" 2 | 3 | 4 |

Error.

5 |

An error occurred while processing your request.

6 | 7 |

Development Mode

8 |

9 | Swapping to Development environment will display more detailed information about the error that occurred. 10 |

11 |

12 | The Development environment shouldn't be enabled for deployed applications. 13 | It can result in displaying sensitive information from exceptions to end users. 14 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 15 | and restarting the app. 16 |

-------------------------------------------------------------------------------- /src/Pages/Guild.razor: -------------------------------------------------------------------------------- 1 | @page "/Guilds/{Id}" 2 | @attribute [Authorize] //Ensures the user is authorized prior to accessing the pages content 3 | @inherits LayoutComponentBase 4 | @using Microsoft.AspNetCore.Http 5 | @inject IHttpContextAccessor httpContextAccessor 6 | @inject BlazorLoginDiscord.Data.UserService usrSvc 7 | 8 | 9 |

Hello, world 2!

10 | 11 | Welcome to your new app. 12 | 13 |

14 | Hello, @httpContextAccessor.HttpContext.User.Identity.Name! 15 |

16 | 17 | @if (state == State.Loading) 18 | { 19 |

Loading Guild...

20 | } 21 | else if (state == State.Unavailable) 22 | { 23 |

Invalid Guild.

24 | } 25 | else 26 | { 27 |

28 | Name: @DscGuild.Name 29 | Id: @DscGuild.Id 30 | Permissions: @DscGuild.Permissions 31 |

32 | } 33 | 34 | @code { 35 | [Parameter] 36 | public string Id { get; set; } 37 | public BlazorLoginDiscord.Data.UserService.Guild DscGuild { get; set; } 38 | 39 | public State state = State.Loading; 40 | public enum State 41 | { 42 | Loading, 43 | Unavailable, 44 | Loaded 45 | } 46 | 47 | //Use OnParametersSet since the guild id can be changed without the page re-rendering the new content otherwise 48 | protected override async Task OnParametersSetAsync() 49 | { 50 | DscGuild = null; 51 | state = State.Loading; 52 | 53 | if (Id == null) 54 | { 55 | //TODO: show all available guilds? 56 | //Probably not needed since guilds are now populated on the sidebar 57 | state = State.Unavailable; 58 | return; 59 | } 60 | 61 | DscGuild = await GetGuild(Id); 62 | if (DscGuild == null) 63 | { 64 | state = State.Unavailable; 65 | } 66 | else 67 | { 68 | state = State.Loaded; 69 | } 70 | } 71 | 72 | private async Task GetGuild(string id) 73 | { 74 | var guilds = await usrSvc.GetUserGuildsAsync(httpContextAccessor.HttpContext); 75 | var match = guilds.FirstOrDefault(x => x.Id == id); 76 | return match; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 |

Hello, world!

4 | 5 | Welcome to your new app. 6 | -------------------------------------------------------------------------------- /src/Pages/RedirectToLogin.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager Navigation 2 | @code { 3 | protected override void OnInitialized() 4 | { 5 | //This auto redirects a user to the login page 6 | Navigation.NavigateTo("Account/Login", true); 7 | } 8 | } -------------------------------------------------------------------------------- /src/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace BlazorLoginDiscord.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | 5 | 6 | 7 | 8 | 9 | 10 | BlazorLoginDiscord 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @(await Html.RenderComponentAsync(RenderMode.Server)) 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace BlazorLoginDiscord 13 | { 14 | public class Program 15 | { 16 | public static void Main(string[] args) 17 | { 18 | CreateHostBuilder(args).Build().Run(); 19 | } 20 | 21 | public static IHostBuilder CreateHostBuilder(string[] args) => 22 | Host.CreateDefaultBuilder(args) 23 | .ConfigureWebHostDefaults(webBuilder => 24 | { 25 | webBuilder.UseStartup(); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:55601", 7 | "sslPort": 44393 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "BlazorLoginDiscord": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 5 | 6 |
7 |
8 | About 9 | 10 | 11 | 12 | Hello, @context.User.Identity.Name! 13 | LogOut 14 | 15 | 16 | Log in 17 | 18 | 19 |
20 |
21 | @Body 22 |
23 |
-------------------------------------------------------------------------------- /src/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http 2 | @inject IHttpContextAccessor httpContextAccessor 3 | @inject BlazorLoginDiscord.Data.UserService usrSvc 4 | 5 | 11 | 12 |
13 | 37 |
38 | 39 | @code { 40 | bool collapseNavMenu = true; 41 | 42 | string NavMenuCssClass => collapseNavMenu ? "collapse" : null; 43 | 44 | void ToggleNavMenu() 45 | { 46 | collapseNavMenu = !collapseNavMenu; 47 | } 48 | 49 | private BlazorLoginDiscord.Data.UserService.DiscordUserClaim claim; 50 | private string token; 51 | private List guilds; 52 | 53 | protected override async Task OnInitializedAsync() 54 | { 55 | //Don't attempt to query if the user isn't authenticated. 56 | if (httpContextAccessor.HttpContext.User.Identity.IsAuthenticated) 57 | { 58 | claim = usrSvc.GetInfo(httpContextAccessor.HttpContext); 59 | token = await usrSvc.GetTokenAsync(httpContextAccessor.HttpContext); 60 | guilds = await usrSvc.GetUserGuildsAsync(httpContextAccessor.HttpContext); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using BlazorLoginDiscord.Data; 7 | using Microsoft.AspNetCore.Authentication.Cookies; 8 | using Discord.OAuth2; 9 | using Microsoft.AspNetCore.DataProtection; 10 | using System.IO; 11 | 12 | namespace BlazorLoginDiscord 13 | { 14 | public class Startup 15 | { 16 | public Startup(IConfiguration configuration) 17 | { 18 | Configuration = configuration; 19 | } 20 | 21 | public IConfiguration Configuration { get; } 22 | 23 | // This method gets called by the runtime. Use this method to add services to the container. 24 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | services.AddRazorPages(); 28 | services.AddServerSideBlazor(); 29 | services.AddHttpContextAccessor(); 30 | services.AddSingleton(); 31 | 32 | //Configure authentication for the user 33 | services.AddAuthentication(opt => 34 | { 35 | opt.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 36 | opt.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; 37 | opt.DefaultChallengeScheme = DiscordDefaults.AuthenticationScheme; 38 | }) 39 | .AddCookie() 40 | .AddDiscord(x => 41 | { 42 | x.AppId = Configuration["Discord:AppId"]; 43 | x.AppSecret = Configuration["Discord:AppSecret"]; 44 | x.Scope.Add("guilds"); 45 | 46 | //Required for accessing the oauth2 token in order to make requests on the user's behalf, ie. accessing the user's guild list 47 | x.SaveTokens = true; 48 | }); 49 | } 50 | 51 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 52 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 53 | { 54 | if (env.IsDevelopment()) 55 | { 56 | app.UseDeveloperExceptionPage(); 57 | } 58 | else 59 | { 60 | app.UseExceptionHandler("/Error"); 61 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 62 | app.UseHsts(); 63 | } 64 | 65 | app.UseHttpsRedirection(); 66 | app.UseStaticFiles(); 67 | 68 | app.UseRouting(); 69 | 70 | app.UseAuthentication(); 71 | 72 | app.UseEndpoints(endpoints => 73 | { 74 | endpoints.MapBlazorHub(); 75 | endpoints.MapDefaultControllerRoute(); 76 | endpoints.MapFallbackToPage("/_Host"); 77 | }); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.JSInterop 8 | @using BlazorLoginDiscord 9 | @using BlazorLoginDiscord.Shared 10 | -------------------------------------------------------------------------------- /src/appsettings.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "Discord": { 10 | "AppId": "", 11 | "AppSecret": "" 12 | }, 13 | "AllowedHosts": "*" 14 | } 15 | -------------------------------------------------------------------------------- /src/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /src/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /src/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /src/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PassiveModding/BlazorDiscordOAuth2/0f8034f24476ff4e54f9a8847c652e4033199f9a/src/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /src/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PassiveModding/BlazorDiscordOAuth2/0f8034f24476ff4e54f9a8847c652e4033199f9a/src/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /src/wwwroot/css/open-iconic/font/fonts/open-iconic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 9 | By P.J. Onori 10 | Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) 11 | 12 | 13 | 14 | 27 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 74 | 76 | 79 | 81 | 84 | 86 | 88 | 91 | 93 | 95 | 98 | 100 | 102 | 104 | 106 | 109 | 112 | 115 | 117 | 121 | 123 | 125 | 127 | 130 | 132 | 134 | 136 | 138 | 141 | 143 | 145 | 147 | 149 | 151 | 153 | 155 | 157 | 159 | 162 | 165 | 167 | 169 | 172 | 174 | 177 | 179 | 181 | 183 | 185 | 189 | 191 | 194 | 196 | 198 | 200 | 202 | 205 | 207 | 209 | 211 | 213 | 215 | 218 | 220 | 222 | 224 | 226 | 228 | 230 | 232 | 234 | 236 | 238 | 241 | 243 | 245 | 247 | 249 | 251 | 253 | 256 | 259 | 261 | 263 | 265 | 267 | 269 | 272 | 274 | 276 | 280 | 282 | 285 | 287 | 289 | 292 | 295 | 298 | 300 | 302 | 304 | 306 | 309 | 312 | 314 | 316 | 318 | 320 | 322 | 324 | 326 | 330 | 334 | 338 | 340 | 343 | 345 | 347 | 349 | 351 | 353 | 355 | 358 | 360 | 363 | 365 | 367 | 369 | 371 | 373 | 375 | 377 | 379 | 381 | 383 | 386 | 388 | 390 | 392 | 394 | 396 | 399 | 401 | 404 | 406 | 408 | 410 | 412 | 414 | 416 | 419 | 421 | 423 | 425 | 428 | 431 | 435 | 438 | 440 | 442 | 444 | 446 | 448 | 451 | 453 | 455 | 457 | 460 | 462 | 464 | 466 | 468 | 471 | 473 | 477 | 479 | 481 | 483 | 486 | 488 | 490 | 492 | 494 | 496 | 499 | 501 | 504 | 506 | 509 | 512 | 515 | 517 | 520 | 522 | 524 | 526 | 529 | 532 | 534 | 536 | 539 | 542 | 543 | 544 | -------------------------------------------------------------------------------- /src/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PassiveModding/BlazorDiscordOAuth2/0f8034f24476ff4e54f9a8847c652e4033199f9a/src/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /src/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PassiveModding/BlazorDiscordOAuth2/0f8034f24476ff4e54f9a8847c652e4033199f9a/src/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /src/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #0366d6; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | app { 18 | position: relative; 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | .top-row { 24 | height: 3.5rem; 25 | display: flex; 26 | align-items: center; 27 | } 28 | 29 | .main { 30 | flex: 1; 31 | } 32 | 33 | .main .top-row { 34 | background-color: #f7f7f7; 35 | border-bottom: 1px solid #d6d5d5; 36 | justify-content: flex-end; 37 | } 38 | 39 | .main .top-row > a { 40 | margin-left: 1.5rem; 41 | } 42 | 43 | .sidebar { 44 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 45 | } 46 | 47 | .sidebar .top-row { 48 | background-color: rgba(0,0,0,0.4); 49 | } 50 | 51 | .sidebar .navbar-brand { 52 | font-size: 1.1rem; 53 | } 54 | 55 | .sidebar .oi { 56 | width: 2rem; 57 | font-size: 1.1rem; 58 | vertical-align: text-top; 59 | top: -2px; 60 | } 61 | 62 | .nav-item { 63 | font-size: 0.9rem; 64 | padding-bottom: 0.5rem; 65 | } 66 | 67 | .nav-item:first-of-type { 68 | padding-top: 1rem; 69 | } 70 | 71 | .nav-item:last-of-type { 72 | padding-bottom: 1rem; 73 | } 74 | 75 | .nav-item a { 76 | color: #d7d7d7; 77 | border-radius: 4px; 78 | height: 3rem; 79 | display: flex; 80 | align-items: center; 81 | line-height: 3rem; 82 | } 83 | 84 | .nav-item a.active { 85 | background-color: rgba(255,255,255,0.25); 86 | color: white; 87 | } 88 | 89 | .nav-item a:hover { 90 | background-color: rgba(255,255,255,0.1); 91 | color: white; 92 | } 93 | 94 | .content { 95 | padding-top: 1.1rem; 96 | } 97 | 98 | .navbar-toggler { 99 | background-color: rgba(255, 255, 255, 0.1); 100 | } 101 | 102 | .valid.modified:not([type=checkbox]) { 103 | outline: 1px solid #26b050; 104 | } 105 | 106 | .invalid { 107 | outline: 1px solid red; 108 | } 109 | 110 | .validation-message { 111 | color: red; 112 | } 113 | 114 | @media (max-width: 767.98px) { 115 | .main .top-row { 116 | display: none; 117 | } 118 | } 119 | 120 | @media (min-width: 768px) { 121 | app { 122 | flex-direction: row; 123 | } 124 | 125 | .sidebar { 126 | width: 250px; 127 | height: 100vh; 128 | position: sticky; 129 | top: 0; 130 | } 131 | 132 | .main .top-row { 133 | position: sticky; 134 | top: 0; 135 | } 136 | 137 | .main > div { 138 | padding-left: 2rem !important; 139 | padding-right: 1.5rem !important; 140 | } 141 | 142 | .navbar-toggler { 143 | display: none; 144 | } 145 | 146 | .sidebar .collapse { 147 | /* Never collapse the sidebar for wide screens */ 148 | display: block; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PassiveModding/BlazorDiscordOAuth2/0f8034f24476ff4e54f9a8847c652e4033199f9a/src/wwwroot/favicon.ico --------------------------------------------------------------------------------