├── global.json
├── Views
├── _ViewStart.cshtml
├── _ViewImports.cshtml
├── Home
│ └── Index.cshtml
└── Shared
│ ├── Error.cshtml
│ └── _Layout.cshtml
├── .babelrc
├── .editorconfig
├── appsettings.json
├── ClientApp
├── store
│ ├── mutations.js
│ ├── index.js
│ └── actions.js
├── boot-app.js
├── router.js
└── components
│ ├── TodoItem.vue
│ ├── Dashboard.vue
│ └── App.vue
├── Models
└── TodoItemModel.cs
├── Controllers
├── HomeController.cs
└── TodoController.cs
├── Services
├── ITodoItemService.cs
├── FakeTodoItemService.cs
└── OktaTodoItemService.cs
├── web.config
├── Program.cs
├── .eslintrc.js
├── Vue2Spa.sln
├── LICENSE
├── package.json
├── Vue2Spa.csproj
├── webpack.config.vendor.js
├── webpack.config.js
├── README.md
├── Startup.cs
└── .gitignore
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": { "version": "2.0.0" }
3 | }
4 |
--------------------------------------------------------------------------------
/Views/_ViewStart.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | Layout = "_Layout";
3 | }
4 |
--------------------------------------------------------------------------------
/Views/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @using Vue2Spa
2 | @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
3 | @addTagHelper "*, Microsoft.AspNetCore.SpaServices"
4 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2"],
3 | "plugins": [
4 | "transform-runtime",
5 | "transform-async-to-generator"
6 | ],
7 | "comments": false
8 | }
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/components/TodoItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
31 |
32 |
55 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
20 |
21 | {{ remaining }} remaining
22 |
23 |
24 |
25 |
26 |
63 |
64 |
75 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ClientApp/components/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
60 |
61 |
99 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------