├── .deploy ├── docker-compose.yml └── nginx-proxy-compose.yml ├── .dockerignore ├── .github └── workflows │ ├── README.md │ ├── build.yml │ └── release.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── MyApp.ServiceInterface ├── MyApp.ServiceInterface.csproj └── MyServices.cs ├── MyApp.ServiceModel ├── Hello.cs ├── MyApp.ServiceModel.csproj └── Types │ └── README.md ├── MyApp.Tests ├── IntegrationTest.cs ├── MyApp.Tests.csproj └── UnitTest.cs ├── MyApp.sln ├── MyApp ├── App_Data │ └── README.md ├── Configure.AppHost.cs ├── Configure.Auth.cs ├── Configure.AuthRepository.cs ├── MyApp.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json ├── appsettings.json ├── cypress.json ├── npm-shrinkwrap.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── svg │ │ ├── app │ │ └── logo.svg │ │ └── svg-icons │ │ └── home.svg ├── src │ ├── App.vue │ ├── app.css │ ├── components │ │ ├── About.vue │ │ ├── Admin │ │ │ └── index.vue │ │ ├── Home │ │ │ ├── HelloApi.vue │ │ │ └── index.vue │ │ ├── Profile.vue │ │ ├── SignIn.vue │ │ └── SignUp.vue │ ├── main.ts │ ├── shared │ │ ├── dtos.ts │ │ ├── index.ts │ │ └── router.ts │ ├── shims-tsx.d.ts │ └── shims-vue.d.ts ├── tests │ ├── setup.js │ └── unit │ │ ├── About.spec.ts │ │ └── Home.spec.ts ├── tsconfig.json ├── tslint.json ├── vue.config.js └── wwwroot │ └── index.html ├── README.md └── jsconfig.json /.deploy/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | app: 4 | image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION} 5 | restart: always 6 | ports: 7 | - "8080" 8 | container_name: ${APP_NAME}_app 9 | environment: 10 | VIRTUAL_HOST: ${HOST_DOMAIN} 11 | VIRTUAL_PORT: 8080 # New default ASP.NET port -> https://learn.microsoft.com/en-us/dotnet/core/compatibility/containers/8.0/aspnet-port 12 | LETSENCRYPT_HOST: ${HOST_DOMAIN} 13 | LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL} 14 | volumes: 15 | - app-mydb:/app/App_Data 16 | 17 | networks: 18 | default: 19 | external: true 20 | name: nginx 21 | 22 | volumes: 23 | app-mydb: 24 | -------------------------------------------------------------------------------- /.deploy/nginx-proxy-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | nginx-proxy: 5 | image: nginxproxy/nginx-proxy 6 | container_name: nginx-proxy 7 | restart: always 8 | ports: 9 | - "80:80" 10 | - "443:443" 11 | volumes: 12 | - conf:/etc/nginx/conf.d 13 | - vhost:/etc/nginx/vhost.d 14 | - html:/usr/share/nginx/html 15 | - dhparam:/etc/nginx/dhparam 16 | - certs:/etc/nginx/certs:ro 17 | - /var/run/docker.sock:/tmp/docker.sock:ro 18 | labels: 19 | - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy" 20 | 21 | letsencrypt: 22 | image: nginxproxy/acme-companion:2.2 23 | container_name: nginx-proxy-le 24 | restart: always 25 | depends_on: 26 | - "nginx-proxy" 27 | environment: 28 | - DEFAULT_EMAIL=you@example.com 29 | volumes: 30 | - certs:/etc/nginx/certs:rw 31 | - acme:/etc/acme.sh 32 | - vhost:/etc/nginx/vhost.d 33 | - html:/usr/share/nginx/html 34 | - /var/run/docker.sock:/var/run/docker.sock:ro 35 | 36 | networks: 37 | default: 38 | name: nginx 39 | 40 | volumes: 41 | conf: 42 | vhost: 43 | html: 44 | dhparam: 45 | certs: 46 | acme: -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | */npm-debug.log 3 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This template uses the deployment configurations for a ServiceStack .NET 8 application. The application is containerized using Docker and is set up to be automatically built and deployed via GitHub Actions. The recommended deployment target is a stand-alone Linux server running Ubuntu, with an NGINX reverse proxy also containerized using Docker, which a Docker Compose file is included in the template under the `.deploy` directory. 4 | 5 | ### Highlights 6 | - 🌐 **NGINX Reverse Proxy**: Utilizes an NGINX reverse proxy to handle web traffic and SSL termination. 7 | - 🚀 **GitHub Actions**: Leverages GitHub Actions for CI/CD, pushing Docker images to GitHub Container Registry and deploying them on a remote server. 8 | - 🐳 **Dockerized ServiceStack App**: The application is containerized, with the image built using `.NET 8`. 9 | - 🔄 **Automated Migrations**: Includes a separate service for running database migrations. 10 | 11 | ### Technology Stack 12 | - **Web Framework**: ServiceStack 13 | - **Language**: C# (.NET 8) 14 | - **Containerization**: Docker 15 | - **Reverse Proxy**: NGINX 16 | - **CI/CD**: GitHub Actions 17 | - **OS**: Ubuntu 22.04 (Deployment Server) 18 | 19 | 20 | 21 | ## Deployment Server Setup 22 | 23 | To successfully host your ServiceStack applications, there are several components you need to set up on your deployment server. This guide assumes you're working on a standalone Linux server (Ubuntu is recommended) with SSH access enabled. 24 | 25 | ### Prerequisites 26 | 27 | 1. **SSH Access**: Required for GitHub Actions to communicate with your server. 28 | 2. **Docker**: To containerize your application. 29 | 3. **Docker-Compose**: For orchestrating multiple containers. 30 | 4. **Ports**: 80 and 443 should be open for web access. 31 | 5. **nginx-reverse-proxy**: For routing traffic to multiple ServiceStack applications and managing TLS certificates. 32 | 33 | You can use any cloud-hosted or on-premises server like Digital Ocean, AWS, Azure, etc., for this setup. 34 | 35 | ### Step-by-Step Guide 36 | 37 | #### 1. Install Docker and Docker-Compose 38 | 39 | It is best to follow the [latest installation instructions on the Docker website](https://docs.docker.com/engine/install/ubuntu/) to ensure to have the correct setup with the latest patches. 40 | 41 | #### 2. Configure SSH for GitHub Actions 42 | 43 | Generate a dedicated SSH key pair to be used by GitHub Actions: 44 | 45 | ```bash 46 | ssh-keygen -t rsa -b 4096 -f ~/.ssh/github_actions 47 | ``` 48 | 49 | Add the public key to the `authorized_keys` file on your server: 50 | 51 | ```bash 52 | cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys 53 | ``` 54 | 55 | Then, add the *private* key to your GitHub Secrets as `DEPLOY_KEY` to enable GitHub Actions to SSH into the server securely. 56 | 57 | #### 3. Set Up nginx-reverse-proxy 58 | 59 | You should have a `docker-compose` file similar to the `nginx-proxy-compose.yml` in your repository. Upload this file to your server: 60 | 61 | ```bash 62 | scp nginx-proxy-compose.yml user@your_server:~/ 63 | ``` 64 | 65 | To bring up the nginx reverse proxy and its companion container for handling TLS certificates, run: 66 | 67 | ```bash 68 | docker compose -f ~/nginx-proxy-compose.yml up -d 69 | ``` 70 | 71 | This will start an nginx reverse proxy along with a companion container. They will automatically watch for additional Docker containers on the same network and initialize them with valid TLS certificates. 72 | 73 | 74 | 75 | ## GitHub Repository Setup 76 | 77 | Configuring your GitHub repository is an essential step for automating deployments via GitHub Actions. This guide assumes you have a `release.yml` workflow file in your repository's `.github/workflows/` directory, and your deployment server has been set up according to the [Deployment Server Setup](#Deployment-Server-Setup) guidelines. 78 | 79 | ### Secrets Configuration 80 | 81 | Your GitHub Actions workflow requires the following secrets to be set in your GitHub repository: 82 | 83 | 1. **`DEPLOY_HOST`**: The hostname for SSH access. This can be either an IP address or a domain with an A-record pointing to your server. 84 | 2. **`DEPLOY_USERNAME`**: The username for SSH login. Common examples include `ubuntu`, `ec2-user`, or `root`. 85 | 3. **`DEPLOY_KEY`**: The SSH private key to securely access the deployment server. This should be the same key you've set up on your server for GitHub Actions. 86 | 4. **`LETSENCRYPT_EMAIL`**: Your email address, required for Let's Encrypt automated TLS certificates. 87 | 88 | #### Using GitHub CLI for Secret Management 89 | 90 | You can conveniently set these secrets using the [GitHub CLI](https://cli.github.com/manual/gh_secret_set) like this: 91 | 92 | ```bash 93 | gh secret set DEPLOY_HOST --body="your-host-or-ip" 94 | gh secret set DEPLOY_USERNAME --body="your-username" 95 | gh secret set DEPLOY_KEY --bodyFile="path/to/your/ssh-private-key" 96 | gh secret set LETSENCRYPT_EMAIL --body="your-email@example.com" 97 | ``` 98 | 99 | These secrets will populate environment variables within your GitHub Actions workflow and other configuration files, enabling secure and automated deployment of your ServiceStack applications. 100 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - '**' # matches every branch 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Setup dotnet 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: '8.0' 20 | 21 | - name: build 22 | run: dotnet build 23 | working-directory: . 24 | 25 | - name: test 26 | run: | 27 | dotnet test 28 | if [ $? -eq 0 ]; then 29 | echo TESTS PASSED 30 | else 31 | echo TESTS FAILED 32 | exit 1 33 | fi 34 | working-directory: ./MyApp.Tests 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | permissions: 3 | packages: write 4 | contents: write 5 | on: 6 | # Triggered on new GitHub Release 7 | release: 8 | types: [published] 9 | # Triggered on every successful Build action 10 | workflow_run: 11 | workflows: ["Build"] 12 | branches: [main,master] 13 | types: 14 | - completed 15 | # Manual trigger for rollback to specific release or redeploy latest 16 | workflow_dispatch: 17 | inputs: 18 | version: 19 | default: latest 20 | description: Tag you want to release. 21 | required: true 22 | 23 | jobs: 24 | push_to_registry: 25 | runs-on: ubuntu-22.04 26 | if: ${{ github.event.workflow_run.conclusion != 'failure' }} 27 | steps: 28 | # Checkout latest or specific tag 29 | - name: checkout 30 | if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }} 31 | uses: actions/checkout@v3 32 | - name: checkout tag 33 | if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }} 34 | uses: actions/checkout@v3 35 | with: 36 | ref: refs/tags/${{ github.event.inputs.version }} 37 | 38 | # Assign environment variables used in subsequent steps 39 | - name: Env variable assignment 40 | run: echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV 41 | # TAG_NAME defaults to 'latest' if not a release or manual deployment 42 | - name: Assign version 43 | run: | 44 | echo "TAG_NAME=latest" >> $GITHUB_ENV 45 | if [ "${{ github.event.release.tag_name }}" != "" ]; then 46 | echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV 47 | fi; 48 | if [ "${{ github.event.inputs.version }}" != "" ]; then 49 | echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV 50 | fi; 51 | if [ ! -z "${{ secrets.APPSETTINGS_PATCH }}" ]; then 52 | echo "HAS_APPSETTINGS_PATCH=true" >> $GITHUB_ENV 53 | else 54 | echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV 55 | fi; 56 | 57 | - name: Login to GitHub Container Registry 58 | uses: docker/login-action@v2 59 | with: 60 | registry: ghcr.io 61 | username: ${{ github.actor }} 62 | password: ${{ secrets.GITHUB_TOKEN }} 63 | 64 | 65 | - name: Setup dotnet 66 | uses: actions/setup-dotnet@v3 67 | with: 68 | dotnet-version: '8.0' 69 | 70 | - name: Install x tool 71 | if: env.HAS_APPSETTINGS_PATCH == 'true' 72 | run: dotnet tool install -g x 73 | 74 | - name: Apply Production AppSettings 75 | if: env.HAS_APPSETTINGS_PATCH == 'true' 76 | working-directory: ./MyApp 77 | run: | 78 | cat <> appsettings.json.patch 79 | ${{ secrets.APPSETTINGS_PATCH }} 80 | EOF 81 | x patch appsettings.json.patch 82 | 83 | 84 | # Build and push new docker image, skip for manual redeploy other than 'latest' 85 | - name: Build and push Docker image 86 | run: | 87 | dotnet publish --os linux --arch x64 -c Release -p:ContainerRepository=${{ env.image_repository_name }} -p:ContainerRegistry=ghcr.io -p:ContainerImageTags=${{ env.TAG_NAME }} -p:ContainerPort=80 88 | 89 | deploy_via_ssh: 90 | needs: push_to_registry 91 | runs-on: ubuntu-22.04 92 | if: ${{ github.event.workflow_run.conclusion != 'failure' }} 93 | steps: 94 | # Checkout latest or specific tag 95 | - name: checkout 96 | if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }} 97 | uses: actions/checkout@v3 98 | - name: checkout tag 99 | if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }} 100 | uses: actions/checkout@v3 101 | with: 102 | ref: refs/tags/${{ github.event.inputs.version }} 103 | 104 | - name: repository name fix and env 105 | run: | 106 | echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV 107 | echo "TAG_NAME=latest" >> $GITHUB_ENV 108 | if [ "${{ github.event.release.tag_name }}" != "" ]; then 109 | echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV 110 | fi; 111 | if [ "${{ github.event.inputs.version }}" != "" ]; then 112 | echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV 113 | fi; 114 | 115 | - name: Create .env file 116 | run: | 117 | echo "Generating .env file" 118 | 119 | echo "# Autogenerated .env file" > .deploy/.env 120 | echo "HOST_DOMAIN=${{ secrets.DEPLOY_HOST }}" >> .deploy/.env 121 | echo "LETSENCRYPT_EMAIL=${{ secrets.LETSENCRYPT_EMAIL }}" >> .deploy/.env 122 | echo "APP_NAME=${{ github.event.repository.name }}" >> .deploy/.env 123 | echo "IMAGE_REPO=${{ env.image_repository_name }}" >> .deploy/.env 124 | echo "RELEASE_VERSION=${{ env.TAG_NAME }}" >> .deploy/.env 125 | 126 | # Copy only the docker-compose.yml to remote server home folder 127 | - name: copy files to target server via scp 128 | uses: appleboy/scp-action@v0.1.3 129 | with: 130 | host: ${{ secrets.DEPLOY_HOST }} 131 | username: ${{ secrets.DEPLOY_USERNAME }} 132 | port: 22 133 | key: ${{ secrets.DEPLOY_KEY }} 134 | strip_components: 2 135 | source: "./.deploy/docker-compose.yml,./.deploy/.env" 136 | target: "~/.deploy/${{ github.event.repository.name }}/" 137 | 138 | - name: Setup App_Data volume directory 139 | uses: appleboy/ssh-action@v0.1.5 140 | env: 141 | APPTOKEN: ${{ secrets.GITHUB_TOKEN }} 142 | USERNAME: ${{ secrets.DEPLOY_USERNAME }} 143 | with: 144 | host: ${{ secrets.DEPLOY_HOST }} 145 | username: ${{ secrets.DEPLOY_USERNAME }} 146 | key: ${{ secrets.DEPLOY_KEY }} 147 | port: 22 148 | envs: APPTOKEN,USERNAME 149 | script: | 150 | set -e 151 | echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin 152 | cd ~/.deploy/${{ github.event.repository.name }} 153 | docker compose pull 154 | export APP_ID=$(docker compose run --entrypoint "id -u" --rm app) 155 | docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/App_Data" --user root --rm app 156 | 157 | # Deploy Docker image with your application using `docker compose up` remotely 158 | - name: remote docker-compose up via ssh 159 | uses: appleboy/ssh-action@v0.1.5 160 | env: 161 | APPTOKEN: ${{ secrets.GITHUB_TOKEN }} 162 | USERNAME: ${{ secrets.DEPLOY_USERNAME }} 163 | with: 164 | host: ${{ secrets.DEPLOY_HOST }} 165 | username: ${{ secrets.DEPLOY_USERNAME }} 166 | key: ${{ secrets.DEPLOY_KEY }} 167 | port: 22 168 | envs: APPTOKEN,USERNAME 169 | script: | 170 | echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin 171 | cd ~/.deploy/${{ github.event.repository.name }} 172 | docker compose pull 173 | docker compose up app -d 174 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # Custom 7 | .DS_Store 8 | dist/ 9 | wwwroot/ 10 | coverage/ 11 | /tests/e2e/videos/ 12 | /tests/e2e/screenshots/ 13 | 14 | # local env files 15 | .env.local 16 | .env.*.local 17 | 18 | # User-specific files 19 | *.suo 20 | *.user 21 | *.userosscache 22 | *.sln.docstates 23 | 24 | # User-specific files (MonoDevelop/Xamarin Studio) 25 | *.userprefs 26 | 27 | # Build results 28 | [Dd]ebug/ 29 | [Dd]ebugPublic/ 30 | [Rr]elease/ 31 | [Rr]eleases/ 32 | x64/ 33 | x86/ 34 | bld/ 35 | [Bb]in/ 36 | [Oo]bj/ 37 | [Ll]og/ 38 | 39 | # Log files 40 | npm-debug.log* 41 | yarn-debug.log* 42 | yarn-error.log* 43 | 44 | # Visual Studio 2015 cache/options directory 45 | .vs/ 46 | # Uncomment if you have tasks that create the project's static files in wwwroot 47 | #wwwroot/ 48 | 49 | # MSTest test Results 50 | [Tt]est[Rr]esult*/ 51 | [Bb]uild[Ll]og.* 52 | 53 | # NUNIT 54 | *.VisualState.xml 55 | TestResult.xml 56 | 57 | # Build Results of an ATL Project 58 | [Dd]ebugPS/ 59 | [Rr]eleasePS/ 60 | dlldata.c 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | #**/Properties/launchSettings.json 67 | 68 | *_i.c 69 | *_p.c 70 | *_i.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.pch 75 | *.pdb 76 | *.pgc 77 | *.pgd 78 | *.rsp 79 | *.sbr 80 | *.tlb 81 | *.tli 82 | *.tlh 83 | *.tmp 84 | *.tmp_proj 85 | *.log 86 | *.vspscc 87 | *.vssscc 88 | .builds 89 | *.pidb 90 | *.svclog 91 | *.scc 92 | 93 | # Chutzpah Test files 94 | _Chutzpah* 95 | 96 | # Visual C++ cache files 97 | ipch/ 98 | *.aps 99 | *.ncb 100 | *.opendb 101 | *.opensdf 102 | *.sdf 103 | *.cachefile 104 | *.VC.db 105 | *.VC.VC.opendb 106 | 107 | # Visual Studio profiler 108 | *.psess 109 | *.vsp 110 | *.vspx 111 | *.sap 112 | 113 | # TFS 2012 Local Workspace 114 | $tf/ 115 | 116 | # Guidance Automation Toolkit 117 | *.gpState 118 | 119 | # ReSharper is a .NET coding add-in 120 | _ReSharper*/ 121 | *.[Rr]e[Ss]harper 122 | *.DotSettings.user 123 | 124 | # JustCode is a .NET coding add-in 125 | .JustCode 126 | 127 | # TeamCity is a build add-in 128 | _TeamCity* 129 | 130 | # DotCover is a Code Coverage Tool 131 | *.dotCover 132 | 133 | # Visual Studio code coverage results 134 | *.coverage 135 | *.coveragexml 136 | 137 | # NCrunch 138 | _NCrunch_* 139 | .*crunch*.local.xml 140 | nCrunchTemp_* 141 | 142 | # MightyMoose 143 | *.mm.* 144 | AutoTest.Net/ 145 | 146 | # Web workbench (sass) 147 | .sass-cache/ 148 | 149 | # Installshield output folder 150 | [Ee]xpress/ 151 | 152 | # DocProject is a documentation generator add-in 153 | DocProject/buildhelp/ 154 | DocProject/Help/*.HxT 155 | DocProject/Help/*.HxC 156 | DocProject/Help/*.hhc 157 | DocProject/Help/*.hhk 158 | DocProject/Help/*.hhp 159 | DocProject/Help/Html2 160 | DocProject/Help/html 161 | 162 | # Click-Once directory 163 | publish/ 164 | 165 | # Publish Web Output 166 | *.[Pp]ublish.xml 167 | *.azurePubxml 168 | # TODO: Comment the next line if you want to checkin your web deploy settings 169 | # but database connection strings (with potential passwords) will be unencrypted 170 | *.pubxml 171 | *.publishproj 172 | 173 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 174 | # checkin your Azure Web App publish settings, but sensitive information contained 175 | # in these scripts will be unencrypted 176 | PublishScripts/ 177 | 178 | # NuGet Packages 179 | *.nupkg 180 | # The packages folder can be ignored because of Package Restore 181 | **/packages/* 182 | # except build/, which is used as an MSBuild target. 183 | !**/packages/build/ 184 | # Uncomment if necessary however generally it will be regenerated when needed 185 | #!**/packages/repositories.config 186 | # NuGet v3's project.json files produces more ignorable files 187 | *.nuget.props 188 | *.nuget.targets 189 | 190 | # Microsoft Azure Build Output 191 | csx/ 192 | *.build.csdef 193 | 194 | # Microsoft Azure Emulator 195 | ecf/ 196 | rcf/ 197 | 198 | # Windows Store app package directories and files 199 | AppPackages/ 200 | BundleArtifacts/ 201 | Package.StoreAssociation.xml 202 | _pkginfo.txt 203 | 204 | # Visual Studio cache files 205 | # files ending in .cache can be ignored 206 | *.[Cc]ache 207 | # but keep track of directories ending in .cache 208 | !*.[Cc]ache/ 209 | 210 | # Others 211 | ClientBin/ 212 | ~$* 213 | *~ 214 | *.dbmdl 215 | *.dbproj.schemaview 216 | *.jfm 217 | *.pfx 218 | *.publishsettings 219 | orleans.codegen.cs 220 | 221 | # Since there are multiple workflows, uncomment next line to ignore bower_components 222 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 223 | #bower_components/ 224 | 225 | # RIA/Silverlight projects 226 | Generated_Code/ 227 | 228 | # Backup & report files from converting an old project file 229 | # to a newer Visual Studio version. Backup files are not needed, 230 | # because we have git ;-) 231 | _UpgradeReport_Files/ 232 | Backup*/ 233 | UpgradeLog*.XML 234 | UpgradeLog*.htm 235 | 236 | # SQL Server files 237 | *.mdf 238 | *.ldf 239 | *.ndf 240 | 241 | # Business Intelligence projects 242 | *.rdl.data 243 | *.bim.layout 244 | *.bim_*.settings 245 | 246 | # Microsoft Fakes 247 | FakesAssemblies/ 248 | 249 | # GhostDoc plugin setting file 250 | *.GhostDoc.xml 251 | 252 | # Node.js Tools for Visual Studio 253 | .ntvs_analysis.dat 254 | node_modules/ 255 | 256 | # Typescript v1 declaration files 257 | typings/ 258 | 259 | # Visual Studio 6 build log 260 | *.plg 261 | 262 | # Visual Studio 6 workspace options file 263 | *.opt 264 | 265 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 266 | *.vbw 267 | 268 | # Visual Studio LightSwitch build output 269 | **/*.HTMLClient/GeneratedArtifacts 270 | **/*.DesktopClient/GeneratedArtifacts 271 | **/*.DesktopClient/ModelManifest.xml 272 | **/*.Server/GeneratedArtifacts 273 | **/*.Server/ModelManifest.xml 274 | _Pvt_Extensions 275 | 276 | # Paket dependency manager 277 | .paket/paket.exe 278 | paket-files/ 279 | 280 | # FAKE - F# Make 281 | .fake/ 282 | 283 | # JetBrains Rider 284 | .idea/ 285 | *.sln.iml 286 | 287 | # CodeRush 288 | .cr/ 289 | 290 | # Python Tools for Visual Studio (PTVS) 291 | __pycache__/ 292 | *.pyc 293 | 294 | # Cake - Uncomment if you are using it 295 | # tools/** 296 | # !tools/packages.config 297 | 298 | # Telerik's JustMock configuration file 299 | *.jmconfig 300 | 301 | # BizTalk build output 302 | *.btp.cs 303 | *.btm.cs 304 | *.odx.cs 305 | *.xsd.cs 306 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/MyApp/bin/Debug/net6.0/MyApp.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/MyApp", 15 | "stopAtEntry": false, 16 | "serverReadyAction": { 17 | "action": "openExternally", 18 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 19 | }, 20 | "env": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | }, 23 | "sourceFileMap": { 24 | "/Views": "${workspaceFolder}/Views" 25 | } 26 | }, 27 | { 28 | "name": ".NET Core Attach", 29 | "type": "coreclr", 30 | "request": "attach" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet build", 9 | "type": "shell", 10 | "group": "build", 11 | "presentation": { 12 | "reveal": "silent" 13 | }, 14 | "problemMatcher": "$msCompile" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /MyApp.ServiceInterface/MyApp.ServiceInterface.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /MyApp.ServiceInterface/MyServices.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack; 3 | using MyApp.ServiceModel; 4 | 5 | namespace MyApp.ServiceInterface 6 | { 7 | public class MyServices : Service 8 | { 9 | public object Any(Hello request) 10 | { 11 | return new HelloResponse { Result = $"Hello, {request.Name}!" }; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MyApp.ServiceModel/Hello.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | 3 | namespace MyApp.ServiceModel 4 | { 5 | [Route("/hello")] 6 | [Route("/hello/{Name}")] 7 | public class Hello : IReturn 8 | { 9 | public string Name { get; set; } 10 | } 11 | 12 | public class HelloResponse 13 | { 14 | public string Result { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MyApp.ServiceModel/MyApp.ServiceModel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /MyApp.ServiceModel/Types/README.md: -------------------------------------------------------------------------------- 1 | As part of our [Physical Project Structure](https://docs.servicestack.net/physical-project-structure) convention we recommend maintaining any shared non Request/Response DTOs in the `ServiceModel.Types` namespace. -------------------------------------------------------------------------------- /MyApp.Tests/IntegrationTest.cs: -------------------------------------------------------------------------------- 1 | using Funq; 2 | using ServiceStack; 3 | using NUnit.Framework; 4 | using MyApp.ServiceInterface; 5 | using MyApp.ServiceModel; 6 | 7 | namespace MyApp.Tests; 8 | 9 | public class IntegrationTest 10 | { 11 | const string BaseUri = "http://localhost:2000/"; 12 | private readonly ServiceStackHost appHost; 13 | 14 | class AppHost : AppSelfHostBase 15 | { 16 | public AppHost() : base(nameof(IntegrationTest), typeof(MyServices).Assembly) { } 17 | 18 | public override void Configure(Container container) 19 | { 20 | } 21 | } 22 | 23 | public IntegrationTest() 24 | { 25 | appHost = new AppHost() 26 | .Init() 27 | .Start(BaseUri); 28 | } 29 | 30 | [OneTimeTearDown] 31 | public void OneTimeTearDown() => appHost.Dispose(); 32 | 33 | public IServiceClient CreateClient() => new JsonServiceClient(BaseUri); 34 | 35 | [Test] 36 | public void Can_call_Hello_Service() 37 | { 38 | var client = CreateClient(); 39 | 40 | var response = client.Get(new Hello { Name = "World" }); 41 | 42 | Assert.That(response.Result, Is.EqualTo("Hello, World!")); 43 | } 44 | } -------------------------------------------------------------------------------- /MyApp.Tests/MyApp.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | portable 6 | Library 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /MyApp.Tests/UnitTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ServiceStack; 3 | using ServiceStack.Testing; 4 | using MyApp.ServiceInterface; 5 | using MyApp.ServiceModel; 6 | 7 | namespace MyApp.Tests; 8 | 9 | public class UnitTest 10 | { 11 | private readonly ServiceStackHost appHost; 12 | 13 | public UnitTest() 14 | { 15 | appHost = new BasicAppHost().Init(); 16 | appHost.Container.AddTransient(); 17 | } 18 | 19 | [OneTimeTearDown] 20 | public void OneTimeTearDown() => appHost.Dispose(); 21 | 22 | [Test] 23 | public void Can_call_MyServices() 24 | { 25 | var service = appHost.Container.Resolve(); 26 | 27 | var response = (HelloResponse)service.Any(new Hello { Name = "World" }); 28 | 29 | Assert.That(response.Result, Is.EqualTo("Hello, World!")); 30 | } 31 | } -------------------------------------------------------------------------------- /MyApp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyApp", "MyApp\MyApp.csproj", "{5F817400-1A3A-48DF-98A6-E7E5A3DC762F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyApp.ServiceInterface", "MyApp.ServiceInterface\MyApp.ServiceInterface.csproj", "{5B8FFF01-1E0B-477D-9D7F-93016C128B23}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyApp.ServiceModel", "MyApp.ServiceModel\MyApp.ServiceModel.csproj", "{0127B6CA-1B79-46A6-8307-B36836D107F0}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyApp.Tests", "MyApp.Tests\MyApp.Tests.csproj", "{455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {02854F2A-8EF4-468E-80A3-CD64BBAF5D15} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /MyApp/App_Data/README.md: -------------------------------------------------------------------------------- 1 | ## App Writable Folder 2 | 3 | This directory is designated for: 4 | 5 | - **Embedded Databases**: Such as SQLite. 6 | - **Writable Files**: Files that the application might need to modify during its operation. 7 | 8 | For applications running in **Docker**, it's a common practice to mount this directory as an external volume. This ensures: 9 | 10 | - **Data Persistence**: App data is preserved across deployments. 11 | - **Easy Replication**: Facilitates seamless data replication for backup or migration purposes. 12 | -------------------------------------------------------------------------------- /MyApp/Configure.AppHost.cs: -------------------------------------------------------------------------------- 1 | using Funq; 2 | using ServiceStack; 3 | using MyApp.ServiceInterface; 4 | 5 | [assembly: HostingStartup(typeof(MyApp.AppHost))] 6 | 7 | namespace MyApp; 8 | 9 | public class AppHost : AppHostBase, IHostingStartup 10 | { 11 | public void Configure(IWebHostBuilder builder) => builder 12 | .ConfigureServices(services => { 13 | // Configure ASP.NET Core IOC Dependencies 14 | }); 15 | 16 | public AppHost() : base("MyApp", typeof(MyServices).Assembly) {} 17 | 18 | public override void Configure(Container container) 19 | { 20 | // enable server-side rendering, see: https://sharpscript.net/docs/sharp-pages 21 | Plugins.Add(new SharpPagesFeature { 22 | EnableSpaFallback = true 23 | }); 24 | 25 | SetConfig(new HostConfig { 26 | AddRedirectParamsToQueryString = true, 27 | }); 28 | 29 | Plugins.Add(new CorsFeature(allowOriginWhitelist:new[]{ 30 | "http://localhost:5000", 31 | "http://localhost:3000", 32 | "http://localhost:8080", 33 | "https://localhost:5001", 34 | }, allowCredentials:true)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MyApp/Configure.Auth.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using ServiceStack; 3 | using ServiceStack.Auth; 4 | using ServiceStack.FluentValidation; 5 | 6 | [assembly: HostingStartup(typeof(MyApp.ConfigureAuth))] 7 | 8 | namespace MyApp 9 | { 10 | // Add any additional metadata properties you want to store in the Users Typed Session 11 | public class CustomUserSession : AuthUserSession 12 | { 13 | } 14 | 15 | // Custom Validator to add custom validators to built-in /register Service requiring DisplayName and ConfirmPassword 16 | public class CustomRegistrationValidator : RegistrationValidator 17 | { 18 | public CustomRegistrationValidator() 19 | { 20 | RuleSet(ApplyTo.Post, () => 21 | { 22 | RuleFor(x => x.DisplayName).NotEmpty(); 23 | RuleFor(x => x.ConfirmPassword).NotEmpty(); 24 | }); 25 | } 26 | } 27 | 28 | public class ConfigureAuth : IHostingStartup 29 | { 30 | public void Configure(IWebHostBuilder builder) => builder 31 | .ConfigureServices(services => { 32 | //services.AddSingleton(new MemoryCacheClient()); //Store User Sessions in Memory Cache (default) 33 | }) 34 | .ConfigureAppHost(appHost => { 35 | var appSettings = appHost.AppSettings; 36 | appHost.Plugins.Add(new AuthFeature(() => new CustomUserSession(), 37 | new IAuthProvider[] { 38 | new CredentialsAuthProvider(appSettings), /* Sign In with Username / Password credentials */ 39 | new FacebookAuthProvider(appSettings), /* Create App https://developers.facebook.com/apps */ 40 | new GoogleAuthProvider(appSettings), /* Create App https://console.developers.google.com/apis/credentials */ 41 | new MicrosoftGraphAuthProvider(appSettings), /* Create App https://apps.dev.microsoft.com */ 42 | })); 43 | 44 | appHost.Plugins.Add(new RegistrationFeature()); //Enable /register Service 45 | 46 | //override the default registration validation with your own custom implementation 47 | appHost.RegisterAs>(); 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MyApp/Configure.AuthRepository.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | using ServiceStack.Web; 3 | using ServiceStack.Auth; 4 | using ServiceStack.Configuration; 5 | 6 | [assembly: HostingStartup(typeof(MyApp.ConfigureAuthRepository))] 7 | 8 | namespace MyApp 9 | { 10 | // Custom User Table with extended Metadata properties 11 | public class AppUser : UserAuth 12 | { 13 | public string? ProfileUrl { get; set; } 14 | public string? LastLoginIp { get; set; } 15 | public DateTime? LastLoginDate { get; set; } 16 | } 17 | 18 | public class AppUserAuthEvents : AuthEvents 19 | { 20 | public override void OnAuthenticated(IRequest req, IAuthSession session, IServiceBase authService, 21 | IAuthTokens tokens, Dictionary authInfo) 22 | { 23 | var authRepo = HostContext.AppHost.GetAuthRepository(req); 24 | using (authRepo as IDisposable) 25 | { 26 | var userAuth = (AppUser)authRepo.GetUserAuth(session.UserAuthId); 27 | userAuth.ProfileUrl = session.GetProfileUrl(); 28 | userAuth.LastLoginIp = req.UserHostAddress; 29 | userAuth.LastLoginDate = DateTime.UtcNow; 30 | authRepo.SaveUserAuth(userAuth); 31 | } 32 | } 33 | } 34 | 35 | public class ConfigureAuthRepository : IHostingStartup 36 | { 37 | public void Configure(IWebHostBuilder builder) => builder 38 | .ConfigureServices(services => services.AddSingleton(c => 39 | new InMemoryAuthRepository())) 40 | .ConfigureAppHost(appHost => { 41 | var authRepo = appHost.Resolve(); 42 | authRepo.InitSchema(); 43 | // CreateUser(authRepo, "admin@email.com", "Admin User", "p@55wOrd", roles:new[]{ RoleNames.Admin }); 44 | }, afterConfigure: appHost => 45 | appHost.AssertPlugin().AuthEvents.Add(new AppUserAuthEvents())); 46 | 47 | // Add initial Users to the configured Auth Repository 48 | public void CreateUser(IAuthRepository authRepo, string email, string name, string password, string[] roles) 49 | { 50 | if (authRepo.GetUserAuthByUserName(email) == null) 51 | { 52 | var newAdmin = new AppUser { Email = email, DisplayName = name }; 53 | var user = authRepo.CreateUserAuth(newAdmin, password); 54 | authRepo.AssignRoles(user, roles); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MyApp/MyApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | DefaultContainer 5 | net8.0 6 | enable 7 | enable 8 | latest 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /MyApp/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | 3 | var app = builder.Build(); 4 | 5 | // Configure the HTTP request pipeline. 6 | if (!app.Environment.IsDevelopment()) 7 | { 8 | app.UseExceptionHandler("/Error"); 9 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 10 | app.UseHsts(); 11 | app.UseHttpsRedirection(); 12 | } 13 | app.UseServiceStack(new AppHost()); 14 | 15 | app.Run(); -------------------------------------------------------------------------------- /MyApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "https://localhost:5001/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "MyApp": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | }, 17 | "applicationUrl": "https://localhost:5001/" 18 | }, 19 | "IIS Express": { 20 | "commandName": "IISExpress", 21 | "launchBrowser": true, 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MyApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | }, 9 | "oauth.RedirectUrl": "https://localhost:5001/", 10 | "oauth.CallbackUrl": "https://localhost:5001/auth/{0}", 11 | "oauth.facebook.Permissions": [ "email", "user_location" ], 12 | "oauth.facebook.AppId": "531608123577340", 13 | "oauth.facebook.AppSecret": "9e1e6591a7f15cbc1b305729f4b14c0b", 14 | "oauth.google.ConsumerKey": "274592649256-nmvuiu5ri7s1nghilbo6nmfd6h8j71sc.apps.googleusercontent.com", 15 | "oauth.google.ConsumerSecret": "aKOJngvq0USp3kyA_mkFH8Il", 16 | "oauth.microsoftgraph.AppId": "8208d98e-400d-4ce9-89ba-d92610c67e13", 17 | "oauth.microsoftgraph.AppSecret": "hsrMP46|_kfkcYCWSW516?%", 18 | "oauth.microsoftgraph.SavePhoto": "true", 19 | "oauth.microsoftgraph.SavePhotoSize": "96x96" 20 | } 21 | -------------------------------------------------------------------------------- /MyApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /MyApp/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /MyApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "npm run build", 7 | "dev": "vue-cli-service build --watch", 8 | "serve": "vue-cli-service serve", 9 | "dtos": "x typescript", 10 | "build": "vue-cli-service build", 11 | "lint": "vue-cli-service lint", 12 | "publish": "vue-cli-service build && dotnet publish -c Release", 13 | "test": "vue-cli-service test:unit", 14 | "test-watch": "vue-cli-service test:unit --watchAll" 15 | }, 16 | "dependencies": { 17 | "@servicestack/client": "^1.2.1", 18 | "@servicestack/vue": "^1.0.13", 19 | "bootstrap": "^5.2.0", 20 | "core-js": "^3.25.0", 21 | "vue": "^2.7.10", 22 | "vue-class-component": "^7.2.6", 23 | "vue-property-decorator": "^9.1.2", 24 | "vue-router": "^3.6.4" 25 | }, 26 | "devDependencies": { 27 | "@iconify/vue2": "^1.2.1", 28 | "@types/jest": "^29.0.0", 29 | "@vue/cli-plugin-babel": "^5.0.8", 30 | "@vue/cli-plugin-typescript": "^5.0.8", 31 | "@vue/cli-plugin-unit-jest": "^5.0.8", 32 | "@vue/cli-service": "^5.0.8", 33 | "@vue/test-utils": "^1.3.0", 34 | "typescript": "^4.8.2", 35 | "vue-template-compiler": "^2.7.10" 36 | }, 37 | "browserslist": [ 38 | "> 1%", 39 | "last 2 versions", 40 | "not dead" 41 | ], 42 | "jest": { 43 | "setupFilesAfterEnv": [ 44 | "/tests/setup.js" 45 | ], 46 | "modulePathIgnorePatterns": [ 47 | "/bin/" 48 | ], 49 | "moduleFileExtensions": [ 50 | "ts", 51 | "tsx", 52 | "js", 53 | "jsx", 54 | "json", 55 | "vue" 56 | ], 57 | "transform": { 58 | "^.+\\.vue$": "vue-jest", 59 | ".+\\.(css|styl|less|png|jpg|ttf|woff|woff2)$": "jest-transform-stub", 60 | "^.+\\.tsx?$": "ts-jest", 61 | "^@servicestack/vue$": "ts-jest" 62 | }, 63 | "transformIgnorePatterns": [ 64 | "node_modules/(?!(babel-jest|jest-vue-preprocessor)/)" 65 | ], 66 | "moduleNameMapper": { 67 | "^@/(.*)$": "/src/$1" 68 | }, 69 | "snapshotSerializers": [ 70 | "jest-serializer-vue" 71 | ], 72 | "testMatch": [ 73 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 74 | ], 75 | "globals": { 76 | "testURL": "https://localhost:5001/" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /MyApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LegacyTemplates/vue-spa/08e627cf7607eb7d1d61ebaf593ec8b90d2ac95e/MyApp/public/favicon.ico -------------------------------------------------------------------------------- /MyApp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MyApp 9 | 10 | 11 | 12 | 21 | 22 | 25 | 26 |
27 | 28 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /MyApp/public/svg/app/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MyApp/public/svg/svg-icons/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MyApp/src/App.vue: -------------------------------------------------------------------------------- 1 |  17 | 18 | 38 | -------------------------------------------------------------------------------- /MyApp/src/app.css: -------------------------------------------------------------------------------- 1 | .form-check.form-control { 2 | padding-left: 1.75rem; 3 | height: auto; 4 | } 5 | .navbar { 6 | border-radius: 0; 7 | background: #41B883 !important; 8 | } 9 | .nav-item.dropdown:hover .dropdown-menu { 10 | display: block; 11 | } 12 | .text-green { 13 | color: #41B883; 14 | } 15 | a { 16 | color: #35495E; 17 | } 18 | .nav-button-group { 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | .btn-block a { 23 | display: block; 24 | } 25 | .nav-button-group .btn-block { 26 | margin-bottom: .25rem; 27 | } 28 | -------------------------------------------------------------------------------- /MyApp/src/components/About.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /MyApp/src/components/Admin/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | -------------------------------------------------------------------------------- /MyApp/src/components/Home/HelloApi.vue: -------------------------------------------------------------------------------- 1 |  7 | 8 | 40 | 41 | -------------------------------------------------------------------------------- /MyApp/src/components/Home/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 45 | -------------------------------------------------------------------------------- /MyApp/src/components/Profile.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 39 | -------------------------------------------------------------------------------- /MyApp/src/components/SignIn.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 99 | -------------------------------------------------------------------------------- /MyApp/src/components/SignUp.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 99 | -------------------------------------------------------------------------------- /MyApp/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css'; 2 | import './app.css'; 3 | import 'es6-shim'; 4 | import Vue from 'vue'; 5 | import { Icon } from '@iconify/vue2'; 6 | import App from './App.vue' 7 | 8 | import Controls from '@servicestack/vue'; 9 | Vue.use(Controls); 10 | 11 | Vue.component('Icon', Icon) 12 | 13 | 14 | import { router } from './shared/router'; 15 | 16 | Vue.config.productionTip = false; 17 | 18 | const app = new Vue({ 19 | el: '#app', 20 | render: (h) => h(App), 21 | router, 22 | }); 23 | -------------------------------------------------------------------------------- /MyApp/src/shared/dtos.ts: -------------------------------------------------------------------------------- 1 | /* Options: 2 | Date: 2021-12-30 13:40:05 3 | Version: 5.133 4 | Tip: To override a DTO option, remove "//" prefix before updating 5 | BaseUrl: https://localhost:5001 6 | 7 | //GlobalNamespace: 8 | //MakePropertiesOptional: False 9 | //AddServiceStackTypes: True 10 | //AddResponseStatus: False 11 | //AddImplicitVersion: 12 | //AddDescriptionAsComments: True 13 | //IncludeTypes: 14 | //ExcludeTypes: 15 | //DefaultImports: 16 | */ 17 | 18 | 19 | export interface IReturn 20 | { 21 | createResponse(): T; 22 | } 23 | 24 | export interface IReturnVoid 25 | { 26 | createResponse(): void; 27 | } 28 | 29 | export interface IHasSessionId 30 | { 31 | sessionId: string; 32 | } 33 | 34 | export interface IHasBearerToken 35 | { 36 | bearerToken: string; 37 | } 38 | 39 | export interface IPost 40 | { 41 | } 42 | 43 | // @DataContract 44 | export class ResponseError 45 | { 46 | // @DataMember(Order=1) 47 | public errorCode: string; 48 | 49 | // @DataMember(Order=2) 50 | public fieldName: string; 51 | 52 | // @DataMember(Order=3) 53 | public message: string; 54 | 55 | // @DataMember(Order=4) 56 | public meta: { [index: string]: string; }; 57 | 58 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 59 | } 60 | 61 | // @DataContract 62 | export class ResponseStatus 63 | { 64 | // @DataMember(Order=1) 65 | public errorCode: string; 66 | 67 | // @DataMember(Order=2) 68 | public message: string; 69 | 70 | // @DataMember(Order=3) 71 | public stackTrace: string; 72 | 73 | // @DataMember(Order=4) 74 | public errors: ResponseError[]; 75 | 76 | // @DataMember(Order=5) 77 | public meta: { [index: string]: string; }; 78 | 79 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 80 | } 81 | 82 | export class HelloResponse 83 | { 84 | public result: string; 85 | 86 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 87 | } 88 | 89 | // @DataContract 90 | export class AuthenticateResponse implements IHasSessionId, IHasBearerToken 91 | { 92 | // @DataMember(Order=1) 93 | public userId: string; 94 | 95 | // @DataMember(Order=2) 96 | public sessionId: string; 97 | 98 | // @DataMember(Order=3) 99 | public userName: string; 100 | 101 | // @DataMember(Order=4) 102 | public displayName: string; 103 | 104 | // @DataMember(Order=5) 105 | public referrerUrl: string; 106 | 107 | // @DataMember(Order=6) 108 | public bearerToken: string; 109 | 110 | // @DataMember(Order=7) 111 | public refreshToken: string; 112 | 113 | // @DataMember(Order=8) 114 | public profileUrl: string; 115 | 116 | // @DataMember(Order=9) 117 | public roles: string[]; 118 | 119 | // @DataMember(Order=10) 120 | public permissions: string[]; 121 | 122 | // @DataMember(Order=11) 123 | public responseStatus: ResponseStatus; 124 | 125 | // @DataMember(Order=12) 126 | public meta: { [index: string]: string; }; 127 | 128 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 129 | } 130 | 131 | // @DataContract 132 | export class AssignRolesResponse 133 | { 134 | // @DataMember(Order=1) 135 | public allRoles: string[]; 136 | 137 | // @DataMember(Order=2) 138 | public allPermissions: string[]; 139 | 140 | // @DataMember(Order=3) 141 | public meta: { [index: string]: string; }; 142 | 143 | // @DataMember(Order=4) 144 | public responseStatus: ResponseStatus; 145 | 146 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 147 | } 148 | 149 | // @DataContract 150 | export class UnAssignRolesResponse 151 | { 152 | // @DataMember(Order=1) 153 | public allRoles: string[]; 154 | 155 | // @DataMember(Order=2) 156 | public allPermissions: string[]; 157 | 158 | // @DataMember(Order=3) 159 | public meta: { [index: string]: string; }; 160 | 161 | // @DataMember(Order=4) 162 | public responseStatus: ResponseStatus; 163 | 164 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 165 | } 166 | 167 | // @DataContract 168 | export class RegisterResponse implements IHasSessionId, IHasBearerToken 169 | { 170 | // @DataMember(Order=1) 171 | public userId: string; 172 | 173 | // @DataMember(Order=2) 174 | public sessionId: string; 175 | 176 | // @DataMember(Order=3) 177 | public userName: string; 178 | 179 | // @DataMember(Order=4) 180 | public referrerUrl: string; 181 | 182 | // @DataMember(Order=5) 183 | public bearerToken: string; 184 | 185 | // @DataMember(Order=6) 186 | public refreshToken: string; 187 | 188 | // @DataMember(Order=7) 189 | public roles: string[]; 190 | 191 | // @DataMember(Order=8) 192 | public permissions: string[]; 193 | 194 | // @DataMember(Order=9) 195 | public responseStatus: ResponseStatus; 196 | 197 | // @DataMember(Order=10) 198 | public meta: { [index: string]: string; }; 199 | 200 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 201 | } 202 | 203 | // @Route("/hello") 204 | // @Route("/hello/{Name}") 205 | export class Hello implements IReturn 206 | { 207 | public name: string; 208 | 209 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 210 | public createResponse() { return new HelloResponse(); } 211 | public getTypeName() { return 'Hello'; } 212 | public getMethod() { return 'POST'; } 213 | } 214 | 215 | // @Route("/auth") 216 | // @Route("/auth/{provider}") 217 | // @DataContract 218 | export class Authenticate implements IReturn, IPost 219 | { 220 | // @DataMember(Order=1) 221 | public provider: string; 222 | 223 | // @DataMember(Order=2) 224 | public state: string; 225 | 226 | // @DataMember(Order=3) 227 | public oauth_token: string; 228 | 229 | // @DataMember(Order=4) 230 | public oauth_verifier: string; 231 | 232 | // @DataMember(Order=5) 233 | public userName: string; 234 | 235 | // @DataMember(Order=6) 236 | public password: string; 237 | 238 | // @DataMember(Order=7) 239 | public rememberMe?: boolean; 240 | 241 | // @DataMember(Order=9) 242 | public errorView: string; 243 | 244 | // @DataMember(Order=10) 245 | public nonce: string; 246 | 247 | // @DataMember(Order=11) 248 | public uri: string; 249 | 250 | // @DataMember(Order=12) 251 | public response: string; 252 | 253 | // @DataMember(Order=13) 254 | public qop: string; 255 | 256 | // @DataMember(Order=14) 257 | public nc: string; 258 | 259 | // @DataMember(Order=15) 260 | public cnonce: string; 261 | 262 | // @DataMember(Order=17) 263 | public accessToken: string; 264 | 265 | // @DataMember(Order=18) 266 | public accessTokenSecret: string; 267 | 268 | // @DataMember(Order=19) 269 | public scope: string; 270 | 271 | // @DataMember(Order=20) 272 | public meta: { [index: string]: string; }; 273 | 274 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 275 | public createResponse() { return new AuthenticateResponse(); } 276 | public getTypeName() { return 'Authenticate'; } 277 | public getMethod() { return 'POST'; } 278 | } 279 | 280 | // @Route("/assignroles") 281 | // @DataContract 282 | export class AssignRoles implements IReturn, IPost 283 | { 284 | // @DataMember(Order=1) 285 | public userName: string; 286 | 287 | // @DataMember(Order=2) 288 | public permissions: string[]; 289 | 290 | // @DataMember(Order=3) 291 | public roles: string[]; 292 | 293 | // @DataMember(Order=4) 294 | public meta: { [index: string]: string; }; 295 | 296 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 297 | public createResponse() { return new AssignRolesResponse(); } 298 | public getTypeName() { return 'AssignRoles'; } 299 | public getMethod() { return 'POST'; } 300 | } 301 | 302 | // @Route("/unassignroles") 303 | // @DataContract 304 | export class UnAssignRoles implements IReturn, IPost 305 | { 306 | // @DataMember(Order=1) 307 | public userName: string; 308 | 309 | // @DataMember(Order=2) 310 | public permissions: string[]; 311 | 312 | // @DataMember(Order=3) 313 | public roles: string[]; 314 | 315 | // @DataMember(Order=4) 316 | public meta: { [index: string]: string; }; 317 | 318 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 319 | public createResponse() { return new UnAssignRolesResponse(); } 320 | public getTypeName() { return 'UnAssignRoles'; } 321 | public getMethod() { return 'POST'; } 322 | } 323 | 324 | // @Route("/register") 325 | // @DataContract 326 | export class Register implements IReturn, IPost 327 | { 328 | // @DataMember(Order=1) 329 | public userName: string; 330 | 331 | // @DataMember(Order=2) 332 | public firstName: string; 333 | 334 | // @DataMember(Order=3) 335 | public lastName: string; 336 | 337 | // @DataMember(Order=4) 338 | public displayName: string; 339 | 340 | // @DataMember(Order=5) 341 | public email: string; 342 | 343 | // @DataMember(Order=6) 344 | public password: string; 345 | 346 | // @DataMember(Order=7) 347 | public confirmPassword: string; 348 | 349 | // @DataMember(Order=8) 350 | public autoLogin?: boolean; 351 | 352 | // @DataMember(Order=10) 353 | public errorView: string; 354 | 355 | // @DataMember(Order=11) 356 | public meta: { [index: string]: string; }; 357 | 358 | public constructor(init?: Partial) { (Object as any).assign(this, init); } 359 | public createResponse() { return new RegisterResponse(); } 360 | public getTypeName() { return 'Register'; } 361 | public getMethod() { return 'POST'; } 362 | } 363 | 364 | -------------------------------------------------------------------------------- /MyApp/src/shared/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { JsonServiceClient, GetNavItemsResponse, UserAttributes, IAuthSession } from '@servicestack/client'; 3 | 4 | declare let global: any; // populated from package.json/jest 5 | 6 | export const client = new JsonServiceClient(global.testURL || '/'); 7 | 8 | export { 9 | errorResponse, errorResponseExcept, 10 | splitOnFirst, toPascalCase, 11 | queryString, 12 | } from '@servicestack/client'; 13 | 14 | export { 15 | ResponseStatus, ResponseError, 16 | Authenticate, AuthenticateResponse, 17 | Register, 18 | Hello, HelloResponse, 19 | } from './dtos'; 20 | 21 | import { Authenticate, AuthenticateResponse } from './dtos'; 22 | 23 | export enum Roles { 24 | Admin = 'Admin', 25 | } 26 | 27 | // Shared state between all Components 28 | interface State { 29 | nav: GetNavItemsResponse; 30 | userSession: IAuthSession | null; 31 | userAttributes?: string[]; 32 | roles?: string[]; 33 | permissions?: string[]; 34 | } 35 | export const store: State = { 36 | nav: global.NAV_ITEMS as GetNavItemsResponse || { results: [], navItemsMap: {} }, 37 | userSession: global.AUTH as AuthenticateResponse, 38 | userAttributes: UserAttributes.fromSession(global.AUTH), 39 | }; 40 | 41 | class EventBus extends Vue { 42 | store = store; 43 | } 44 | export const bus = new EventBus({ data: store }); 45 | 46 | bus.$on('signout', async () => { 47 | bus.$set(store, 'userSession', null); 48 | bus.$set(store, 'userAttributes', null); 49 | 50 | await client.post(new Authenticate({ provider: 'logout' })); 51 | }); 52 | export const signout = () => bus.$emit('signout'); 53 | 54 | bus.$on('signin', (userSession: AuthenticateResponse) => { 55 | const userAttributes = UserAttributes.fromSession(userSession); 56 | bus.$set(store, 'userSession', userSession); 57 | bus.$set(store, 'userAttributes', userAttributes); 58 | }); 59 | 60 | export const checkAuth = async () => { 61 | try { 62 | bus.$emit('signin', await client.post(new Authenticate())); 63 | } catch (e) { 64 | bus.$emit('signout'); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /MyApp/src/shared/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router, { Route } from 'vue-router'; 3 | 4 | import { store, bus } from './index'; 5 | 6 | import { Forbidden } from '@servicestack/vue'; 7 | import Home from '../components/Home/index.vue'; 8 | import About from '../components/About.vue'; 9 | import SignIn from '../components/SignIn.vue'; 10 | import SignUp from '../components/SignUp.vue'; 11 | import Profile from '../components/Profile.vue'; 12 | import Admin from '../components/Admin/index.vue'; 13 | 14 | export enum Routes { 15 | Home = '/', 16 | About = '/about', 17 | SignIn = '/signin', 18 | SignUp = '/signup', 19 | Profile = '/profile', 20 | Admin = '/admin', 21 | Forbidden = '/forbidden', 22 | } 23 | 24 | Vue.use(Router); 25 | 26 | function requiresAuth(to: Route, from: Route, next: (to?: string) => void) { 27 | if (!store.userSession) { 28 | next(`${Routes.SignIn}?redirect=${encodeURIComponent(to.path)}`); 29 | return; 30 | } 31 | next(); 32 | } 33 | 34 | function requiresRole(role: string) { 35 | return (to: Route, from: Route, next: (to?: string) => void) => { 36 | if (!store.userSession) { 37 | next(`${Routes.SignIn}?redirect=${encodeURIComponent(to.path)}`); 38 | } 39 | else if (!store.userSession.roles || store.userSession.roles.indexOf(role) < 0) { 40 | next(`${Routes.Forbidden}?role=${encodeURIComponent(role)}`); 41 | } 42 | else { 43 | next(); 44 | } 45 | }; 46 | } 47 | 48 | const routes = [ 49 | { path: Routes.Home, component: Home, props: { name: 'Vue' } }, 50 | { path: Routes.About, component: About, props: { message: 'About page' } }, 51 | { path: Routes.SignIn, component: SignIn }, 52 | { path: Routes.SignUp, component: SignUp }, 53 | { path: Routes.Profile, component: Profile, beforeEnter: requiresAuth }, 54 | { path: Routes.Admin, component: Admin, beforeEnter: requiresRole('Admin') }, 55 | { path: Routes.Forbidden, component: Forbidden }, 56 | { path: '*', redirect: '/' }, 57 | ]; 58 | 59 | export const router = new Router ({ 60 | mode: 'history', 61 | linkActiveClass: 'active', 62 | routes, 63 | }); 64 | 65 | export const redirect = (path: string) => { 66 | const externalUrl = path.indexOf('://') >= 0; 67 | if (!externalUrl) { 68 | router.push({ path }); 69 | } else { 70 | location.href = path; 71 | } 72 | }; 73 | 74 | bus.$on('signout', async () => { 75 | // reload current page after and run route guards after signing out. 76 | const to = router.currentRoute; 77 | router.replace('/'); 78 | router.replace(to.fullPath); 79 | }); 80 | -------------------------------------------------------------------------------- /MyApp/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MyApp/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } -------------------------------------------------------------------------------- /MyApp/tests/setup.js: -------------------------------------------------------------------------------- 1 | var Vue = require('vue'); 2 | var Controls = require('@servicestack/vue'); 3 | 4 | Vue.use(Controls.default); 5 | 6 | Vue.config.productionTip = false; -------------------------------------------------------------------------------- /MyApp/tests/unit/About.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | 4 | import About from '@/components/About.vue'; 5 | 6 | describe('/about About.vue', () => { 7 | 8 | it ('should render correct contents', () => { 9 | const wrapper = shallowMount(About, { 10 | propsData: { message: 'This is the About page' }, 11 | }); 12 | expect(wrapper.vm.$el.querySelector('h3')!.textContent).toBe('This is the About page'); 13 | }); 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /MyApp/tests/unit/Home.spec.ts: -------------------------------------------------------------------------------- 1 | import 'node-fetch'; 2 | import { Input } from '@servicestack/vue'; 3 | import Home from '@/components/Home/index.vue'; 4 | import HelloApi from '@/components/Home/HelloApi.vue'; 5 | import { shallowMount } from '@vue/test-utils'; 6 | 7 | describe('Home.vue', () => { 8 | 9 | it ('should have correct data', async () => { 10 | const wrapper = shallowMount(HelloApi) as any; 11 | expect(wrapper.vm.result).toBe(''); 12 | }); 13 | 14 | it ('should render correct contents', async () => { 15 | const wrapper = shallowMount(HelloApi, { 16 | propsData: { name: 'Vue' }, 17 | }); 18 | const vm = wrapper.vm as any; 19 | expect(vm.name).toBe('Vue'); 20 | expect(wrapper.findComponent(Input).props().type).toBe('text'); 21 | expect(vm.$el.querySelector('h3.result')!.textContent).toBe(''); 22 | 23 | await wrapper.setData({result: 'Bye Vue'}); 24 | 25 | expect(vm.$el.querySelector('h3.result')!.textContent).toBe('Bye Vue'); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /MyApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "strictPropertyInitialization": false, 7 | "jsx": "preserve", 8 | "importHelpers": true, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "allowSyntheticDefaultImports": true, 13 | "esModuleInterop": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "types": [ 17 | "node", 18 | "webpack-env", 19 | "jest" 20 | ], 21 | "paths": { 22 | "@/*": [ 23 | "src/*" 24 | ] 25 | }, 26 | "lib": [ 27 | "esnext", 28 | "dom", 29 | "dom.iterable", 30 | "scripthost" 31 | ] 32 | }, 33 | "include": [ 34 | "src/**/*.ts", 35 | "src/**/*.tsx", 36 | "src/**/*.vue", 37 | "tests/**/*.ts", 38 | "tests/**/*.tsx" 39 | ], 40 | "exclude": [ 41 | "bin", 42 | "node_modules", 43 | "wwwroot" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /MyApp/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "gulpfile.js", 9 | "bin/**", 10 | "wwwroot/**", 11 | "config/**/*.js", 12 | "node_modules/**" 13 | ] 14 | }, 15 | "rules": { 16 | "quotemark": [true, "single"], 17 | "indent": [true, "spaces", 2], 18 | "interface-name": false, 19 | "ordered-imports": false, 20 | "object-literal-sort-keys": false, 21 | "no-consecutive-blank-lines": false, 22 | "no-trailing-whitespace": [true, "ignore-comments"], 23 | "one-line": false, 24 | "max-classes-per-file": false, 25 | "member-access": false, 26 | "no-empty-interface": false, 27 | "variable-name": [true, "allow-pascal-case", "allow-snake-case"], 28 | "max-line-length": [true, 160] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MyApp/vue.config.js: -------------------------------------------------------------------------------- 1 | // vue.config.js 2 | module.exports = { 3 | outputDir: 'wwwroot', 4 | devServer: { 5 | proxy: 'https://localhost:5001' 6 | } 7 | }; -------------------------------------------------------------------------------- /MyApp/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-spa 2 | 3 | .NET 6.0 Vue Webpack App Template 4 | 5 | [![](https://raw.githubusercontent.com/ServiceStack/Assets/master/csharp-templates/vue-spa.png)](http://vue-spa.web-templates.io/) 6 | 7 | > Browse [source code](https://github.com/NetCoreTemplates/vue-spa), view live demo [vue-spa.web-templates.io](http://vue-spa.web-templates.io) and install with [x new](https://docs.servicestack.net/dotnet-new): 8 | 9 | $ dotnet tool install -g x 10 | 11 | $ x new vue-spa ProjectName 12 | 13 | Alternatively write new project files directly into an empty repository, using the Directory Name as the ProjectName: 14 | 15 | $ git clone https://github.com//.git 16 | $ cd 17 | $ x new vue-spa 18 | 19 | ## Development workflow 20 | 21 | Our recommendation during development is to run the `dev` npm script or Gulp task and leave it running in the background: 22 | 23 | $ npm run dev 24 | 25 | This initially generates a full development build of your Web App then stays running in the background to process files as they’re changed. This enables the normal dev workflow of running your ASP.NET Web App, saving changes locally which are then reloaded using ServiceStack’s built-in hot reloading. Alternatively hitting `F5` will refresh the page and view the latest changes. 26 | 27 | Each change updates the output dev resources so even if you stop the dev task your Web App remains in a working state that’s viewable when running the ASP.NET Web App. 28 | 29 | ### Live reload with built-in Dev Server 30 | 31 | The alternative dev workflow is to run the `serve` npm or gulp script to run Create React App's built-in Webpack dev server: 32 | 33 | $ npm run serve 34 | 35 | This launches the Webpack dev server listening at `http://localhost:8080/` and configured to proxy all non-Webpack HTTP requests to the ASP.NET Web App where it handles all Server API requests. The benefit of viewing your App through the Webpack dev server is its built-in Live Reload feature where it will automatically reload the page as resources are updated. We’ve found the Webpack dev server ideal when developing UI’s where your Web App is running side-by-side VS.NET, where every change saved triggers the dev server to reload the current page so changes are visible immediately. 36 | 37 | The disadvantage of the dev server is that all transformations are kept in memory so when the dev server is stopped, the Web Apps resources are lost, so it requires a webpack-build in order to generate a current build. There’s also a lag in API requests resulting from all server request being proxied. 38 | 39 | ### Watched .NET Core builds 40 | 41 | .NET Core projects can also benefit from Live Coding using dotnet watch which performs a “watched build” where it automatically stops, recompiles and restarts your .NET Core App when it detects source file changes. You can start a watched build from the command-line with: 42 | 43 | $ dotnet watch run 44 | 45 | ### Create a production build 46 | 47 | Run the `build` npm script or gulp task to generate a static production build of your App saved to your .NET App's `/wwwroot` folder: 48 | 49 | $ npm run build 50 | 51 | This will let you run the production build of your App that's hosted by your .NET App. 52 | 53 | ### Updating Server TypeScript DTOs 54 | 55 | Run the `dtos` npm script or Gulp task to update your server dtos in `/src/dtos.ts`: 56 | 57 | $ npm run dtos 58 | 59 | ### Deployments 60 | 61 | When your App is ready to deploy, run the `publish` npm (or Gulp) script to package your App for deployment: 62 | 63 | $ npm run publish 64 | 65 | Which will create a production build of your App which then runs `dotnet publish -c Release` to Publish a Release build of your App in the `/bin/net5/publish` folder which can then copied to remote server or an included in a Docker container to deploy your App. 66 | 67 | ### Testing 68 | 69 | Run the `test` npm script or gulp task to launch the test runner in the interactive watch mode: 70 | 71 | $ npm test 72 | 73 | To launch a live testing session that continuously reruns tests when making source code changes, run the `test-watch` npm script instead: 74 | 75 | $ npm run test-watch 76 | 77 | To run end-to-end integration tests with [cypress](https://www.cypress.io/): 78 | 79 | $ npm run e2e 80 | 81 | ## Vue CLI App 82 | 83 | This project was bootstrapped with [Vue CLI App](https://cli.vuejs.org) which contains the official documentation to learn more about developing with Vue CLI Apps. 84 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | } 5 | } 6 | --------------------------------------------------------------------------------