├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── ClientApp ├── boot-app.js ├── components │ ├── App.vue │ ├── Dashboard.vue │ └── TodoItem.vue ├── router.js └── store │ ├── actions.js │ ├── index.js │ └── mutations.js ├── Controllers ├── HomeController.cs └── TodoController.cs ├── LICENSE ├── Models └── TodoItemModel.cs ├── Program.cs ├── README.md ├── Services ├── FakeTodoItemService.cs ├── ITodoItemService.cs └── OktaTodoItemService.cs ├── Startup.cs ├── Views ├── Home │ └── Index.cshtml ├── Shared │ ├── Error.cshtml │ └── _Layout.cshtml ├── _ViewImports.cshtml └── _ViewStart.cshtml ├── Vue2Spa.csproj ├── Vue2Spa.sln ├── appsettings.json ├── global.json ├── package.json ├── web.config ├── webpack.config.js └── webpack.config.vendor.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": [ 4 | "transform-runtime", 5 | "transform-async-to-generator" 6 | ], 7 | "comments": false 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 8 | extends: 'standard', 9 | // required to lint *.vue files 10 | plugins: [ 11 | 'html' 12 | ], 13 | // add your custom rules here 14 | 'rules': { 15 | // allow paren-less arrow functions 16 | 'arrow-parens': 0, 17 | // allow async-await 18 | 'generator-star-spacing': 0, 19 | // allow debugger during development 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | 5 | /Properties/launchSettings.json 6 | 7 | package-lock.json 8 | 9 | ## Ignore Visual Studio temporary files, build results, and 10 | ## files generated by popular Visual Studio add-ons. 11 | 12 | # User-specific files 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | build/ 29 | bld/ 30 | bin/ 31 | Bin/ 32 | obj/ 33 | Obj/ 34 | 35 | # Visual Studio 2015 cache/options directory 36 | .vs/ 37 | /wwwroot/dist/** 38 | 39 | # Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 40 | !/wwwroot/dist/_placeholder.txt 41 | 42 | /yarn.lock 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUNIT 49 | *.VisualState.xml 50 | TestResult.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # DNX 58 | project.lock.json 59 | artifacts/ 60 | 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | 98 | # Visual Studio profiler 99 | *.psess 100 | *.vsp 101 | *.vspx 102 | *.sap 103 | 104 | # TFS 2012 Local Workspace 105 | $tf/ 106 | 107 | # Guidance Automation Toolkit 108 | *.gpState 109 | 110 | # ReSharper is a .NET coding add-in 111 | _ReSharper*/ 112 | *.[Rr]e[Ss]harper 113 | *.DotSettings.user 114 | 115 | # JustCode is a .NET coding add-in 116 | .JustCode 117 | 118 | # TeamCity is a build add-in 119 | _TeamCity* 120 | 121 | # DotCover is a Code Coverage Tool 122 | *.dotCover 123 | 124 | # NCrunch 125 | _NCrunch_* 126 | .*crunch*.local.xml 127 | nCrunchTemp_* 128 | 129 | # MightyMoose 130 | *.mm.* 131 | AutoTest.Net/ 132 | 133 | # Web workbench (sass) 134 | .sass-cache/ 135 | 136 | # Installshield output folder 137 | [Ee]xpress/ 138 | 139 | # DocProject is a documentation generator add-in 140 | DocProject/buildhelp/ 141 | DocProject/Help/*.HxT 142 | DocProject/Help/*.HxC 143 | DocProject/Help/*.hhc 144 | DocProject/Help/*.hhk 145 | DocProject/Help/*.hhp 146 | DocProject/Help/Html2 147 | DocProject/Help/html 148 | 149 | # Click-Once directory 150 | publish/ 151 | 152 | # Publish Web Output 153 | *.[Pp]ublish.xml 154 | *.azurePubxml 155 | # TODO: Comment the next line if you want to checkin your web deploy settings 156 | # but database connection strings (with potential passwords) will be unencrypted 157 | *.pubxml 158 | *.publishproj 159 | 160 | # NuGet Packages 161 | *.nupkg 162 | # The packages folder can be ignored because of Package Restore 163 | **/packages/* 164 | # except build/, which is used as an MSBuild target. 165 | !**/packages/build/ 166 | # Uncomment if necessary however generally it will be regenerated when needed 167 | #!**/packages/repositories.config 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Microsoft Azure ApplicationInsights config file 178 | ApplicationInsights.config 179 | 180 | # Windows Store app package directory 181 | AppPackages/ 182 | BundleArtifacts/ 183 | 184 | # Visual Studio cache files 185 | # files ending in .cache can be ignored 186 | *.[Cc]ache 187 | # but keep track of directories ending in .cache 188 | !*.[Cc]ache/ 189 | 190 | # Others 191 | ClientBin/ 192 | ~$* 193 | *~ 194 | *.dbmdl 195 | *.dbproj.schemaview 196 | *.pfx 197 | *.publishsettings 198 | orleans.codegen.cs 199 | 200 | # Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 201 | /node_modules/** 202 | !/node_modules/_placeholder.txt 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 | 250 | # FAKE - F# Make 251 | .fake/ 252 | 253 | .vscode/ 254 | -------------------------------------------------------------------------------- /ClientApp/boot-app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './components/App' 3 | import router from './router' 4 | import store from './store' 5 | import { sync } from 'vuex-router-sync' 6 | 7 | // Sync Vue router and the Vuex store 8 | sync(store, router) 9 | 10 | new Vue({ 11 | el: '#app', 12 | store, 13 | router, 14 | template: '', 15 | components: { App } 16 | }) 17 | -------------------------------------------------------------------------------- /ClientApp/components/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Log out 8 | 9 | 10 | 11 | Log in 12 | 13 | 14 | 15 | 16 | 17 | 60 | 61 | 99 | -------------------------------------------------------------------------------- /ClientApp/components/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome! 5 | Log in to view your to-do list. 6 | 7 | 8 | 9 | {{name}}, here's your to-do list 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | {{ remaining }} remaining 22 | 23 | 24 | 25 | 26 | 63 | 64 | 75 | -------------------------------------------------------------------------------- /ClientApp/components/TodoItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 31 | 32 | 55 | -------------------------------------------------------------------------------- /ClientApp/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import store from './store' 4 | import Dashboard from './components/Dashboard' 5 | // Import the Okta Vue SDK 6 | import Auth from '@okta/okta-vue' 7 | 8 | Vue.use(Router) 9 | 10 | // Add the $auth plugin from the Okta Vue SDK to the Vue instance 11 | Vue.use(Auth, { 12 | // Replace this with your Okta domain: 13 | issuer: 'https://{yourOktaDomain}.com/oauth2/default', 14 | // Replace this with the client ID of the Okta app you just created: 15 | client_id: '{clientId}', 16 | redirect_uri: 'http://localhost:5000/implicit/callback', 17 | scope: 'openid profile email' 18 | }) 19 | 20 | const router = new Router({ 21 | mode: 'history', 22 | base: __dirname, 23 | routes: [ 24 | { path: '/', component: Dashboard }, 25 | // Handle the redirect from Okta using the Okta Vue SDK 26 | { path: '/implicit/callback', component: Auth.handleCallback() }, 27 | ] 28 | }) 29 | 30 | // Check the authentication status before router transitions 31 | router.beforeEach(Vue.prototype.$auth.authRedirectGuard()) 32 | 33 | export default router 34 | -------------------------------------------------------------------------------- /ClientApp/store/actions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const addAuthHeader = async (auth) => { 4 | return { 5 | headers: { 'Authorization': 'Bearer ' + await auth.getAccessToken() } 6 | } 7 | } 8 | 9 | export const actions = { 10 | async getAllTodos({ commit }, data) { 11 | let response = await axios.get('/api/todo', await addAuthHeader(data.$auth)) 12 | 13 | if (response && response.data) { 14 | let updatedTodos = response.data 15 | commit('loadTodos', updatedTodos) 16 | } 17 | }, 18 | 19 | async addTodo({ dispatch }, data) { 20 | await axios.post( 21 | '/api/todo', 22 | { text: data.text }, 23 | await addAuthHeader(data.$auth)) 24 | 25 | await dispatch('getAllTodos', { $auth: data.$auth }) 26 | }, 27 | 28 | async toggleTodo({ dispatch }, data) { 29 | await axios.post( 30 | '/api/todo/' + data.id, 31 | { completed: data.completed }, 32 | await addAuthHeader(data.$auth)) 33 | 34 | await dispatch('getAllTodos', { $auth: data.$auth }) 35 | }, 36 | 37 | async deleteTodo({ dispatch }, data) { 38 | await axios.delete('/api/todo/' + data.id, await addAuthHeader(data.$auth)) 39 | await dispatch('getAllTodos', { $auth: data.$auth }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ClientApp/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { state, mutations } from './mutations' 4 | import { actions } from './actions' 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Vuex.Store({ 9 | state, 10 | mutations, 11 | actions 12 | }) 13 | -------------------------------------------------------------------------------- /ClientApp/store/mutations.js: -------------------------------------------------------------------------------- 1 | export const state = { 2 | todos: [] 3 | } 4 | 5 | export const mutations = { 6 | loadTodos(state, todos) { 7 | state.todos = todos || []; 8 | }, 9 | 10 | clearTodos(state) { 11 | state.todos = []; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace Vue2Spa.Controllers 8 | { 9 | public class HomeController : Controller 10 | { 11 | public IActionResult Index() 12 | { 13 | return View(); 14 | } 15 | 16 | public IActionResult Error() 17 | { 18 | return View(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Controllers/TodoController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Vue2Spa.Services; 8 | using Vue2Spa.Models; 9 | 10 | namespace Vue2Spa.Controllers 11 | { 12 | [Route("api/[controller]")] 13 | [Authorize] 14 | public class TodoController : Controller 15 | { 16 | private readonly ITodoItemService _todoItemService; 17 | 18 | public TodoController(ITodoItemService todoItemService) 19 | { 20 | _todoItemService = todoItemService; 21 | } 22 | 23 | // GET /api/todo 24 | [HttpGet] 25 | public async Task GetAllTodos() 26 | { 27 | var userId = User.Claims.FirstOrDefault(c => c.Type == "uid")?.Value; 28 | if (string.IsNullOrEmpty(userId)) return BadRequest(); 29 | 30 | var todos = await _todoItemService.GetItems(userId); 31 | var todosInReverseOrder = todos.Reverse(); 32 | 33 | return Ok(todosInReverseOrder); 34 | } 35 | 36 | // POST /api/todo 37 | [HttpPost] 38 | public async Task AddTodo([FromBody]TodoItemModel newTodo) 39 | { 40 | if (string.IsNullOrEmpty(newTodo?.Text)) return BadRequest(); 41 | 42 | var userId = User.Claims.FirstOrDefault(c => c.Type == "uid")?.Value; 43 | if (string.IsNullOrEmpty(userId)) return BadRequest(); 44 | 45 | await _todoItemService.AddItem(userId, newTodo.Text); 46 | 47 | return Ok(); 48 | } 49 | 50 | // POST /api/todo/{id} 51 | [HttpPost("{id}")] 52 | public async Task UpdateTodo(Guid id, [FromBody]TodoItemModel updatedData) 53 | { 54 | var userId = User.Claims.FirstOrDefault(c => c.Type == "uid")?.Value; 55 | if (string.IsNullOrEmpty(userId)) return BadRequest(); 56 | 57 | await _todoItemService.UpdateItem(userId, id, updatedData); 58 | 59 | return Ok(); 60 | } 61 | 62 | // DELETE /api/todo/{id} 63 | [HttpDelete("{id}")] 64 | public async Task DeleteTodo(Guid id) 65 | { 66 | var userId = User.Claims.FirstOrDefault(c => c.Type == "uid")?.Value; 67 | if (string.IsNullOrEmpty(userId)) return BadRequest(); 68 | 69 | try 70 | { 71 | await _todoItemService.DeleteItem(userId, id); 72 | } 73 | catch (Exception ex) 74 | { 75 | return BadRequest(ex.Message); 76 | } 77 | 78 | return Ok(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Okta 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Models/TodoItemModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Vue2Spa.Models 4 | { 5 | public class TodoItemModel 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string Text { get; set; } 10 | 11 | public bool Completed { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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.Hosting; 7 | using Microsoft.AspNetCore; 8 | 9 | namespace Vue2Spa 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | BuildWebHost(args).Run(); 16 | } 17 | 18 | public static IWebHost BuildWebHost(string[] args) => 19 | WebHost.CreateDefaultBuilder(args) 20 | .UseStartup() 21 | .Build(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core API with Vue UI 2 | 3 | This example shows how to build a simple to-do app with an ASP.NET Core backend API and a Vue.js frontend. 4 | 5 | Read [Build a Secure To-Do App with Vue, ASP.NET Core, and Okta](https://developer.okta.com/blog/2018/01/31/build-secure-todo-app-vuejs-aspnetcore) to see how this app was created. 6 | 7 | **Prerequisites:** [.NET Core 2.0](https://dot.net/core) and [Node.js](https://nodejs.org/). 8 | 9 | > [Okta](https://developer.okta.com/) has Authentication and User Management APIs that reduce development time with instant-on, scalable user infrastructure. Okta's intuitive API and expert support make it easy for developers to authenticate, manage and secure users and roles in any application. 10 | 11 | * [Getting started](#getting-started) 12 | * [Links](#links) 13 | * [Help](#help) 14 | * [License](#license) 15 | 16 | ## Getting started 17 | 18 | To install this example application, run the following commands: 19 | 20 | ```bash 21 | git clone https://github.com/oktadeveloper/okta-vuejs-aspnetcore-todo-example.git 22 | cd okta-vuejs-aspnetcore-todo-example 23 | ``` 24 | 25 | This will download a copy of the project. 26 | 27 | ### Create an application in Okta 28 | 29 | You will need to create an OpenID Connect application in Okta to to perform authentication. 30 | 31 | Log in to your Okta Developer account (or [sign up](https://developer.okta.com/signup/) if you don't have an account) and navigate to **Applications** > **Add Application**. Click **Single-Page App**, click **Next**, and give the app a name you'll remember. 32 | 33 | Change the Base URI to `http://localhost:5000`, and the login redirect URI to `http://localhost:5000/implicit/callback`. Click **Done**. 34 | 35 | #### Server configuration 36 | 37 | Set the issuer (authority) in `Startup.cs`. 38 | 39 | **Note:** The value of `{yourOktaDomain}` should be something like `dev-123456.oktapreview.com`. Make sure you don't include `-admin` in the value! 40 | 41 | ```csharp 42 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 43 | .AddJwtBearer(options => 44 | { 45 | options.Authority = "https://{yourOktaDomain}.com/oauth2/default"; 46 | options.Audience = "api://default"; 47 | }); 48 | ``` 49 | 50 | #### Client configuration 51 | 52 | Set the `issuer` and copy the `clientId` of the Okta application into `ClientApp/router.js`. 53 | 54 | ```javascript 55 | Vue.use(Auth, { 56 | // Replace this with your Okta domain: 57 | issuer: 'https://{yourOktaDomain}.com/oauth2/default', 58 | // Replace this with the client ID of the Okta app you just created: 59 | client_id: '{clientId}', 60 | redirect_uri: 'http://localhost:5000/implicit/callback', 61 | scope: 'openid profile email' 62 | }) 63 | ``` 64 | 65 | ### Start the app 66 | 67 | To install all of the dependencies and start the app, run: 68 | 69 | ```bash 70 | npm install 71 | 72 | dotnet run 73 | ``` 74 | 75 | ## Links 76 | 77 | This example uses the following libraries provided by Okta: 78 | 79 | * [Okta Vue SDK](https://github.com/okta/okta-oidc-js/tree/master/packages/okta-vue) 80 | * [Okta .NET SDK](https://github.com/okta/okta-sdk-dotnet) 81 | 82 | ## Help 83 | 84 | Please post any questions as comments on the [blog post](https://developer.okta.com/blog/2018/01/31/build-secure-todo-app-vuejs-aspnetcore), or visit the [Okta Developer Forums](https://devforum.okta.com/). You can also email developers@okta.com if you would like to create a support ticket. 85 | 86 | ## License 87 | 88 | MIT, see [LICENSE](LICENSE). 89 | -------------------------------------------------------------------------------- /Services/FakeTodoItemService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Vue2Spa.Models; 6 | 7 | namespace Vue2Spa.Services 8 | { 9 | public class FakeTodoItemService : ITodoItemService 10 | { 11 | public Task> GetItems(string userId) 12 | { 13 | var todos = new[] 14 | { 15 | new TodoItemModel { Text = "Learn Vue.js", Completed = true }, 16 | new TodoItemModel { Text = "Learn ASP.NET Core" } 17 | }; 18 | 19 | return Task.FromResult(todos.AsEnumerable()); 20 | } 21 | 22 | public Task AddItem(string userId, string text) 23 | { 24 | throw new NotImplementedException(); 25 | } 26 | 27 | public Task DeleteItem(string userId, Guid id) 28 | { 29 | throw new NotImplementedException(); 30 | } 31 | 32 | public Task UpdateItem(string userId, Guid id, TodoItemModel updatedData) 33 | { 34 | throw new NotImplementedException(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Services/ITodoItemService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Vue2Spa.Models; 6 | 7 | namespace Vue2Spa.Services 8 | { 9 | public interface ITodoItemService 10 | { 11 | Task> GetItems(string userId); 12 | 13 | Task AddItem(string userId, string text); 14 | 15 | Task UpdateItem(string userId, Guid id, TodoItemModel updatedData); 16 | 17 | Task DeleteItem(string userId, Guid id); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Services/OktaTodoItemService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using Okta.Sdk; 7 | using Vue2Spa.Models; 8 | 9 | namespace Vue2Spa.Services 10 | { 11 | public class OktaTodoItemService : ITodoItemService 12 | { 13 | private const string TodoProfileKey = "todos"; 14 | 15 | private readonly IOktaClient _oktaClient; 16 | 17 | public OktaTodoItemService(IOktaClient oktaClient) 18 | { 19 | _oktaClient = oktaClient; 20 | } 21 | 22 | private IEnumerable GetItemsFromProfile(IUser oktaUser) 23 | { 24 | if (oktaUser == null) 25 | { 26 | return Enumerable.Empty(); 27 | } 28 | 29 | var json = oktaUser.Profile.GetProperty(TodoProfileKey); 30 | if (string.IsNullOrEmpty(json)) 31 | { 32 | return Enumerable.Empty(); 33 | } 34 | 35 | return JsonConvert.DeserializeObject(json); 36 | } 37 | 38 | private async Task SaveItemsToProfile(IUser user, IEnumerable todos) 39 | { 40 | var json = JsonConvert.SerializeObject(todos.ToArray()); 41 | 42 | user.Profile[TodoProfileKey] = json; 43 | await user.UpdateAsync(); 44 | } 45 | 46 | public async Task AddItem(string userId, string text) 47 | { 48 | var user = await _oktaClient.Users.GetUserAsync(userId); 49 | 50 | var existingItems = GetItemsFromProfile(user) 51 | .ToList(); 52 | 53 | existingItems.Add(new TodoItemModel 54 | { 55 | Id = Guid.NewGuid(), 56 | Completed = false, 57 | Text = text 58 | }); 59 | 60 | await SaveItemsToProfile(user, existingItems); 61 | } 62 | 63 | public async Task DeleteItem(string userId, Guid id) 64 | { 65 | var user = await _oktaClient.Users.GetUserAsync(userId); 66 | 67 | var updatedItems = GetItemsFromProfile(user) 68 | .Where(item => item.Id != id); 69 | 70 | await SaveItemsToProfile(user, updatedItems); 71 | } 72 | 73 | public async Task> GetItems(string userId) 74 | { 75 | var user = await _oktaClient.Users.GetUserAsync(userId); 76 | return GetItemsFromProfile(user); 77 | } 78 | 79 | public async Task UpdateItem(string userId, Guid id, TodoItemModel updatedData) 80 | { 81 | var user = await _oktaClient.Users.GetUserAsync(userId); 82 | 83 | var existingItems = GetItemsFromProfile(user) 84 | .ToList(); 85 | 86 | var itemToUpdate = existingItems 87 | .FirstOrDefault(item => item.Id == id); 88 | if (itemToUpdate == null) 89 | { 90 | return; 91 | } 92 | 93 | // Update the item with the new data 94 | itemToUpdate.Completed = updatedData.Completed; 95 | if (!string.IsNullOrEmpty(updatedData.Text)) 96 | { 97 | itemToUpdate.Text = updatedData.Text; 98 | } 99 | 100 | await SaveItemsToProfile(user, existingItems); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.SpaServices.Webpack; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.AspNetCore.Authentication.JwtBearer; 12 | using Vue2Spa.Services; 13 | using Okta.Sdk; 14 | using Okta.Sdk.Configuration; 15 | 16 | namespace Vue2Spa 17 | { 18 | public class Startup 19 | { 20 | public Startup(IHostingEnvironment env) 21 | { 22 | var builder = new ConfigurationBuilder() 23 | .SetBasePath(env.ContentRootPath) 24 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 25 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 26 | .AddEnvironmentVariables(); 27 | 28 | if (env.IsDevelopment()) 29 | { 30 | builder.AddUserSecrets(); 31 | } 32 | 33 | Configuration = builder.Build(); 34 | } 35 | 36 | public IConfigurationRoot Configuration { get; } 37 | 38 | // This method gets called by the runtime. Use this method to add services to the container. 39 | public void ConfigureServices(IServiceCollection services) 40 | { 41 | // Add framework services. 42 | services.AddMvc(); 43 | 44 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 45 | .AddJwtBearer(options => 46 | { 47 | options.Authority = "https://{yourOktaDomain}.com/oauth2/default"; 48 | options.Audience = "api://default"; 49 | }); 50 | 51 | services.AddSingleton(); 52 | 53 | services.AddSingleton(new OktaClient(new OktaClientConfiguration 54 | { 55 | OrgUrl = "https://{yourOktaDomain}.com", 56 | Token = Configuration["okta:token"] 57 | })); 58 | } 59 | 60 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 61 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 62 | { 63 | loggerFactory.AddConsole(Configuration.GetSection("Logging")); 64 | loggerFactory.AddDebug(); 65 | 66 | if (env.IsDevelopment()) 67 | { 68 | app.UseDeveloperExceptionPage(); 69 | app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions 70 | { 71 | HotModuleReplacement = true 72 | }); 73 | } 74 | else 75 | { 76 | app.UseExceptionHandler("/Home/Error"); 77 | } 78 | 79 | app.UseStaticFiles(); 80 | 81 | app.UseAuthentication(); 82 | 83 | app.UseMvc(routes => 84 | { 85 | routes.MapRoute( 86 | name: "default", 87 | template: "{controller=Home}/{action=Index}/{id?}"); 88 | 89 | routes.MapSpaFallbackRoute( 90 | name: "spa-fallback", 91 | defaults: new { controller = "Home", action = "Index" }); 92 | }); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ ViewData["Title"] = "Home Page"; } 2 | 3 | 4 | 5 | @section scripts { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 | Error. 6 | An error occurred while processing your request. 7 | -------------------------------------------------------------------------------- /Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | @ViewData["Title"] - aspnetcore_Vue_starter 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @RenderBody() 17 | 18 | 19 | @RenderSection("scripts", required: false) 20 | 21 | 22 | -------------------------------------------------------------------------------- /Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Vue2Spa 2 | @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" 3 | @addTagHelper "*, Microsoft.AspNetCore.SpaServices" 4 | -------------------------------------------------------------------------------- /Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /Vue2Spa.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | c487bf6c-f065-41ae-b5db-af52da19fc0d 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | %(DistFiles.Identity) 30 | PreserveNewest 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Vue2Spa.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26228.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vue2Spa", "Vue2Spa.csproj", "{B5FE715F-AB99-4FA6-AEC1-D3B644C76DF7}" 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 | {B5FE715F-AB99-4FA6-AEC1-D3B644C76DF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {B5FE715F-AB99-4FA6-AEC1-D3B644C76DF7}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {B5FE715F-AB99-4FA6-AEC1-D3B644C76DF7}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {B5FE715F-AB99-4FA6-AEC1-D3B644C76DF7}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": {}, 3 | "Logging": { 4 | "IncludeScopes": false, 5 | "LogLevel": { 6 | "Default": "Debug", 7 | "System": "Information", 8 | "Microsoft": "Information" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { "version": "2.0.0" } 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aspnetcore-vuejs", 3 | "description": "ASP.NET Core & VueJS Starter project", 4 | "author": "Mark Pieszak", 5 | "scripts": { 6 | "dev": "cross-env ASPNETCORE_ENVIRONMENT=Development NODE_ENV=development dotnet run", 7 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", 8 | "install": "webpack --config webpack.config.vendor.js" 9 | }, 10 | "dependencies": { 11 | "@okta/okta-vue": "0.0.3", 12 | "axios": "^0.17.1", 13 | "core-js": "^2.4.1", 14 | "font-awesome": "^4.6.3", 15 | "vue": "^2.5.2", 16 | "vue-router": "^3.0.1", 17 | "vue-server-renderer": "^2.1.8", 18 | "vue-template-compiler": "^2.5.2", 19 | "vuex": "^3.0.1", 20 | "vuex-router-sync": "^5.0.0" 21 | }, 22 | "devDependencies": { 23 | "aspnet-webpack": "^2.0.1", 24 | "babel-core": "^6.21.0", 25 | "babel-loader": "^6.2.10", 26 | "babel-plugin-transform-async-to-generator": "^6.22.0", 27 | "babel-plugin-transform-runtime": "^6.15.0", 28 | "babel-preset-es2015": "^6.18.0", 29 | "babel-preset-stage-2": "^6.18.0", 30 | "babel-register": "^6.18.0", 31 | "bootstrap": "^3.3.6", 32 | "cross-env": "^3.1.3", 33 | "css-loader": "^0.26.1", 34 | "event-source-polyfill": "^0.0.7", 35 | "extract-text-webpack-plugin": "^2.0.0-rc", 36 | "file-loader": "^0.9.0", 37 | "jquery": "^2.2.1", 38 | "node-sass": "^4.1.0", 39 | "optimize-css-assets-webpack-plugin": "^1.3.1", 40 | "sass-loader": "^4.1.0", 41 | "style-loader": "^0.13.1", 42 | "url-loader": "^0.5.7", 43 | "vue-loader": "^10.0.2", 44 | "webpack": "^2.2.0", 45 | "webpack-hot-middleware": "^2.12.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | const bundleOutputDir = './wwwroot/dist'; 5 | 6 | module.exports = (env) => { 7 | const isDevBuild = !(env && env.prod); 8 | return [{ 9 | stats: { modules: false }, 10 | entry: { 'main': './ClientApp/boot-app.js' }, 11 | resolve: { 12 | extensions: ['.js', '.vue'], 13 | alias: { 14 | 'vue$': 'vue/dist/vue', 15 | 'components': path.resolve(__dirname, './ClientApp/components'), 16 | 'views': path.resolve(__dirname, './ClientApp/views'), 17 | 'utils': path.resolve(__dirname, './ClientApp/utils'), 18 | 'api': path.resolve(__dirname, './ClientApp/store/api') 19 | } 20 | }, 21 | output: { 22 | path: path.join(__dirname, bundleOutputDir), 23 | filename: '[name].js', 24 | publicPath: '/dist/' 25 | }, 26 | module: { 27 | rules: [ 28 | { test: /\.vue$/, include: /ClientApp/, use: 'vue-loader' }, 29 | { test: /\.js$/, include: /ClientApp/, use: 'babel-loader' }, 30 | { test: /\.css$/, use: isDevBuild ? ['style-loader', 'css-loader'] : ExtractTextPlugin.extract({ use: 'css-loader' }) }, 31 | { test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' } 32 | ] 33 | }, 34 | plugins: [ 35 | new webpack.DllReferencePlugin({ 36 | context: __dirname, 37 | manifest: require('./wwwroot/dist/vendor-manifest.json') 38 | }) 39 | ].concat(isDevBuild ? [ 40 | // Plugins that apply in development builds only 41 | new webpack.SourceMapDevToolPlugin({ 42 | filename: '[file].map', // Remove this line if you prefer inline source maps 43 | moduleFilenameTemplate: path.relative(bundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk 44 | }) 45 | ] : [ 46 | // Plugins that apply in production builds only 47 | new webpack.optimize.UglifyJsPlugin(), 48 | new ExtractTextPlugin('site.css') 49 | ]) 50 | }]; 51 | }; 52 | -------------------------------------------------------------------------------- /webpack.config.vendor.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin'); 5 | 6 | module.exports = (env) => { 7 | const extractCSS = new ExtractTextPlugin('vendor.css'); 8 | const isDevBuild = !(env && env.prod); 9 | return [{ 10 | stats: { modules: false }, 11 | resolve: { 12 | extensions: ['.js'] 13 | }, 14 | module: { 15 | rules: [ 16 | { test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, use: 'url-loader?limit=100000' }, 17 | { test: /\.css(\?|$)/, use: extractCSS.extract(['css-loader']) } 18 | ] 19 | }, 20 | entry: { 21 | vendor: ['bootstrap', 'bootstrap/dist/css/bootstrap.css', 'event-source-polyfill', 'vue', 'vuex', 'axios', 'vue-router', 'jquery'], 22 | }, 23 | output: { 24 | path: path.join(__dirname, 'wwwroot', 'dist'), 25 | publicPath: '/dist/', 26 | filename: '[name].js', 27 | library: '[name]_[hash]', 28 | }, 29 | plugins: [ 30 | extractCSS, 31 | // Compress extracted CSS. 32 | new OptimizeCSSPlugin({ 33 | cssProcessorOptions: { 34 | safe: true 35 | } 36 | }), 37 | new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' }), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable) 38 | new webpack.DllPlugin({ 39 | path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'), 40 | name: '[name]_[hash]' 41 | }), 42 | new webpack.DefinePlugin({ 43 | 'process.env.NODE_ENV': isDevBuild ? '"development"' : '"production"' 44 | }) 45 | ].concat(isDevBuild ? [] : [ 46 | new webpack.optimize.UglifyJsPlugin() 47 | ]) 48 | }]; 49 | }; 50 | --------------------------------------------------------------------------------
Log in to view your to-do list.
{{ remaining }} remaining