├── .github └── workflows │ ├── build-container.yml │ ├── build.yml │ └── release.yml ├── .gitignore ├── .kamal ├── hooks │ ├── docker-setup.sample │ ├── post-deploy.sample │ ├── post-proxy-reboot.sample │ ├── pre-build.sample │ ├── pre-connect.sample │ ├── pre-deploy.sample │ └── pre-proxy-reboot.sample └── secrets ├── .vscode ├── launch.json └── tasks.json ├── CreatorKit.Extensions.ServiceModel ├── CreatorKit.Extensions.ServiceModel.csproj ├── MarkdownEmail.cs ├── NewsletterMailRun.cs ├── RenderNewsletter.cs ├── Types │ └── README.md └── Website.cs ├── CreatorKit.Extensions ├── CreatorKit.Extensions.csproj ├── CustomEmailRunServices.cs ├── CustomEmailServices.cs ├── CustomRendererServices.cs └── WebsiteData.cs ├── CreatorKit.ServiceInterface ├── AdminServices.cs ├── AppData.cs ├── AppExtensions.cs ├── ContactServices.cs ├── CreatorKit.ServiceInterface.csproj ├── Data │ ├── ApplicationDbContext.cs │ └── CustomUserSession.cs ├── EmailExtensions.cs ├── EmailMessageCommand.cs ├── EmailRenderer.cs ├── EmailRenderersServices.cs ├── EmailRunsServices.cs ├── EmailServices.cs ├── ImageCreator.cs ├── MailCrudServices.cs ├── MailProvider.cs ├── MailingServices.cs ├── MarkdownScripts.cs ├── MyServices.cs ├── PostServices.cs ├── SendMessagesCommand.cs └── ValidationScripts.cs ├── CreatorKit.ServiceModel ├── Admin │ └── Posts.cs ├── AppUser.cs ├── Contacts.cs ├── CreatorKit.ServiceModel.csproj ├── EmailRenderers.cs ├── EmailRuns.cs ├── Emails.cs ├── Hello.cs ├── Icons.cs ├── Mail.cs ├── Mq.cs ├── Posts.cs ├── RendererAttribute.cs ├── Tag.cs └── Types │ ├── Mail.cs │ ├── MailingList.cs │ ├── Post.cs │ └── README.md ├── CreatorKit.Tests ├── CreatorKit.Tests.csproj ├── DbTests.cs ├── IntegrationTest.cs ├── MigrationTasks.cs ├── SeedDataTasks.cs ├── UnitTest.cs └── appsettings.json ├── CreatorKit.sln ├── CreatorKit ├── Configure.AppHost.cs ├── Configure.Auth.cs ├── Configure.AutoQuery.cs ├── Configure.BackgroundJobs.cs ├── Configure.Cors.cs ├── Configure.Db.Migrations.cs ├── Configure.Db.cs ├── Configure.Extensions.cs ├── Configure.HealthChecks.cs ├── Configure.Mail.cs ├── CreatorKit.csproj ├── Migrations │ ├── Migration1000.cs │ ├── Migration1001.cs │ └── seed │ │ ├── mailinglists.csv │ │ ├── subscribers.csv │ │ └── users.csv ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json ├── appsettings.json ├── docs │ ├── docs.md │ ├── index.md │ └── install.md ├── emails │ ├── empty.html │ ├── layouts │ │ ├── basic.html │ │ ├── empty.html │ │ └── marketing.html │ ├── newsletter-welcome.html │ ├── newsletter.html │ ├── partials │ │ ├── button-centered.html │ │ ├── divider.html │ │ ├── image-centered.html │ │ ├── section-icon.html │ │ ├── section.html │ │ └── title.html │ ├── vars │ │ ├── info.txt │ │ └── urls.txt │ └── verify-email.html ├── package-lock.json ├── package.json ├── postbuild.js ├── postinstall.js ├── tailwind.config.js ├── tailwind.input.css └── wwwroot │ ├── css │ ├── app.css │ └── typography.css │ ├── img │ ├── logo.svg │ ├── mail │ │ ├── blog_48x48@2x.png │ │ ├── chat_48x48@2x.png │ │ ├── email_100x100@2x.png │ │ ├── logo_72x72@2x.png │ │ ├── logofull_350x60@2x.png │ │ ├── mail_48x48@2x.png │ │ ├── speaker_48x48@2x.png │ │ ├── twitter_24x24@2x.png │ │ ├── video_48x48@2x.png │ │ ├── website_24x24@2x.png │ │ ├── welcome_650x487.jpg │ │ ├── youtube_24x24@2x.png │ │ └── youtube_48x48@2x.png │ └── portal.png │ ├── index.html │ ├── js │ ├── servicestack-vue.min.mjs │ └── servicestack-vue.mjs │ ├── lib │ └── mjs │ │ ├── servicestack-client.min.mjs │ │ ├── servicestack-client.mjs │ │ ├── servicestack-vue.min.mjs │ │ ├── servicestack-vue.mjs │ │ ├── vue.min.mjs │ │ └── vue.mjs │ ├── login.html │ ├── mjs │ ├── Mail.dtos.mjs │ ├── Posts.dtos.mjs │ ├── app.mjs │ ├── components │ │ ├── Auth.mjs │ │ ├── Inputs.mjs │ │ ├── init.mjs │ │ ├── mail.mjs │ │ └── post.mjs │ └── dtos.mjs │ ├── modules │ ├── locode │ │ └── custom.js │ └── shared │ │ └── custom-body.html │ ├── portal │ └── index.html │ ├── stats │ └── projects.json │ └── tailwind │ └── all.components.txt ├── LICENSE.txt ├── NuGet.Config ├── README.md └── config └── deploy.yml /.github/workflows/build-container.yml: -------------------------------------------------------------------------------- 1 | name: Build Container 2 | permissions: 3 | packages: write 4 | contents: write 5 | on: 6 | workflow_run: 7 | workflows: ["Build"] 8 | types: 9 | - completed 10 | branches: 11 | - main 12 | - master 13 | workflow_dispatch: 14 | 15 | env: 16 | DOCKER_BUILDKIT: 1 17 | KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 18 | KAMAL_REGISTRY_USERNAME: ${{ github.actor }} 19 | 20 | jobs: 21 | build-container: 22 | runs-on: ubuntu-latest 23 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v3 27 | 28 | - name: Set up environment variables 29 | run: | 30 | echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV 31 | echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV 32 | echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV 33 | echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV 34 | if [ -n "${{ secrets.APPSETTINGS_PATCH }}" ]; then 35 | echo "HAS_APPSETTINGS_PATCH=true" >> $GITHUB_ENV 36 | else 37 | echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV 38 | fi 39 | if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then 40 | echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV 41 | else 42 | echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV 43 | fi 44 | 45 | # This step is for the deployment of the templates only, safe to delete 46 | - name: Modify csproj for template deploy 47 | if: env.HAS_DEPLOY_ACTION == 'true' 48 | run: | 49 | sed -i 's###g' CreatorKit/CreatorKit.csproj 50 | 51 | - name: Check for Client directory and package.json 52 | id: check_client 53 | run: | 54 | if [ -d "CreatorKit.Client" ] && [ -f "CreatorKit.Client/package.json" ]; then 55 | echo "client_exists=true" >> $GITHUB_OUTPUT 56 | else 57 | echo "client_exists=false" >> $GITHUB_OUTPUT 58 | fi 59 | 60 | - name: Setup Node.js 61 | if: steps.check_client.outputs.client_exists == 'true' 62 | uses: actions/setup-node@v3 63 | with: 64 | node-version: 22 65 | 66 | - name: Install npm dependencies 67 | if: steps.check_client.outputs.client_exists == 'true' 68 | working-directory: ./CreatorKit.Client 69 | run: npm install 70 | 71 | - name: Install x tool 72 | run: dotnet tool install -g x 73 | 74 | - name: Apply Production AppSettings 75 | if: env.HAS_APPSETTINGS_PATCH == 'true' 76 | working-directory: ./CreatorKit 77 | run: | 78 | cat <> appsettings.json.patch 79 | ${{ secrets.APPSETTINGS_PATCH }} 80 | EOF 81 | x patch appsettings.json.patch 82 | 83 | - name: Login to GitHub Container Registry 84 | uses: docker/login-action@v3 85 | with: 86 | registry: ghcr.io 87 | username: ${{ env.KAMAL_REGISTRY_USERNAME }} 88 | password: ${{ env.KAMAL_REGISTRY_PASSWORD }} 89 | 90 | - name: Setup .NET 91 | uses: actions/setup-dotnet@v3 92 | with: 93 | dotnet-version: '8.0' 94 | 95 | - name: Build and push Docker image 96 | run: | 97 | dotnet publish --os linux --arch x64 -c Release -p:ContainerRepository=${{ env.image_repository_name }} -p:ContainerRegistry=ghcr.io -p:ContainerImageTags=latest -p:ContainerPort=80 98 | -------------------------------------------------------------------------------- /.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-latest 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.*' 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: ./CreatorKit.Tests 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | permissions: 3 | packages: write 4 | contents: write 5 | on: 6 | workflow_run: 7 | workflows: ["Build Container"] 8 | types: 9 | - completed 10 | branches: 11 | - main 12 | - master 13 | workflow_dispatch: 14 | 15 | env: 16 | DOCKER_BUILDKIT: 1 17 | KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 18 | KAMAL_REGISTRY_USERNAME: ${{ github.actor }} 19 | 20 | jobs: 21 | release: 22 | runs-on: ubuntu-latest 23 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v3 27 | 28 | - name: Set up environment variables 29 | run: | 30 | echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV 31 | echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV 32 | echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV 33 | echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV 34 | if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then 35 | echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV 36 | else 37 | echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV 38 | fi 39 | if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then 40 | echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV 41 | else 42 | echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV 43 | fi 44 | 45 | # This step is for the deployment of the templates only, safe to delete 46 | - name: Modify deploy.yml 47 | if: env.HAS_DEPLOY_ACTION == 'true' 48 | run: | 49 | sed -i "s/service: creator-kit/service: ${{ env.repository_name_lower }}/g" config/deploy.yml 50 | sed -i "s#image: my-user/creatorkit#image: ${{ env.image_repository_name }}#g" config/deploy.yml 51 | sed -i "s/- 192.168.0.1/- ${{ secrets.KAMAL_DEPLOY_IP }}/g" config/deploy.yml 52 | sed -i "s/host: creator-kit.example.com/host: ${{ secrets.KAMAL_DEPLOY_HOST }}/g" config/deploy.yml 53 | sed -i "s/CreatorKit/${{ env.repository_name }}/g" config/deploy.yml 54 | 55 | - name: Login to GitHub Container Registry 56 | uses: docker/login-action@v3 57 | with: 58 | registry: ghcr.io 59 | username: ${{ env.KAMAL_REGISTRY_USERNAME }} 60 | password: ${{ env.KAMAL_REGISTRY_PASSWORD }} 61 | 62 | - name: Set up SSH key 63 | uses: webfactory/ssh-agent@v0.9.0 64 | with: 65 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 66 | 67 | - name: Setup Ruby 68 | uses: ruby/setup-ruby@v1 69 | with: 70 | ruby-version: 3.3.0 71 | bundler-cache: true 72 | 73 | - name: Install Kamal 74 | run: gem install kamal -v 2.3.0 75 | 76 | - name: Set up Docker Buildx 77 | uses: docker/setup-buildx-action@v3 78 | with: 79 | driver-opts: image=moby/buildkit:master 80 | 81 | - name: Kamal bootstrap 82 | run: kamal server bootstrap 83 | 84 | - name: Check if first run and execute kamal app boot if necessary 85 | run: | 86 | FIRST_RUN_FILE=".${{ env.repository_name }}" 87 | if ! kamal server exec --no-interactive -q "test -f $FIRST_RUN_FILE"; then 88 | kamal server exec --no-interactive -q "touch $FIRST_RUN_FILE" || true 89 | kamal deploy -q -P --version latest || true 90 | else 91 | echo "Not first run, skipping kamal app boot" 92 | fi 93 | 94 | - name: Ensure file permissions 95 | run: | 96 | kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}" 97 | 98 | - name: Migration 99 | if: env.HAS_MIGRATIONS == 'true' 100 | run: | 101 | kamal server exec --no-interactive 'echo "${{ env.KAMAL_REGISTRY_PASSWORD }}" | docker login ghcr.io -u ${{ env.KAMAL_REGISTRY_USERNAME }} --password-stdin' 102 | kamal server exec --no-interactive "docker pull ghcr.io/${{ env.image_repository_name }}:latest || true" 103 | kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate" 104 | 105 | - name: Deploy with Kamal 106 | run: | 107 | kamal lock release -v 108 | kamal deploy -P --version latest -------------------------------------------------------------------------------- /.kamal/hooks/docker-setup.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Docker set up on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/hooks/post-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample post-deploy hook 4 | # 5 | # These environment variables are available: 6 | # KAMAL_RECORDED_AT 7 | # KAMAL_PERFORMER 8 | # KAMAL_VERSION 9 | # KAMAL_HOSTS 10 | # KAMAL_ROLE (if set) 11 | # KAMAL_DESTINATION (if set) 12 | # KAMAL_RUNTIME 13 | 14 | echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" 15 | -------------------------------------------------------------------------------- /.kamal/hooks/post-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooted kamal-proxy on $KAMAL_HOSTS" 4 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-build.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample pre-build hook 4 | # 5 | # Checks: 6 | # 1. We have a clean checkout 7 | # 2. A remote is configured 8 | # 3. The branch has been pushed to the remote 9 | # 4. The version we are deploying matches the remote 10 | # 11 | # These environment variables are available: 12 | # KAMAL_RECORDED_AT 13 | # KAMAL_PERFORMER 14 | # KAMAL_VERSION 15 | # KAMAL_HOSTS 16 | # KAMAL_ROLE (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | if [ -n "$(git status --porcelain)" ]; then 20 | echo "Git checkout is not clean, aborting..." >&2 21 | git status --porcelain >&2 22 | exit 1 23 | fi 24 | 25 | first_remote=$(git remote) 26 | 27 | if [ -z "$first_remote" ]; then 28 | echo "No git remote set, aborting..." >&2 29 | exit 1 30 | fi 31 | 32 | current_branch=$(git branch --show-current) 33 | 34 | if [ -z "$current_branch" ]; then 35 | echo "Not on a git branch, aborting..." >&2 36 | exit 1 37 | fi 38 | 39 | remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) 40 | 41 | if [ -z "$remote_head" ]; then 42 | echo "Branch not pushed to remote, aborting..." >&2 43 | exit 1 44 | fi 45 | 46 | if [ "$KAMAL_VERSION" != "$remote_head" ]; then 47 | echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 48 | exit 1 49 | fi 50 | 51 | exit 0 52 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-connect.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-connect check 4 | # 5 | # Warms DNS before connecting to hosts in parallel 6 | # 7 | # These environment variables are available: 8 | # KAMAL_RECORDED_AT 9 | # KAMAL_PERFORMER 10 | # KAMAL_VERSION 11 | # KAMAL_HOSTS 12 | # KAMAL_ROLE (if set) 13 | # KAMAL_DESTINATION (if set) 14 | # KAMAL_RUNTIME 15 | 16 | hosts = ENV["KAMAL_HOSTS"].split(",") 17 | results = nil 18 | max = 3 19 | 20 | elapsed = Benchmark.realtime do 21 | results = hosts.map do |host| 22 | Thread.new do 23 | tries = 1 24 | 25 | begin 26 | Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) 27 | rescue SocketError 28 | if tries < max 29 | puts "Retrying DNS warmup: #{host}" 30 | tries += 1 31 | sleep rand 32 | retry 33 | else 34 | puts "DNS warmup failed: #{host}" 35 | host 36 | end 37 | end 38 | 39 | tries 40 | end 41 | end.map(&:value) 42 | end 43 | 44 | retries = results.sum - hosts.size 45 | nopes = results.count { |r| r == max } 46 | 47 | puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] 48 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-deploy hook 4 | # 5 | # Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. 6 | # 7 | # Fails unless the combined status is "success" 8 | # 9 | # These environment variables are available: 10 | # KAMAL_RECORDED_AT 11 | # KAMAL_PERFORMER 12 | # KAMAL_VERSION 13 | # KAMAL_HOSTS 14 | # KAMAL_COMMAND 15 | # KAMAL_SUBCOMMAND 16 | # KAMAL_ROLE (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | # Only check the build status for production deployments 20 | if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" 21 | exit 0 22 | end 23 | 24 | require "bundler/inline" 25 | 26 | # true = install gems so this is fast on repeat invocations 27 | gemfile(true, quiet: true) do 28 | source "https://rubygems.org" 29 | 30 | gem "octokit" 31 | gem "faraday-retry" 32 | end 33 | 34 | MAX_ATTEMPTS = 72 35 | ATTEMPTS_GAP = 10 36 | 37 | def exit_with_error(message) 38 | $stderr.puts message 39 | exit 1 40 | end 41 | 42 | class GithubStatusChecks 43 | attr_reader :remote_url, :git_sha, :github_client, :combined_status 44 | 45 | def initialize 46 | @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") 47 | @git_sha = `git rev-parse HEAD`.strip 48 | @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) 49 | refresh! 50 | end 51 | 52 | def refresh! 53 | @combined_status = github_client.combined_status(remote_url, git_sha) 54 | end 55 | 56 | def state 57 | combined_status[:state] 58 | end 59 | 60 | def first_status_url 61 | first_status = combined_status[:statuses].find { |status| status[:state] == state } 62 | first_status && first_status[:target_url] 63 | end 64 | 65 | def complete_count 66 | combined_status[:statuses].count { |status| status[:state] != "pending"} 67 | end 68 | 69 | def total_count 70 | combined_status[:statuses].count 71 | end 72 | 73 | def current_status 74 | if total_count > 0 75 | "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." 76 | else 77 | "Build not started..." 78 | end 79 | end 80 | end 81 | 82 | 83 | $stdout.sync = true 84 | 85 | puts "Checking build status..." 86 | attempts = 0 87 | checks = GithubStatusChecks.new 88 | 89 | begin 90 | loop do 91 | case checks.state 92 | when "success" 93 | puts "Checks passed, see #{checks.first_status_url}" 94 | exit 0 95 | when "failure" 96 | exit_with_error "Checks failed, see #{checks.first_status_url}" 97 | when "pending" 98 | attempts += 1 99 | end 100 | 101 | exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS 102 | 103 | puts checks.current_status 104 | sleep(ATTEMPTS_GAP) 105 | checks.refresh! 106 | end 107 | rescue Octokit::NotFound 108 | exit_with_error "Build status could not be found" 109 | end 110 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/secrets: -------------------------------------------------------------------------------- 1 | # Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, 2 | # and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either 3 | # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. 4 | 5 | # Option 1: Read secrets from the environment 6 | KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD 7 | KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME 8 | 9 | # Option 2: Read secrets via a command 10 | # RAILS_MASTER_KEY=$(cat config/master.key) 11 | 12 | # Option 3: Read secrets via kamal secrets helpers 13 | # These will handle logging in and fetching the secrets in as few calls as possible 14 | # There are adapters for 1Password, LastPass + Bitwarden 15 | # 16 | # SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) 17 | # KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS) 18 | # RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS) 19 | -------------------------------------------------------------------------------- /.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}/CreatorKit/bin/Debug/net6.0/CreatorKit.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/CreatorKit", 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 | } -------------------------------------------------------------------------------- /CreatorKit.Extensions.ServiceModel/CreatorKit.Extensions.ServiceModel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | CreatorKit.ServiceModel 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /CreatorKit.Extensions.ServiceModel/MarkdownEmail.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.ServiceModel.Types; 2 | using ServiceStack; 3 | using ServiceStack.DataAnnotations; 4 | 5 | namespace CreatorKit.ServiceModel; 6 | 7 | [Renderer(typeof(RenderCustomHtml), Layout = "basic", Template="empty")] 8 | [Tag(Tag.Mail), ValidateIsAdmin] 9 | [Icon(Svg = Icons.TextMarkup)] 10 | [Description("Markdown Email")] 11 | public class MarkdownEmail : CreateEmailBase, IPost, IReturn 12 | { 13 | [ValidateNotEmpty] 14 | [FieldCss(Field = "col-span-12")] 15 | public string Subject { get; set; } 16 | 17 | [ValidateNotEmpty] 18 | [Input(Type = "MarkdownEmailInput", Label = ""), FieldCss(Field = "col-span-12", Input = "h-56")] 19 | public string? Body { get; set; } 20 | public bool? Draft { get; set; } 21 | } -------------------------------------------------------------------------------- /CreatorKit.Extensions.ServiceModel/NewsletterMailRun.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack; 3 | using ServiceStack.DataAnnotations; 4 | 5 | namespace CreatorKit.ServiceModel; 6 | 7 | [Renderer(typeof(RenderNewsletter))] 8 | [Tag(Tag.Emails)] 9 | [ValidateIsAdmin] 10 | [Description("Generate Newsletter")] 11 | [Icon(Svg = Icons.Newsletter)] 12 | public class NewsletterMailRun : MailRunBase, IPost, IReturn 13 | { 14 | [ValidateNotEmpty] 15 | public DateTime? FromDate { get; set; } 16 | public DateTime? ToDate { get; set; } 17 | [Input(Type = "MarkdownEmailInput"), FieldCss(Field = "col-span-12", Input = "h-56")] 18 | public string? Header { get; set; } 19 | [Input(Type = "MarkdownEmailInput"), FieldCss(Field = "col-span-12", Input = "h-56")] 20 | public string? Footer { get; set; } 21 | } -------------------------------------------------------------------------------- /CreatorKit.Extensions.ServiceModel/RenderNewsletter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack; 3 | using ServiceStack.DataAnnotations; 4 | 5 | namespace CreatorKit.ServiceModel; 6 | 7 | [Tag(Tag.Mail), ValidateIsAdmin, ExcludeMetadata] 8 | public class RenderNewsletter : RenderEmailBase, IGet, IReturn 9 | { 10 | [ValidateNotEmpty] 11 | public DateTime? FromDate { get; set; } 12 | public DateTime? ToDate { get; set; } 13 | public string? Header { get; set; } 14 | public string? Footer { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /CreatorKit.Extensions.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. -------------------------------------------------------------------------------- /CreatorKit.Extensions.ServiceModel/Website.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ServiceStack; 4 | 5 | namespace CreatorKit.ServiceModel; 6 | 7 | [Tag(Tag.Mail), ValidateIsAdmin] 8 | public class ViewMailData 9 | { 10 | public int? Year { get; set; } 11 | public int? Month { get; set; } 12 | public bool? Force { get; set; } 13 | } 14 | 15 | public class ViewMailDataResponse 16 | { 17 | public DateTime LastUpdated { get; set; } 18 | public SiteMeta SiteMeta { get; set; } 19 | } 20 | 21 | public class SiteMeta 22 | { 23 | public DateTime CreatedDate { get; set; } 24 | public List Pages { get; set; } 25 | public List Posts { get; set; } 26 | public List WhatsNew { get; set; } 27 | public List Videos { get; set; } 28 | } 29 | 30 | public class MarkdownFile 31 | { 32 | public string Slug { get; set; } 33 | public string? Layout { get; set; } 34 | public bool Draft { get; set; } 35 | public string Title { get; set; } 36 | public string Summary { get; set; } 37 | public string? Content { get; set; } 38 | public string? Image { get; set; } 39 | public string? Author { get; set; } 40 | public List? Tags { get; set; } = new(); 41 | public DateTime Date { get; set; } 42 | public string? Url { get; set; } 43 | public string? Group { get; set; } 44 | public int? Order { get; set; } 45 | public int? WordCount { get; set; } 46 | public int? LineCount { get; set; } 47 | public int MinutesToRead => (int)Math.Ceiling((WordCount ?? 1) / (double)225); 48 | } 49 | -------------------------------------------------------------------------------- /CreatorKit.Extensions/CreatorKit.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /CreatorKit.Extensions/CustomEmailRunServices.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.ServiceInterface; 2 | using CreatorKit.ServiceModel; 3 | using CreatorKit.ServiceModel.Types; 4 | using ServiceStack; 5 | 6 | namespace CreatorKit.Extensions; 7 | 8 | public class CustomEmailRunServices(EmailRenderer renderer, IMailProvider mail) 9 | : Service 10 | { 11 | public object Any(NewsletterMailRun request) 12 | { 13 | var newsletterDate = request.ToDate ?? DateTime.UtcNow; 14 | var response = renderer.CreateMailRunResponse(); 15 | 16 | using var mailDb = mail.OpenMonthDb(); 17 | var mailRun = renderer.CreateMailRun(mailDb, new MailRun { 18 | Layout = "marketing", 19 | Template = "newsletter", 20 | }, request); 21 | 22 | foreach (var sub in Db.GetActiveSubscribers(request.MailingList)) 23 | { 24 | var viewRequest = request.ConvertTo().FromContact(sub); 25 | var bodyHtml = (string) Gateway.Send(typeof(string), viewRequest); 26 | response.AddMessage(renderer.CreateMessageRun(mailDb, new MailMessageRun 27 | { 28 | Message = new EmailMessage 29 | { 30 | To = sub.ToMailTos(), 31 | Subject = string.Format(AppData.Info.NewsletterFmt, $"{newsletterDate:MMMM} {newsletterDate:yyyy}"), 32 | BodyHtml = bodyHtml, 33 | } 34 | }.FromRequest(viewRequest), mailRun, sub)); 35 | } 36 | 37 | mailDb.CompletedMailRun(mailRun, response); 38 | return response; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CreatorKit.Extensions/CustomEmailServices.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.ServiceInterface; 2 | using CreatorKit.ServiceModel; 3 | using CreatorKit.ServiceModel.Types; 4 | using ServiceStack; 5 | 6 | namespace CreatorKit.Extensions; 7 | 8 | public class CustomEmailServices(EmailRenderer renderer, IMailProvider mail) : Service 9 | { 10 | public object Any(MarkdownEmail request) 11 | { 12 | var contact = Db.GetOrCreateContact(request); 13 | var viewRequest = request.ConvertTo().FromContact(contact); 14 | viewRequest.Layout = "basic"; 15 | viewRequest.Template = "empty"; 16 | var bodyHtml = (string)Gateway.Send(typeof(string), viewRequest); 17 | 18 | using var mailDb = mail.OpenMonthDb(); 19 | var email = renderer.CreateMessage(mailDb, new MailMessage 20 | { 21 | Draft = request.Draft ?? false, 22 | Message = new EmailMessage 23 | { 24 | To = contact.ToMailTos(), 25 | Subject = request.Subject, 26 | Body = request.Body, 27 | BodyHtml = bodyHtml, 28 | }, 29 | }.FromRequest(viewRequest)); 30 | return email; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CreatorKit.Extensions/CustomRendererServices.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.ServiceInterface; 2 | using CreatorKit.ServiceModel; 3 | using ServiceStack; 4 | using ServiceStack.Script; 5 | 6 | namespace CreatorKit.Extensions; 7 | 8 | public class CustomRendererServices(EmailRenderer renderer, WebsiteData data) : Service 9 | { 10 | public object Any(RenderDoc request) => WebsiteData.RenderDoc(VirtualFiles, request.Page); 11 | 12 | public async Task Any(ViewMailData request) 13 | { 14 | if (request.Force == true) 15 | data.MetaCache.Clear(); 16 | 17 | var year = request.Year ?? DateTime.UtcNow.Year; 18 | var fromDate = new DateTime(year, request.Month ?? 1, 1); 19 | var meta = await data.SearchAsync(fromDate: fromDate, 20 | toDate: request.Month != null ? new DateTime(year, request.Month.Value, 1).AddMonths(1) : null); 21 | 22 | return new ViewMailDataResponse 23 | { 24 | LastUpdated = data.LastUpdated, 25 | SiteMeta = meta, 26 | }; 27 | } 28 | 29 | public async Task Any(RenderNewsletter request) 30 | { 31 | var fromDate = request.FromDate!; 32 | var toDate = request.ToDate ?? DateTime.UtcNow; 33 | var meta = await data.SearchAsync(fromDate: fromDate, toDate: toDate); 34 | 35 | var context = renderer.CreateMailContext(layout:"marketing", page:"newsletter", 36 | args:new() { 37 | ["meta"] = meta 38 | }); 39 | 40 | return renderer.RenderToHtmlResult(Db, context, request, args: new() { 41 | ["title"] = $"{toDate:MMMM} {toDate:yyyy}", 42 | ["header"] = request.Header != null ? await context.RenderScriptAsync(request.Header, request.ToObjectDictionary()) : null, 43 | ["footer"] = request.Footer != null ? await context.RenderScriptAsync(request.Footer, request.ToObjectDictionary()) : null, 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CreatorKit.Extensions/WebsiteData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using ServiceStack; 3 | using CreatorKit.ServiceModel; 4 | using Markdig; 5 | using Markdig.Syntax; 6 | using ServiceStack.IO; 7 | 8 | namespace CreatorKit.ServiceInterface; 9 | 10 | public class WebsiteData 11 | { 12 | public DateTime LastUpdated { get; set; } 13 | public AppData AppData { get; } 14 | 15 | public WebsiteData(AppData appData) 16 | { 17 | AppData = appData; 18 | } 19 | 20 | public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(10); 21 | public ConcurrentDictionary MetaCache { get; } = new(); 22 | 23 | public async Task SearchAsync(DateTime? fromDate = null, DateTime? toDate = null) 24 | { 25 | var year = fromDate?.Year ?? DateTime.UtcNow.Year; 26 | var metaCache = MetaCache.TryGetValue(year, out var siteMeta) && siteMeta.CreatedDate < DateTime.UtcNow.Add(CacheDuration) 27 | ? siteMeta 28 | : null; 29 | 30 | if (metaCache == null) 31 | { 32 | var metaJson = await AppData.WebsiteBaseUrl.CombineWith($"/meta/{year}/all.json").GetJsonFromUrlAsync(); 33 | metaCache = metaJson.FromJson(); 34 | metaCache.CreatedDate = DateTime.UtcNow; 35 | MetaCache[year] = metaCache; 36 | } 37 | 38 | var results = new SiteMeta 39 | { 40 | CreatedDate = metaCache.CreatedDate, 41 | Pages = WithinRange(metaCache.Pages, fromDate, toDate).ToList(), 42 | Posts = WithinRange(metaCache.Posts, fromDate, toDate).ToList(), 43 | WhatsNew = WithinRange(metaCache.WhatsNew, fromDate, toDate).ToList(), 44 | Videos = WithinRange(metaCache.Videos, fromDate, toDate).ToList(), 45 | }; 46 | return results; 47 | } 48 | 49 | private static IEnumerable WithinRange(IEnumerable docs, DateTime? fromDate, DateTime? toDate) 50 | { 51 | if (fromDate != null) 52 | docs = docs.Where(x => x.Date >= fromDate); 53 | if (toDate != null) 54 | docs = docs.Where(x => x.Date < toDate); 55 | return docs; 56 | } 57 | 58 | private static readonly char[] InvalidFileNameChars = { '\"', '<', '>', '|', '\0', ':', '*', '?', '\\', '/' }; 59 | public static string RenderDoc(IVirtualFiles vfs, string page) 60 | { 61 | var isValid = !page.Contains("..") 62 | && page.IndexOfAny(InvalidFileNameChars) == -1; 63 | var file = isValid 64 | ? vfs.GetFile($"/docs/{page}") 65 | : null; 66 | if (file == null) 67 | throw HttpError.NotFound("File not found"); 68 | 69 | var pipeline = new MarkdownPipelineBuilder() 70 | .UseYamlFrontMatter() 71 | .UseAdvancedExtensions() 72 | .Build(); 73 | 74 | var writer = new StringWriter(); 75 | var renderer = new Markdig.Renderers.HtmlRenderer(writer); 76 | pipeline.Setup(renderer); 77 | 78 | var content = file.ReadAllText(); 79 | var document = Markdown.Parse(content, pipeline); 80 | renderer.Render(document); 81 | 82 | var block = document 83 | .Descendants() 84 | .FirstOrDefault(); 85 | 86 | var doc = block? 87 | .Lines // StringLineGroup[] 88 | .Lines // StringLine[] 89 | .Select(x => $"{x}\n") 90 | .ToList() 91 | .Select(x => x.Replace("---", string.Empty)) 92 | .Where(x => !string.IsNullOrWhiteSpace(x)) 93 | .Select(x => KeyValuePairs.Create(x.LeftPart(':').Trim(), x.RightPart(':').Trim())) 94 | .ToObjectDictionary() 95 | .ConvertTo(); 96 | 97 | var html = writer.ToString(); 98 | return html; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/AdminServices.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CreatorKit.ServiceModel; 4 | using CreatorKit.ServiceModel.Admin; 5 | using CreatorKit.ServiceModel.Types; 6 | using ServiceStack; 7 | using ServiceStack.OrmLite; 8 | 9 | namespace CreatorKit.ServiceInterface; 10 | 11 | public class AdminServices(IAutoQueryDb autoQuery) : Service 12 | { 13 | public object Any(AdminUpdateCommentReport request) 14 | { 15 | var userId = Request.GetRequiredUserId(); 16 | var report = Db.SingleById(request.Id); 17 | if (request.Moderation == ModerationDecision.Delete) 18 | { 19 | Db.UpdateOnly(() => new Comment { 20 | DeletedDate = DateTime.UtcNow, DeletedBy = $"{userId}", Notes = request.Notes 21 | }, where: x => x.Id == report.CommentId); 22 | } 23 | else if (request.Moderation == ModerationDecision.Flag) 24 | { 25 | Db.UpdateOnly(() => new Comment { 26 | FlagReason = request.Moderation.ToString(), 27 | Notes = request.Notes, 28 | }, 29 | where: x => x.Id == report.CommentId); 30 | } 31 | else if (request.Moderation is ModerationDecision.Ban1Day or ModerationDecision.Ban1Month or ModerationDecision.Ban1Week) 32 | { 33 | var banUntil = request.Moderation switch { 34 | ModerationDecision.Ban1Day => DateTime.UtcNow.AddDays(1), 35 | ModerationDecision.Ban1Week => DateTime.UtcNow.AddDays(7), 36 | ModerationDecision.Ban1Month => DateTime.UtcNow.AddDays(30), 37 | _ => throw new NotSupportedException() 38 | }; 39 | var comment = Db.SingleById(report.CommentId); 40 | Db.UpdateOnly(() => new AppUser { BanUntilDate = banUntil }, 41 | where: x => x.Id == comment.AppUserId); 42 | AppData.Instance.BannedUsersMap[comment.AppUserId] = banUntil; 43 | } 44 | else if (request.Moderation == ModerationDecision.PermanentBan) 45 | { 46 | var comment = Db.SingleById(report.CommentId); 47 | Db.UpdateOnly(() => new AppUser { LockedDate = DateTime.UtcNow }, 48 | where: x => x.Id == comment.AppUserId); 49 | AppData.Instance.BannedUsersMap[comment.AppUserId] = DateTime.UtcNow; 50 | } 51 | return autoQuery.Patch(request, base.Request); 52 | } 53 | } -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/AppData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using ServiceStack; 8 | using ServiceStack.Html; 9 | using ServiceStack.IO; 10 | using CreatorKit.ServiceModel; 11 | using CreatorKit.ServiceModel.Types; 12 | using Microsoft.Extensions.Configuration; 13 | using ServiceStack.Auth; 14 | 15 | namespace CreatorKit.ServiceInterface; 16 | 17 | public class AppData 18 | { 19 | public static AppData Instance { get; private set; } = new(); 20 | 21 | public static void Set(IConfiguration config) 22 | { 23 | var instance = new AppData(); 24 | config.Bind(nameof(AppData), instance); 25 | Set(instance); 26 | } 27 | 28 | public static void Set(AppData instance) 29 | { 30 | instance.ReplaceTokensInVars["{{" + nameof(PublicBaseUrl) + "}}"] = instance.PublicBaseUrl; 31 | instance.ReplaceTokensInVars["{{" + nameof(BaseUrl) + "}}"] = instance.BaseUrl; 32 | instance.ReplaceTokensInVars["{{" + nameof(WebsiteBaseUrl) + "}}"] = instance.WebsiteBaseUrl; 33 | Instance = instance; 34 | } 35 | 36 | public string WebsiteBaseUrl { get; init; } 37 | public string BaseUrl { get; init; } 38 | /// 39 | /// Images in emails need to be hosted from publicly accessible URLs 40 | /// 41 | public string PublicBaseUrl { get; init; } 42 | public List? AllowOrigins { get; init; } 43 | 44 | public Dictionary ReplaceTokensInVars { get; set; } = new(); 45 | 46 | public TimeSpan PeriodicTasksInterval { get; set; } = TimeSpan.FromMinutes(10); 47 | public List EmailLayouts { get; set; } = new(); 48 | public List EmailPartials { get; set; } = new(); 49 | public List EmailTemplates { get; set; } = new(); 50 | public List EmailVars { get; set; } = new(); 51 | 52 | public List EmailLayoutOptions => EmailLayouts.Map(x => x.WithoutExtension()); 53 | public List EmailTemplateOptions => EmailTemplates.Map(x => x.WithoutExtension()); 54 | public List RenderEmailApis { get; set; } = new(); 55 | public List MailRunGeneratorApis { get; set; } = new(); 56 | public KeyValuePair[] MailingListOptions => Input.GetEnumEntries(typeof(MailingList)); 57 | public string[]? MailingListValues => Input.GetEnumValues(typeof(MailingList)); 58 | 59 | public Dictionary> Vars { get; set; } = new(); 60 | 61 | public ConcurrentDictionary BannedUsersMap { get; set; } = new(); 62 | 63 | public IVirtualDirectory EmailsDir { get; set; } 64 | public IVirtualDirectory EmailImagesDir { get; set; } 65 | 66 | public void Load(ServiceStackHost appHost, IVirtualDirectory emailsDir, IVirtualDirectory emailImagesDir) 67 | { 68 | EmailsDir = emailsDir; 69 | EmailImagesDir = emailImagesDir; 70 | 71 | RenderEmailApis = appHost.Metadata.RequestTypes.Where(x => typeof(RenderEmailBase).IsAssignableFrom(x)).Map(x => x.Name); 72 | MailRunGeneratorApis = appHost.Metadata.RequestTypes.Where(x => typeof(MailRunBase).IsAssignableFrom(x)).Map(x => x.Name); 73 | 74 | EmailLayouts.Clear(); 75 | EmailPartials.Clear(); 76 | EmailTemplates.Clear(); 77 | 78 | var files = emailsDir.GetFiles(); 79 | foreach (var file in files.Where(x => x.Name.EndsWith(".html"))) 80 | { 81 | EmailTemplates.Add(file.Name); 82 | } 83 | foreach (var file in emailsDir.GetDirectory("layouts").GetFiles().Where(x => x.Name.EndsWith(".html"))) 84 | { 85 | EmailLayouts.Add(file.Name); 86 | } 87 | foreach (var file in emailsDir.GetDirectory("partials").GetFiles().Where(x => x.Name.EndsWith(".html"))) 88 | { 89 | EmailPartials.Add(file.Name); 90 | } 91 | foreach (var file in emailsDir.GetDirectory("vars").GetFiles().Where(x => x.Name.EndsWith(".txt"))) 92 | { 93 | EmailVars.Add(file.Name); 94 | } 95 | LoadVars(); 96 | } 97 | 98 | public static string Var(string collection, string name) => Instance.Vars[collection][name]; 99 | public static class Info 100 | { 101 | public static string Company => Var("info", "Company"); 102 | public static string NewsletterFmt => Var("info", "NewsletterFmt"); 103 | } 104 | public static class Urls 105 | { 106 | public static string SignupConfirmed => Var("urls", "SignupConfirmed"); 107 | } 108 | 109 | public void LoadVars() 110 | { 111 | Vars.Clear(); 112 | foreach (var file in EmailsDir.GetDirectory("vars").GetFiles().Where(x => x.Name.EndsWith(".txt"))) 113 | { 114 | using var fs = file.OpenRead(); 115 | var contents = fs.ReadToEnd(); 116 | foreach (var token in ReplaceTokensInVars) 117 | { 118 | contents = contents.Replace(token.Key, token.Value); 119 | } 120 | 121 | Vars[file.Name.WithoutExtension()] = new(contents.ParseAsKeyValues()); 122 | } 123 | 124 | var images = Vars["images"] = new Dictionary(); 125 | var imgBaseUrl = PublicBaseUrl.CombineWith(EmailImagesDir.VirtualPath); 126 | var delims = new[] { '@', '.', '-' }; 127 | foreach (var file in EmailImagesDir.GetFiles()) 128 | { 129 | var splitOn = file.Name.IndexOfAny(delims); 130 | if (splitOn == -1) continue; 131 | 132 | var varName = file.Name.Substring(0, splitOn); 133 | images[varName] = imgBaseUrl.CombineWith(file.Name); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/AppExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using ServiceStack; 3 | using ServiceStack.Web; 4 | 5 | namespace CreatorKit.ServiceInterface; 6 | 7 | public static class AppExtensions 8 | { 9 | public static int? GetUserId(this IRequest? req) 10 | { 11 | var user = req.GetClaimsPrincipal(); 12 | return user.IsAuthenticated() 13 | ? user.GetUserId().ToInt() 14 | : null; 15 | } 16 | 17 | public static int GetRequiredUserId(this IRequest? req) => 18 | req.GetClaimsPrincipal().GetUserId().ToInt(); 19 | 20 | public static string? GetNickName(this ClaimsPrincipal? principal) => 21 | principal?.FindFirst(JwtClaimTypes.NickName)?.Value ?? principal.GetDisplayName(); 22 | } 23 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/ContactServices.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using CreatorKit.ServiceModel; 5 | using CreatorKit.ServiceModel.Types; 6 | using ServiceStack; 7 | using ServiceStack.OrmLite; 8 | 9 | namespace CreatorKit.ServiceInterface; 10 | 11 | public class ContactServices : Service 12 | { 13 | public async Task Any(InvalidateEmails request) 14 | { 15 | if (!request.Emails.IsEmpty()) 16 | { 17 | var invalidEmails = request.Emails.Map(x => new InvalidEmail 18 | { 19 | Email = x, 20 | EmailLower = x.ToLower(), 21 | Status = request.Status, 22 | }); 23 | var existingInvalidEmails = await Db.ColumnDistinctAsync(Db.From() 24 | .Select(x => x.EmailLower)); 25 | foreach (var invalidEmail in invalidEmails) 26 | { 27 | if (existingInvalidEmails.Contains(invalidEmail.EmailLower)) 28 | continue; 29 | await Db.InsertAsync(invalidEmail); 30 | existingInvalidEmails.Add(invalidEmail.EmailLower); 31 | } 32 | 33 | var lowerEmails = invalidEmails.Map(x => x.EmailLower); 34 | await Db.DeleteAsync(x => lowerEmails.Contains(x.EmailLower)); 35 | } 36 | return new ErrorResponse(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/CreatorKit.ServiceInterface.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.ServiceModel; 2 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace CreatorKit.Data; 6 | 7 | public class ApplicationDbContext(DbContextOptions options) 8 | : IdentityDbContext(options) 9 | { 10 | protected override void OnModelCreating(ModelBuilder builder) 11 | { 12 | base.OnModelCreating(builder); 13 | builder.Entity() 14 | .HasIndex(x => x.Handle) 15 | .IsUnique(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/Data/CustomUserSession.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Claims; 4 | using System.Threading.Tasks; 5 | using CreatorKit.ServiceModel; 6 | using CreatorKit.ServiceModel.Types; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.AspNetCore.Identity.UI.Services; 9 | using Microsoft.Extensions.Options; 10 | using ServiceStack; 11 | using ServiceStack.Web; 12 | 13 | namespace CreatorKit.Data; 14 | 15 | // Add any additional metadata properties you want to store in the Users Typed Session 16 | public class CustomUserSession : AuthUserSession 17 | { 18 | public string Handle { get; set; } 19 | public string Avatar { get; set; } 20 | public DateTime? LockedDate { get; set; } 21 | public DateTime? BanUntilDate { get; set; } 22 | 23 | public int GetUserId() => UserAuthId.ToInt(); 24 | public override void PopulateFromClaims(IRequest httpReq, ClaimsPrincipal principal) 25 | { 26 | // Populate Session with data from Identity Auth Claims 27 | ProfileUrl = principal.FindFirstValue(JwtClaimTypes.Picture); 28 | } 29 | } 30 | 31 | public static class UsersExtensions 32 | { 33 | public static CustomUserSession ToUserSession(this AppUser appUser) 34 | { 35 | var session = appUser.ConvertTo(); 36 | session.Id = SessionExtensions.CreateRandomSessionId(); 37 | session.IsAuthenticated = true; 38 | session.FromToken = true; // use embedded roles 39 | return session; 40 | } 41 | } 42 | 43 | 44 | /// 45 | /// Add additional claims to the Identity Auth Cookie 46 | /// 47 | public class AdditionalUserClaimsPrincipalFactory( 48 | UserManager userManager, 49 | RoleManager roleManager, 50 | IOptions optionsAccessor) 51 | : UserClaimsPrincipalFactory(userManager, roleManager, optionsAccessor) 52 | { 53 | public override async Task CreateAsync(AppUser user) 54 | { 55 | var principal = await base.CreateAsync(user); 56 | var identity = (ClaimsIdentity)principal.Identity!; 57 | 58 | var claims = new List(); 59 | // Add additional claims here 60 | if (user.DisplayName != null) 61 | { 62 | claims.Add(new Claim(JwtClaimTypes.NickName, user.DisplayName)); 63 | } 64 | if (user.ProfileUrl != null) 65 | { 66 | claims.Add(new Claim(JwtClaimTypes.Picture, user.ProfileUrl)); 67 | } 68 | if (user.BanUntilDate != null) 69 | { 70 | claims.Add(new Claim(nameof(AppUser.BanUntilDate), user.BanUntilDate.Value.ToString("u"))); 71 | } 72 | 73 | identity.AddClaims(claims); 74 | return principal; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/EmailExtensions.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.ServiceModel; 2 | using CreatorKit.ServiceModel.Types; 3 | 4 | namespace CreatorKit.ServiceInterface; 5 | 6 | public static class EmailExtensions 7 | { 8 | public static void AddMessage(this MailRunResponse ret, MailMessageRun msg) => 9 | ret.CreatedIds.Add(msg.Id); 10 | } -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/EmailMessageCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net.Mail; 3 | using System.Net.Mime; 4 | using CreatorKit.ServiceModel.Types; 5 | using ServiceStack; 6 | 7 | namespace CreatorKit.ServiceInterface; 8 | 9 | [Worker("smtp")] 10 | public class EmailMessageCommand(SmtpConfig config) : SyncCommand 11 | { 12 | protected override void Run(EmailMessage request) 13 | { 14 | using var client = new SmtpClient(config.Host, config.Port); 15 | client.Credentials = new System.Net.NetworkCredential(config.Username, config.Password); 16 | client.EnableSsl = true; 17 | 18 | var emailTo = request.To.First().ToMailAddress(); 19 | var emailFrom = (request.From ?? new MailTo { Email = config.FromEmail, Name = config.FromName! }).ToMailAddress(); 20 | 21 | var msg = new System.Net.Mail.MailMessage(emailFrom, emailTo) 22 | { 23 | Subject = request.Subject, 24 | Body = request.BodyText ?? request.BodyHtml, 25 | IsBodyHtml = request.BodyText == null, 26 | }; 27 | 28 | if (!msg.IsBodyHtml && request.BodyHtml != null) 29 | { 30 | var mimeType = new ContentType(MimeTypes.Html); 31 | var alternate = AlternateView.CreateAlternateViewFromString(request.BodyHtml, mimeType); 32 | msg.AlternateViews.Add(alternate); 33 | } 34 | 35 | foreach (var to in request.To.Skip(1)) 36 | { 37 | msg.To.Add(to.ToMailAddress()); 38 | } 39 | foreach (var cc in request.Cc.Safe()) 40 | { 41 | msg.CC.Add(cc.ToMailAddress()); 42 | } 43 | foreach (var bcc in request.Bcc.Safe()) 44 | { 45 | msg.Bcc.Add(bcc.ToMailAddress()); 46 | } 47 | if (!string.IsNullOrEmpty(config.Bcc)) 48 | { 49 | msg.Bcc.Add(new MailAddress(config.Bcc)); 50 | } 51 | 52 | client.Send(msg); 53 | } 54 | } 55 | 56 | public static class EmailUtils 57 | { 58 | public static MailAddress ToMailAddress(this MailTo from) 59 | { 60 | return string.IsNullOrEmpty(from.Name) 61 | ? new MailAddress(from.Email) 62 | : new MailAddress(from.Email, from.Name); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/EmailRunsServices.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using ServiceStack; 3 | using CreatorKit.ServiceModel; 4 | using CreatorKit.ServiceModel.Types; 5 | 6 | namespace CreatorKit.ServiceInterface; 7 | 8 | public class EmailRunsServices(EmailRenderer renderer, IMailProvider mail) : Service 9 | { 10 | public object Any(SimpleTextMailRun request) 11 | { 12 | var response = renderer.CreateMailRunResponse(); 13 | 14 | using var mailDb = mail.OpenMonthDb(); 15 | var mailRun = renderer.CreateMailRun(mailDb, new MailRun(), request); 16 | foreach (var sub in Db.GetActiveSubscribers(request.MailingList)) 17 | { 18 | var viewRequest = request.ConvertTo().FromContact(sub); 19 | var bodyHtml = (string) Gateway.Send(typeof(string), viewRequest); 20 | 21 | response.AddMessage(renderer.CreateMessageRun(mailDb, new MailMessageRun 22 | { 23 | Message = new EmailMessage 24 | { 25 | To = sub.ToMailTos(), 26 | Subject = request.Subject, 27 | Body = request.Body, 28 | BodyText = bodyHtml, 29 | } 30 | }, mailRun, sub)); 31 | } 32 | 33 | mailDb.CompletedMailRun(mailRun, response); 34 | return response; 35 | } 36 | 37 | public object Any(MarkdownMailRun request) 38 | { 39 | var to = request.ConvertTo(); 40 | to.Layout = "basic"; 41 | to.Template = "empty"; 42 | return Any(to); 43 | } 44 | 45 | public object Any(CustomHtmlMailRun request) 46 | { 47 | var response = renderer.CreateMailRunResponse(); 48 | 49 | using var mailDb = mail.OpenMonthDb(); 50 | var mailRun = renderer.CreateMailRun(mailDb, new MailRun { 51 | Layout = request.Layout, 52 | Template = request.Template, 53 | }, request); 54 | 55 | foreach (var sub in Db.GetActiveSubscribers(request.MailingList)) 56 | { 57 | var viewRequest = request.ConvertTo().FromContact(sub); 58 | var bodyHtml = (string) Gateway.Send(typeof(string), viewRequest); 59 | 60 | response.AddMessage(renderer.CreateMessageRun(mailDb, new MailMessageRun 61 | { 62 | Message = new EmailMessage 63 | { 64 | To = sub.ToMailTos(), 65 | Subject = request.Subject, 66 | Body = request.Body, 67 | BodyHtml = bodyHtml, 68 | } 69 | }.FromRequest(viewRequest), mailRun, sub)); 70 | } 71 | 72 | mailDb.CompletedMailRun(mailRun, response); 73 | return response; 74 | } 75 | } -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/EmailServices.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mail; 2 | using System.Threading; 3 | using Microsoft.Extensions.Logging; 4 | using ServiceStack; 5 | using ServiceStack.Jobs; 6 | 7 | namespace CreatorKit.ServiceInterface; 8 | 9 | /// 10 | /// Configuration for sending emails using SMTP servers in EmailServices 11 | /// E.g. for managed services like Amazon (SES): https://aws.amazon.com/ses/ or https://mailtrap.io 12 | /// 13 | public class SmtpConfig 14 | { 15 | /// 16 | /// Username of the SMTP Server account 17 | /// 18 | public string Username { get; set; } 19 | /// 20 | /// Password of the SMTP Server account 21 | /// 22 | public string Password { get; set; } 23 | /// 24 | /// Hostname of the SMTP Server 25 | /// 26 | public string Host { get; set; } 27 | /// 28 | /// Port of the SMTP Server 29 | /// 30 | public int Port { get; set; } = 587; 31 | /// 32 | /// Which email address to send emails from 33 | /// 34 | public string FromEmail { get; set; } 35 | /// 36 | /// The name of the Email Sender 37 | /// 38 | public string? FromName { get; set; } 39 | /// 40 | /// Keep a copy of all emails sent by BCC'ing a copy to this email address 41 | /// 42 | public string? Bcc { get; set; } 43 | /// 44 | /// Prevent emails from being sent to real users during development by sending to this Dev email instead 45 | /// 46 | public string? DevToEmail { get; set; } 47 | /// 48 | /// Send notifications of new Thread Comments and Reports 49 | /// 50 | public string? NotificationsEmail { get; set; } 51 | } 52 | 53 | public class SendEmail 54 | { 55 | public string To { get; set; } 56 | public string? ToName { get; set; } 57 | public string Subject { get; set; } 58 | public string? BodyText { get; set; } 59 | public string? BodyHtml { get; set; } 60 | } 61 | 62 | [Worker("smtp")] 63 | public class SendEmailCommand(ILogger logger, IBackgroundJobs jobs, SmtpConfig config) 64 | : SyncCommand 65 | { 66 | private static long count = 0; 67 | protected override void Run(SendEmail request) 68 | { 69 | Interlocked.Increment(ref count); 70 | var log = Request.CreateJobLogger(jobs, logger); 71 | log.LogInformation("Sending {Count} email to {Email} with subject {Subject}", 72 | count, request.To, request.Subject); 73 | 74 | using var client = new SmtpClient(config.Host, config.Port); 75 | client.Credentials = new System.Net.NetworkCredential(config.Username, config.Password); 76 | client.EnableSsl = true; 77 | 78 | // If DevToEmail is set, send all emails to that address instead 79 | var emailTo = config.DevToEmail != null 80 | ? new MailAddress(config.DevToEmail) 81 | : new MailAddress(request.To, request.ToName); 82 | 83 | var emailFrom = new MailAddress(config.FromEmail, config.FromName); 84 | 85 | var msg = new MailMessage(emailFrom, emailTo) 86 | { 87 | Subject = request.Subject, 88 | Body = request.BodyHtml ?? request.BodyText, 89 | IsBodyHtml = request.BodyHtml != null, 90 | }; 91 | 92 | if (config.Bcc != null) 93 | { 94 | msg.Bcc.Add(new MailAddress(config.Bcc)); 95 | } 96 | 97 | client.Send(msg); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/ImageCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack; 3 | 4 | namespace CreatorKit.ServiceInterface; 5 | 6 | public class ImageCreator 7 | { 8 | public static ImageCreator Instance { get; } = new(); 9 | 10 | private static string[] DarkColors = 11 | [ 12 | "#334155", 13 | "#374151", 14 | "#44403c", 15 | "#b91c1c", 16 | "#c2410c", 17 | "#b45309", 18 | "#4d7c0f", 19 | "#15803d", 20 | "#047857", 21 | "#0f766e", 22 | "#0e7490", 23 | "#0369a1", 24 | "#1d4ed8", 25 | "#4338ca", 26 | "#6d28d9", 27 | "#7e22ce", 28 | "#a21caf", 29 | "#be185d", 30 | "#be123c", 31 | // 32 | "#824d26", 33 | "#865081", 34 | "#0c7047", 35 | "#0064a7", 36 | "#8220d0", 37 | "#009645", 38 | "#ab00f0", 39 | "#9a3c69", 40 | "#227632", 41 | "#4b40bd", 42 | "#ad3721", 43 | "#6710f2", 44 | "#1a658a", 45 | "#078e57", 46 | "#2721e1", 47 | "#168407", 48 | "#019454", 49 | "#967312", 50 | "#6629d8", 51 | "#108546", 52 | "#9a2aa1", 53 | "#3d7813", 54 | "#257124", 55 | "#6f14ed", 56 | "#1f781d", 57 | "#a29906", 58 | ]; 59 | 60 | public string CreateSvg(char letter, string? bgColor = null, string? textColor = null) 61 | { 62 | bgColor ??= DarkColors[new Random().Next(0, DarkColors.Length)]; 63 | textColor ??= "#FFF"; 64 | 65 | var svg = $@" 66 | 67 | {letter} 68 | "; 69 | return svg; 70 | } 71 | 72 | public string CreateSvgDataUri(char letter, string? bgColor = null, string? textColor = null) => 73 | Svg.ToDataUri(CreateSvg(letter, bgColor, textColor)); 74 | 75 | public static string Decode(string dataUri) 76 | { 77 | return dataUri 78 | .Replace("'","\"") 79 | .Replace("%25","%") 80 | .Replace("%23","#") 81 | .Replace("%3C","<") 82 | .Replace("%3E",">") 83 | .Replace("%3F","?") 84 | .Replace("%5B","[") 85 | .Replace("%5C","\\") 86 | .Replace("%5D","]") 87 | .Replace("%5E","^") 88 | .Replace("%60","`") 89 | .Replace("%7B","{") 90 | .Replace("%7C","|") 91 | .Replace("%7D","}"); 92 | } 93 | 94 | public string DataUriToSvg(string dataUri) => Decode(dataUri.RightPart(',')); 95 | } 96 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/MailCrudServices.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.ServiceModel; 2 | using CreatorKit.ServiceModel.Types; 3 | using ServiceStack; 4 | using ServiceStack.OrmLite; 5 | 6 | namespace CreatorKit.ServiceInterface; 7 | 8 | public class MailCrudServices(IMailProvider mail, IAutoQueryDb autoQuery) : Service 9 | { 10 | public object Any(QueryMailMessages request) 11 | { 12 | using var db = mail.OpenMonthDb(request.Month); 13 | var q = autoQuery.CreateQuery(request, base.Request, db); 14 | return autoQuery.Execute(request, q, base.Request, db); 15 | } 16 | 17 | public object Any(UpdateMailMessage request) 18 | { 19 | using var db = mail.OpenMonthDb(); 20 | return autoQuery.PartialUpdate(request, base.Request, db); 21 | } 22 | 23 | public object Any(DeleteMailMessages request) 24 | { 25 | using var db = mail.OpenMonthDb(request.Month); 26 | return autoQuery.Delete(request, base.Request, db); 27 | } 28 | 29 | public object Any(QueryMailRuns request) 30 | { 31 | using var db = mail.OpenMonthDb(request.Month); 32 | var q = autoQuery.CreateQuery(request, base.Request, db); 33 | return autoQuery.Execute(request, q, base.Request, db); 34 | } 35 | 36 | public object Any(CreateMailRun request) 37 | { 38 | using var db = mail.OpenMonthDb(); 39 | return autoQuery.Create(request, base.Request, db); 40 | } 41 | 42 | public object Any(UpdateMailRun request) 43 | { 44 | using var db = mail.OpenMonthDb(); 45 | return autoQuery.Update(request, base.Request, db); 46 | } 47 | 48 | public object Any(DeleteMailRun request) 49 | { 50 | using var db = mail.OpenMonthDb(request.Month); 51 | return autoQuery.Delete(request, base.Request, db); 52 | } 53 | 54 | public object Any(QueryMailRunMessages request) 55 | { 56 | using var db = mail.OpenMonthDb(request.Month); 57 | var q = autoQuery.CreateQuery(request, base.Request, db); 58 | return autoQuery.Execute(request, q, base.Request, db); 59 | } 60 | 61 | public object Any(UpdateMailRunMessage request) 62 | { 63 | using var db = mail.OpenMonthDb(); 64 | return autoQuery.PartialUpdate(request, base.Request, db); 65 | } 66 | 67 | public object Any(DeleteMailRunMessage request) 68 | { 69 | using var db = mail.OpenMonthDb(request.Month); 70 | return autoQuery.Delete(request, base.Request, db); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/MailProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using CreatorKit.ServiceModel.Types; 4 | using Microsoft.AspNetCore.Hosting; 5 | using ServiceStack; 6 | using ServiceStack.Data; 7 | using ServiceStack.OrmLite; 8 | 9 | namespace CreatorKit.ServiceInterface; 10 | 11 | public interface IMailProvider 12 | { 13 | IDbConnection OpenMonthDb(DateTime? createdDate=null); 14 | } 15 | 16 | public class MailProvider(IWebHostEnvironment env, IDbConnectionFactory dbFactory) 17 | : IMailProvider, IRequiresSchema 18 | { 19 | public string DbDir { get; set; } = "App_Data/mail"; 20 | public static string DbMonthFile(DateTime createdDate) => $"mail_{createdDate.Year}-{createdDate.Month:00}.db"; 21 | 22 | public IDbConnection ResolveMonthDb(DateTime createdDate) 23 | { 24 | var monthDb = DbMonthFile(createdDate); 25 | if (!OrmLiteConnectionFactory.NamedConnections.ContainsKey(monthDb)) 26 | { 27 | var absoluteDbDir = env.ContentRootPath.CombineWith(DbDir).AssertDir(); 28 | var dataSource = absoluteDbDir.CombineWith(monthDb); 29 | dbFactory.RegisterConnection(monthDb, $"DataSource={dataSource};Cache=Shared", SqliteDialect.Provider); 30 | var db = dbFactory.OpenDbConnection(monthDb); 31 | InitMonthDbSchema(db); 32 | return db; 33 | } 34 | return dbFactory.OpenDbConnection(monthDb); 35 | } 36 | 37 | public void InitMonthDbSchema(IDbConnection db) 38 | { 39 | db.CreateTableIfNotExists(); 40 | db.CreateTableIfNotExists(); 41 | db.CreateTableIfNotExists(); 42 | } 43 | 44 | public IDbConnection OpenMonthDb(DateTime? createdDate=null) => ResolveMonthDb(createdDate ?? DateTime.UtcNow); 45 | 46 | public void InitSchema() 47 | { 48 | using var db = OpenMonthDb(DateTime.UtcNow); 49 | InitMonthDbSchema(db); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/MarkdownScripts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using ServiceStack; 6 | using ServiceStack.Script; 7 | using ServiceStack.Text; 8 | using CreatorKit.ServiceInterface; 9 | using CreatorKit.ServiceModel; 10 | 11 | namespace CreatorKit; 12 | 13 | public class EmailMarkdownScriptMethods : ScriptMethods 14 | { 15 | public IRawString emailmarkdown(string? markdown) => markdown != null 16 | ? EmailMarkdownScriptBlock.Transform(markdown).ToRawString() 17 | : RawString.Empty; 18 | } 19 | 20 | 21 | /// 22 | /// Converts markdown contents to HTML using the configured MarkdownConfig.Transformer. 23 | /// If a variable name is specified the HTML output is captured and saved instead. 24 | /// 25 | /// Usages: {{#emailmarkdown}} ## The Heading {{/emailmarkdown}} 26 | /// {{#emailmarkdown content}} ## The Heading {{/emailmarkdown}} HTML: {{content}} 27 | /// 28 | public class EmailMarkdownScriptBlock : ScriptBlock 29 | { 30 | public override string Name => "emailmarkdown"; 31 | 32 | public static string Prefix = "
"; 33 | public static string Suffix = "
"; 34 | 35 | public static Dictionary ReplaceTokens { get; } = new() 36 | { 37 | ["

"] = "

", 45 | ["

"] = "
", 46 | ["
    "] = "
      ", 47 | ["
        "] = "
          ", 48 | ["
        • "] = "
        • ", 49 | ["
          "] = "
          ", 50 | }; 51 | 52 | public static string Transform(string markdown) 53 | { 54 | var html = MarkdownConfig.Transform(markdown); 55 | foreach (var entry in ReplaceTokens) 56 | { 57 | html = html.Replace(entry.Key, entry.Value); 58 | } 59 | return Prefix + html + Suffix; 60 | } 61 | 62 | public override async Task WriteAsync( 63 | ScriptScopeContext scope, PageBlockFragment block, CancellationToken token) 64 | { 65 | var strFragment = (PageStringFragment)block.Body[0]; 66 | 67 | if (!block.Argument.IsNullOrWhiteSpace()) 68 | { 69 | Capture(scope, block, strFragment); 70 | } 71 | else 72 | { 73 | await scope.OutputStream.WriteAsync(Transform(strFragment.ValueString), token); 74 | } 75 | } 76 | 77 | private static void Capture( 78 | ScriptScopeContext scope, PageBlockFragment block, PageStringFragment strFragment) 79 | { 80 | var literal = block.Argument.AdvancePastWhitespace(); 81 | 82 | literal = literal.ParseVarName(out var name); 83 | var nameString = name.ToString(); 84 | scope.PageResult.Args[nameString] = Transform(strFragment.ValueString).ToRawString(); 85 | } 86 | } -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/MyServices.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | using CreatorKit.ServiceModel; 3 | 4 | namespace CreatorKit.ServiceInterface; 5 | 6 | public class MyServices : Service 7 | { 8 | public object Any(Hello request) 9 | { 10 | return new HelloResponse { Result = $"Hello, {request.Name}!" }; 11 | } 12 | } -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/SendMessagesCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using CreatorKit.ServiceModel; 4 | using CreatorKit.ServiceModel.Types; 5 | using ServiceStack; 6 | using ServiceStack.Jobs; 7 | using ServiceStack.OrmLite; 8 | 9 | namespace CreatorKit.ServiceInterface; 10 | 11 | public class SendMessages : IGet, IReturn 12 | { 13 | public List? MailMessageIds { get; set; } 14 | public List? MailRunMessageIds { get; set; } 15 | } 16 | 17 | public class SendMessagesResponse 18 | { 19 | public List? MailMessageIds { get; set; } 20 | public List? MailRunMessageIds { get; set; } 21 | public List? Errors { get; set; } 22 | public ResponseStatus? ResponseStatus { get; set; } 23 | } 24 | 25 | public class SendMessagesCommand(IMailProvider mail, IBackgroundJobs jobs) 26 | : SyncCommandWithResult 27 | { 28 | protected override SendMessagesResponse Run(SendMessages request) 29 | { 30 | var ret = new SendMessagesResponse(); 31 | using var mailDb = mail.OpenMonthDb(); 32 | 33 | foreach (var id in request.MailMessageIds.Safe()) 34 | { 35 | try 36 | { 37 | var cmd = new SendMailMessageCommand(mail, jobs); 38 | cmd.ExecuteAsync(new SendMailMessage { Id = id }).Wait(); 39 | } 40 | catch (Exception e) 41 | { 42 | ret.Errors ??= []; 43 | ret.Errors.Add($"[Error {id}] {e.GetType().Name}: {e.Message}"); 44 | } 45 | } 46 | 47 | foreach (var id in request.MailRunMessageIds.Safe()) 48 | { 49 | try 50 | { 51 | var cmd = new SendMailMessageRunCommand(mail, jobs); 52 | cmd.ExecuteAsync(new SendMailMessageRun { Id = id }).Wait(); 53 | } 54 | catch (Exception e) 55 | { 56 | ret.Errors ??= []; 57 | ret.Errors.Add($"[Error {id}] {e.GetType().Name}: {e.Message}"); 58 | } 59 | } 60 | 61 | if (request.MailRunMessageIds?.Count > 0) 62 | { 63 | var mailRunId = mailDb.Scalar(mailDb.From() 64 | .Where(x => x.Id == request.MailRunMessageIds[0]) 65 | .Select(x => x.MailRunId)); 66 | mailDb.UpdateOnly(() => new MailRun { CompletedDate = DateTime.UtcNow }, 67 | where: x => x.Id == mailRunId); 68 | } 69 | 70 | return ret; 71 | } 72 | } 73 | 74 | public class SendMailMessageCommand(IMailProvider mail, IBackgroundJobs jobs) 75 | : SyncCommandWithResult 76 | { 77 | protected override MailMessage Run(SendMailMessage request) 78 | { 79 | using var mailDb = mail.OpenMonthDb(); 80 | var msg = mailDb.SingleById(request.Id); 81 | if (msg.CompletedDate != null && request.Force != true) 82 | throw new Exception($"Message {request.Id} has already been sent"); 83 | 84 | // ensure message is only sent once 85 | if (mailDb.UpdateOnly(() => new MailMessage { StartedDate = DateTime.UtcNow, Draft = false }, 86 | where: x => x.Id == request.Id && (x.StartedDate == null || request.Force == true)) == 1) 87 | { 88 | jobs.EnqueueCommand(msg.Message); 89 | 90 | mailDb.UpdateOnly(() => new MailMessage { CompletedDate = DateTime.UtcNow }, 91 | where: x => x.Id == request.Id); 92 | } 93 | 94 | return msg; 95 | } 96 | } 97 | 98 | public class SendMailMessageRunCommand(IMailProvider mail, IBackgroundJobs jobs) 99 | : SyncCommandWithResult 100 | { 101 | protected override MailMessageRun Run(SendMailMessageRun request) 102 | { 103 | using var mailDb = mail.OpenMonthDb(); 104 | var msg = mailDb.SingleById(request.Id); 105 | if (msg.CompletedDate != null && request.Force != true) 106 | throw new Exception($"Message {request.Id} has already been sent"); 107 | 108 | // ensure message is only sent once 109 | if (mailDb.UpdateOnly(() => new MailMessageRun { StartedDate = DateTime.UtcNow }, 110 | where: x => x.Id == request.Id && x.StartedDate == null) == 1) 111 | { 112 | jobs.EnqueueCommand(msg.Message); 113 | 114 | mailDb.UpdateOnly(() => new MailMessageRun { CompletedDate = DateTime.UtcNow }, 115 | where: x => x.Id == request.Id); 116 | } 117 | 118 | return msg; 119 | } 120 | } -------------------------------------------------------------------------------- /CreatorKit.ServiceInterface/ValidationScripts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using CreatorKit.Data; 6 | using CreatorKit.ServiceModel; 7 | using CreatorKit.ServiceModel.Types; 8 | using ServiceStack; 9 | using ServiceStack.OrmLite; 10 | using ServiceStack.Script; 11 | using ServiceStack.Text; 12 | using ServiceStack.Web; 13 | 14 | namespace CreatorKit.ServiceInterface; 15 | 16 | public class ValidationScripts : ScriptMethods 17 | { 18 | public ITypeValidator ActiveUser() => new ActiveUserValidator(); 19 | } 20 | 21 | public class ActiveUserValidator : TypeValidator, IAuthTypeValidator 22 | { 23 | public static string DefaultErrorMessage { get; set; } = "Your account is locked"; 24 | public ActiveUserValidator() 25 | : base(nameof(HttpStatusCode.Forbidden), DefaultErrorMessage, 403) 26 | { 27 | this.ContextArgs = new Dictionary(); 28 | } 29 | 30 | public override async Task ThrowIfNotValidAsync(object dto, IRequest request) 31 | { 32 | await IsAuthenticatedValidator.Instance.ThrowIfNotValidAsync(dto, request).ConfigAwait(); 33 | 34 | var userId = request.GetRequiredUserId(); 35 | var hasBannedDate = request.GetClaimsPrincipal()?.HasClaim(x => x.Type == nameof(AppUser.BanUntilDate)) == true; 36 | var appData = request.TryResolve(); 37 | var checkDb = appData.BannedUsersMap.ContainsKey(userId) || hasBannedDate; 38 | if (checkDb) 39 | { 40 | using var db = HostContext.AppHost.GetDbConnection(request); 41 | var user = db.SingleById(userId); 42 | if (user == null) 43 | throw new HttpError(ResolveStatusCode(), ResolveErrorCode(), "Your account no longer exists"); 44 | 45 | if (user.BanUntilDate != null && user.BanUntilDate > DateTime.UtcNow) 46 | throw new HttpError(ResolveStatusCode(), ResolveErrorCode(), 47 | $"Your account will be unbanned in {(user.BanUntilDate.Value - DateTime.UtcNow).Humanize()}"); 48 | 49 | if (user.LockedDate != null) 50 | throw new HttpError(ResolveStatusCode(), ResolveErrorCode(), ResolveErrorMessage(request, dto)); 51 | 52 | appData.BannedUsersMap.TryRemove(userId, out _); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/Admin/Posts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CreatorKit.ServiceModel.Types; 3 | using ServiceStack; 4 | using ServiceStack.DataAnnotations; 5 | 6 | namespace CreatorKit.ServiceModel.Admin; 7 | 8 | [Tag(TagNames.Auth)] 9 | [ValidateIsAdmin] 10 | public class AdminQueryAppUsers : QueryDb {} 11 | 12 | 13 | [Tag(Tag.Admin)] 14 | [ValidateIsAdmin] 15 | [AutoApply(Behavior.AuditQuery)] 16 | public class AdminQueryThreads : QueryDb 17 | { 18 | public int? Id { get; set; } 19 | } 20 | [Tag(Tag.Admin)] 21 | [ValidateIsAdmin] 22 | public class AdminUpdateThread : IPatchDb, IReturn 23 | { 24 | public int Id { get; set; } 25 | public string Url { get; set; } 26 | public string Description { get; set; } 27 | public string ExternalRef { get; set; } 28 | public long? RefId { get; set; } 29 | public string RefIdStr { get; set; } 30 | public DateTime CreatedDate { get; set; } 31 | public DateTime? ClosedDate { get; set; } 32 | public DateTime? DeletedDate { get; set; } 33 | } 34 | [Tag(Tag.Admin)] 35 | [ValidateIsAdmin] 36 | [AutoApply(Behavior.AuditDelete)] 37 | public class AdminDeleteThread : IDeleteDb, IReturnVoid 38 | { 39 | public int Id { get; set; } 40 | } 41 | 42 | 43 | [Tag(Tag.Admin)] 44 | [ValidateIsAdmin] 45 | public class AdminQueryComments : QueryDb {} 46 | [Tag(Tag.Admin)] 47 | [ValidateIsAdmin] 48 | [AutoApply(Behavior.AuditModify)] 49 | public class AdminUpdateComment : IPatchDb, IReturn 50 | { 51 | public int Id { get; set; } 52 | public int? ThreadId { get; set; } 53 | public int? ReplyId { get; set; } 54 | public int? UpVotes { get; set; } 55 | public int? DownVotes { get; set; } 56 | public int? Votes { get; set; } 57 | public string? FlagReason { get; set; } 58 | public string? Notes { get; set; } 59 | public int? AppUserId { get; set; } 60 | public DateTime? DeletedDate { get; set; } 61 | [Input(Type = "textarea"), FieldCss(Field = "col-span-12", Input = "h-36")] 62 | public string Content { get; set; } 63 | } 64 | [Tag(Tag.Admin)] 65 | [ValidateIsAdmin] 66 | [AutoApply(Behavior.AuditSoftDelete)] 67 | public class AdminDeleteComment : IDeleteDb, IReturnVoid 68 | { 69 | public int Id { get; set; } 70 | } 71 | 72 | 73 | [Tag(Tag.Admin)] 74 | [ValidateIsAdmin] 75 | public class AdminQueryCommentVotes : QueryDb {} 76 | 77 | [Tag(Tag.Admin)] 78 | [ValidateIsAdmin] 79 | public class AdminUpdateCommentVote : IPatchDb, IReturn 80 | { 81 | public long Id { get; set; } 82 | public int CommentId { get; set; } 83 | public int AppUserId { get; set; } 84 | public int Vote { get; set; } // -1 / 1 85 | } 86 | [Tag(Tag.Admin)] 87 | [ValidateIsAdmin] 88 | public class AdminDeleteCommentVote : IDeleteDb, IReturnVoid 89 | { 90 | public int Id { get; set; } 91 | } 92 | 93 | 94 | [Tag(Tag.Admin)] 95 | [ValidateIsAdmin] 96 | public class AdminQueryCommentReports : QueryDb {} 97 | 98 | [Tag(Tag.Admin)] 99 | [ValidateIsAdmin] 100 | public class AdminUpdateCommentReport : IPatchDb, IReturn 101 | { 102 | [Input(Type = "hidden")] 103 | public long Id { get; set; } 104 | 105 | [References(typeof(AppUser))] 106 | public int? AppUserId { get; set; } 107 | 108 | public PostReport? PostReport { get; set; } 109 | 110 | [Input(Type = "textarea"), FieldCss(Field = "col-span-12", Input = "h-20")] 111 | public string? Description { get; set; } 112 | 113 | public ModerationDecision? Moderation { get; set; } 114 | 115 | public string? Notes { get; set; } 116 | } 117 | 118 | [Tag(Tag.Admin)] 119 | [ValidateIsAdmin] 120 | public class AdminDeleteCommentReport : IDeleteDb, IReturnVoid 121 | { 122 | public int Id { get; set; } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/AppUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using ServiceStack; 3 | using ServiceStack.DataAnnotations; 4 | using System; 5 | 6 | namespace CreatorKit.ServiceModel; 7 | 8 | // Add profile data for application users by adding properties to the AppUser class 9 | [Alias("AspNetUsers")] 10 | public class AppUser : IdentityUser 11 | { 12 | public string? FirstName { get; set; } 13 | public string? LastName { get; set; } 14 | public string? DisplayName { get; set; } 15 | public string? ProfileUrl { get; set; } 16 | [Input(Type = "file"), UploadTo("avatars")] 17 | public string? Avatar { get; set; } //overrides ProfileUrl 18 | public string? Handle { get; set; } 19 | public int? RefId { get; set; } 20 | public string RefIdStr { get; set; } = Guid.NewGuid().ToString(); 21 | public bool IsArchived { get; set; } 22 | public DateTime? ArchivedDate { get; set; } 23 | public string? LastLoginIp { get; set; } 24 | public DateTime? LastLoginDate { get; set; } 25 | public DateTime CreatedDate { get; set; } = DateTime.UtcNow; 26 | public DateTime ModifiedDate { get; set; } = DateTime.UtcNow; 27 | public DateTime? LockedDate { get; set; } 28 | public DateTime? BanUntilDate { get; set; } 29 | public string? FacebookUserId { get; set; } 30 | public string? GoogleUserId { get; set; } 31 | public string? GoogleProfilePageUrl { get; set; } 32 | public string? MicrosoftUserId { get; set; } 33 | 34 | } 35 | 36 | [Alias("AspNetRoles")] 37 | public class AppRole : IdentityRole 38 | { 39 | public AppRole() {} 40 | public AppRole(string roleName) : base(roleName) {} 41 | } 42 | -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/Contacts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ServiceStack; 4 | using CreatorKit.ServiceModel.Types; 5 | 6 | namespace CreatorKit.ServiceModel; 7 | 8 | [Tag(Tag.Mail)] 9 | public class CreateContact : ICreateDb, IReturn 10 | { 11 | [ValidateNotEmpty] 12 | public string Email { get; set; } 13 | [ValidateNotEmpty] 14 | public string FirstName { get; set; } 15 | [ValidateNotEmpty] 16 | public string LastName { get; set; } 17 | public Source Source { get; set; } 18 | 19 | [Input(Type = "tag", EvalAllowableValues = "AppData.MailingListValues"), FieldCss(Field = "col-span-12")] 20 | public List? MailingLists { get; set; } 21 | } 22 | 23 | [Tag(Tag.Mail)] 24 | public class SubscribeToMailingList : IPost, IReturnVoid 25 | { 26 | [ValidateNotEmpty] 27 | public string Email { get; set; } 28 | [ValidateNotEmpty] 29 | public string FirstName { get; set; } 30 | [ValidateNotEmpty] 31 | public string LastName { get; set; } 32 | public Source Source { get; set; } 33 | [Input(Type = "tag", EvalAllowableValues = "AppData.MailingListValues"), FieldCss(Field = "col-span-12")] 34 | public List? MailingLists { get; set; } 35 | } 36 | 37 | [Tag(Tag.Mail)] 38 | public class UpdateContactMailingLists : IPost, IReturnVoid 39 | { 40 | [ValidateNotEmpty] 41 | public string? Ref { get; set; } 42 | [Input(Type = "tag", EvalAllowableValues = "AppData.MailingListValues"), FieldCss(Field = "col-span-12")] 43 | public List MailingLists { get; set; } 44 | public bool? UnsubscribeAll { get; set; } 45 | } 46 | 47 | [Tag(Tag.Mail)] 48 | public class FindContact : IGet, IReturn 49 | { 50 | public string? Email { get; set; } 51 | public string? Ref { get; set; } 52 | } 53 | public class FindContactResponse 54 | { 55 | public Contact Result { get; set; } 56 | public ResponseStatus ResponseStatus { get; set; } 57 | } 58 | 59 | [Tag(Tag.Mail)] 60 | [ValidateIsAdmin] 61 | public class QueryContacts : QueryDb 62 | { 63 | [QueryDbField(Template = "EmailLower LIKE {Value} OR NameLower LIKE {Value}", ValueFormat = "%{0}%", Field = "EmailLower")] 64 | public string? Search { get; set; } 65 | } 66 | 67 | [Tag(Tag.Mail)] 68 | [ValidateIsAdmin] 69 | [AutoPopulate(nameof(Contact.MailingLists), Eval = "dto.MailingLists.fromEnumFlagsList(typeof('MailingList'))")] 70 | public class AdminCreateContact : IReturn 71 | { 72 | [ValidateNotEmpty] 73 | public string Email { get; set; } 74 | [ValidateNotEmpty] 75 | public string FirstName { get; set; } 76 | [ValidateNotEmpty] 77 | public string LastName { get; set; } 78 | public Source Source { get; set; } 79 | [ValidateNotEmpty] 80 | [Input(Type = "tag", EvalAllowableValues = "AppData.MailingListValues"), FieldCss(Field = "col-span-12")] 81 | public List MailingLists { get; set; } 82 | public DateTime? VerifiedDate { get; set; } 83 | public int? AppUserId { get; set; } 84 | public DateTime? CreatedDate { get; set; } 85 | } 86 | 87 | [Tag(Tag.Mail)] 88 | [ValidateIsAdmin] 89 | [AutoPopulate(nameof(Contact.MailingLists), Eval = "dto.MailingLists.fromEnumFlagsList(typeof('MailingList'))")] 90 | public class UpdateContact : IPatchDb, IReturn 91 | { 92 | public int Id { get; set; } 93 | public string? Email { get; set; } 94 | public string? FirstName { get; set; } 95 | public string? LastName { get; set; } 96 | public Source? Source { get; set; } 97 | [Input(Type = "tag", EvalAllowableValues = "AppData.MailingListValues", Options = "{ converter:enumFlagsConverter('MailingList') }"), FieldCss(Field = "col-span-12")] 98 | public List? MailingLists { get; set; } 99 | 100 | public string? ExternalRef { get; set; } 101 | public int? AppUserId { get; set; } 102 | public DateTime? CreatedDate { get; set; } 103 | public DateTime? VerifiedDate { get; set; } 104 | public DateTime? DeletedDate { get; set; } 105 | public DateTime? UnsubscribedDate { get; set; } 106 | } 107 | 108 | [Tag(Tag.Mail)] 109 | [ValidateIsAdmin] 110 | public class DeleteContact : IDeleteDb, IReturnVoid 111 | { 112 | public int Id { get; set; } 113 | } 114 | 115 | /// 116 | /// Soft Delete Contact so invalid Contact Emails are prevented from being re-added 117 | /// 118 | [Tag(Tag.Mail)] 119 | [ValidateIsAdmin] 120 | public class InvalidateEmails : IReturn 121 | { 122 | public InvalidEmailStatus Status { get; set; } 123 | public List Emails { get; set; } = new(); 124 | } 125 | 126 | [Tag(Tag.Mail)] 127 | [ValidateIsAdmin] 128 | public class QueryInvalidEmails : QueryDb {} 129 | 130 | [Tag(Tag.Mail)] 131 | [ValidateIsAdmin] 132 | [AutoPopulate(nameof(InvalidEmail.EmailLower), Eval = "lower(Request.Dto.Email)")] 133 | public class CreateInvalidEmail : ICreateDb, IReturnVoid 134 | { 135 | [ValidateNotEmpty] 136 | public string Email { get; set; } 137 | public InvalidEmailStatus Status { get; set; } 138 | } 139 | 140 | [Tag(Tag.Mail)] 141 | [ValidateIsAdmin] 142 | [AutoPopulate(nameof(InvalidEmail.EmailLower), Eval = "lower(Request.Dto.Email)")] 143 | public class UpdateInvalidEmail : IPatchDb, IReturnVoid 144 | { 145 | public string? Email { get; set; } 146 | public string? EmailLower { get; set; } 147 | public InvalidEmailStatus? Status { get; set; } 148 | } 149 | 150 | [Tag(Tag.Mail)] 151 | [ValidateIsAdmin] 152 | public class DeleteInvalidEmail : IDeleteDb, IReturnVoid 153 | { 154 | public int? Id { get; set; } 155 | } 156 | -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/CreatorKit.ServiceModel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/EmailRenderers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceStack; 3 | using ServiceStack.DataAnnotations; 4 | 5 | namespace CreatorKit.ServiceModel; 6 | 7 | [Tag(Tag.Mail), ValidateIsAdmin] 8 | public class PreviewEmail : IPost, IReturn 9 | { 10 | public string? Request { get; set; } 11 | public string? Renderer { get; set; } 12 | [ValidateNotNull] 13 | public Dictionary RequestArgs { get; set; } 14 | } 15 | 16 | [Tag(Tag.Mail), ValidateIsAdmin, ExcludeMetadata] 17 | public class RenderSimpleText : RenderEmailBase, IGet, IReturn 18 | { 19 | public string Body { get; set; } 20 | } 21 | 22 | [Tag(Tag.Mail), ValidateIsAdmin, ExcludeMetadata] 23 | public class RenderCustomHtml : RenderEmailBase, IGet, IReturn 24 | { 25 | [ValidateNotEmpty] 26 | [Input(Type = "combobox", EvalAllowableValues = "AppData.EmailLayoutOptions")] 27 | public string Layout { get; set; } 28 | 29 | [ValidateNotEmpty] 30 | [Input(Type = "combobox", EvalAllowableValues = "AppData.EmailTemplateOptions")] 31 | public string Template { get; set; } 32 | 33 | public string Body { get; set; } 34 | } 35 | 36 | [Route("/docs/{Page}")] 37 | public class RenderDoc : IGet, IReturn 38 | { 39 | [ValidateNotEmpty] 40 | public string Page { get; set; } 41 | } -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/EmailRuns.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | using ServiceStack.DataAnnotations; 3 | 4 | namespace CreatorKit.ServiceModel; 5 | 6 | [Renderer(typeof(RenderSimpleText))] 7 | [Tag(Tag.Mail), ValidateIsAdmin] 8 | [Description("Simple Text Email")] 9 | public class SimpleTextMailRun : MailRunBase, IPost, IReturn 10 | { 11 | [ValidateNotEmpty] 12 | [FieldCss(Field = "col-span-12")] 13 | public string Subject { get; set; } 14 | [ValidateNotEmpty] 15 | [Input(Type = "textarea"), FieldCss(Field = "col-span-12", Input = "h-36")] 16 | public string Body { get; set; } 17 | } 18 | 19 | [Renderer(typeof(RenderCustomHtml), Layout = "basic", Template="empty")] 20 | [Tag(Tag.Mail), ValidateIsAdmin] 21 | [Icon(Svg = Icons.TextMarkup)] 22 | [Description("Markdown Email")] 23 | public class MarkdownMailRun : MailRunBase, IPost, IReturn 24 | { 25 | [ValidateNotEmpty] 26 | [FieldCss(Field = "col-span-12")] 27 | public string Subject { get; set; } 28 | 29 | [ValidateNotEmpty] 30 | [Input(Type = "MarkdownEmailInput", Label = ""), FieldCss(Field = "col-span-12", Input = "h-56")] 31 | public string? Body { get; set; } 32 | } 33 | 34 | [Renderer(typeof(RenderCustomHtml))] 35 | [Tag(Tag.Mail), ValidateIsAdmin] 36 | [Icon(Svg = Icons.RichHtml)] 37 | [Description("Custom HTML Email")] 38 | public class CustomHtmlMailRun : MailRunBase, IPost, IReturn 39 | { 40 | [ValidateNotEmpty] 41 | [Input(Type = "combobox", EvalAllowableValues = "AppData.EmailLayoutOptions")] 42 | public string Layout { get; set; } 43 | [ValidateNotEmpty] 44 | [Input(Type = "combobox", EvalAllowableValues = "AppData.EmailTemplateOptions")] 45 | public string Template { get; set; } 46 | [ValidateNotEmpty] 47 | public string Subject { get; set; } 48 | [ValidateNotEmpty] 49 | [Input(Type = "MarkdownEmailInput", Label = ""), FieldCss(Field = "col-span-12", Input = "h-56")] 50 | public string? Body { get; set; } 51 | } -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/Emails.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | using ServiceStack.DataAnnotations; 3 | using CreatorKit.ServiceModel.Types; 4 | 5 | namespace CreatorKit.ServiceModel; 6 | 7 | [Renderer(typeof(RenderSimpleText))] 8 | [Tag(Tag.Mail), ValidateIsAdmin] 9 | [Description("Simple Text Email")] 10 | public class SimpleTextEmail : CreateEmailBase, IPost, IReturn 11 | { 12 | [ValidateNotEmpty] 13 | [FieldCss(Field = "col-span-12")] 14 | public string Subject { get; set; } 15 | 16 | [ValidateNotEmpty] 17 | [Input(Type = "textarea"), FieldCss(Field = "col-span-12", Input = "h-36")] 18 | public string Body { get; set; } 19 | public bool? Draft { get; set; } 20 | } 21 | 22 | [Renderer(typeof(RenderCustomHtml))] 23 | [Tag(Tag.Mail), ValidateIsAdmin] 24 | [Icon(Svg = Icons.RichHtml)] 25 | [Description("Custom HTML Email")] 26 | public class CustomHtmlEmail : CreateEmailBase, IPost, IReturn 27 | { 28 | [ValidateNotEmpty] 29 | [Input(Type = "combobox", EvalAllowableValues = "AppData.EmailLayoutOptions")] 30 | public string Layout { get; set; } 31 | 32 | [ValidateNotEmpty] 33 | [Input(Type = "combobox", EvalAllowableValues = "AppData.EmailTemplateOptions")] 34 | public string Template { get; set; } 35 | 36 | [ValidateNotEmpty] 37 | [FieldCss(Field = "col-span-12")] 38 | public string Subject { get; set; } 39 | 40 | [Input(Type = "MarkdownEmailInput", Label = ""), FieldCss(Field = "col-span-12", Input = "h-56")] 41 | public string? Body { get; set; } 42 | public bool? Draft { get; set; } 43 | } 44 | -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/Hello.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | 3 | namespace CreatorKit.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 | -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/Icons.cs: -------------------------------------------------------------------------------- 1 | namespace CreatorKit.ServiceModel; 2 | 3 | public class Icons 4 | { 5 | public const string Contact = ""; 6 | public const string Mail = ""; 7 | public const string MailRun = ""; 8 | public const string MailSubscription = ""; 9 | public const string Thread = ""; 10 | public const string Comment = ""; 11 | public const string Vote = ""; 12 | public const string Report = ""; 13 | public const string AppUser = ""; 14 | public const string Newsletter = ""; 15 | public const string PlainText = ""; 16 | public const string TextMarkup = ""; 17 | public const string RichHtml = ""; 18 | } -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/Mq.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceStack; 3 | using ServiceStack.DataAnnotations; 4 | using ServiceStack.Messaging; 5 | 6 | namespace CreatorKit.ServiceModel; 7 | -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/Posts.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | using System; 3 | using System.Collections.Generic; 4 | using CreatorKit.ServiceModel.Types; 5 | 6 | namespace CreatorKit.ServiceModel; 7 | 8 | [Tag(Tag.Posts)] 9 | [ValidateIsAuthenticated] 10 | [AutoApply(Behavior.AuditQuery)] 11 | public class GetThreadUserData : IGet, IReturn 12 | { 13 | public int ThreadId { get; set; } 14 | } 15 | public class GetThreadUserDataResponse 16 | { 17 | public int ThreadId { get; set; } 18 | public bool Liked { get; set; } 19 | public List UpVoted { get; set; } 20 | public List DownVoted { get; set; } 21 | public ResponseStatus ResponseStatus { get; set; } 22 | } 23 | 24 | [Tag(Tag.Posts)] 25 | [AutoApply(Behavior.AuditQuery)] 26 | public class QueryComments : QueryDb, 27 | IJoin 28 | { 29 | public int? ThreadId { get; set; } 30 | } 31 | 32 | [Tag(Tag.Posts)] 33 | public class GetThread : IGet, IReturn 34 | { 35 | public int? Id { get; set; } 36 | public string? Url { get; set; } 37 | } 38 | public class GetThreadResponse 39 | { 40 | public Thread Result { get; set; } 41 | public ResponseStatus ResponseStatus { get; set; } 42 | } 43 | 44 | [Tag(Tag.Posts)] 45 | [ValidateIsAuthenticated, ValidateActiveUser] 46 | [AutoApply(Behavior.AuditCreate)] 47 | [AutoPopulate(nameof(Comment.AppUserId), Eval = "userAuthIntId()")] 48 | [AutoPopulate(nameof(Comment.Votes), Value = 1)] 49 | public class CreateComment : ICreateDb, IReturn 50 | { 51 | public int ThreadId { get; set; } 52 | public int? ReplyId { get; set; } 53 | [ValidateLength(1,280)] 54 | public string Content { get; set; } 55 | } 56 | 57 | [Tag(Tag.Posts)] 58 | [ValidateIsAuthenticated, ValidateActiveUser] 59 | [AutoApply(Behavior.AuditModify)] 60 | [AutoFilter(QueryTerm.Ensure, nameof(Comment.AppUserId), Eval = "userAuthIntId()")] 61 | public class UpdateComment : IPatchDb, IReturn 62 | { 63 | public int Id { get; set; } 64 | public string? Content { get; set; } 65 | } 66 | 67 | [Tag(Tag.Posts)] 68 | [ValidateIsAuthenticated, ValidateActiveUser] 69 | [AutoApply(Behavior.AuditSoftDelete)] 70 | [AutoFilter(QueryTerm.Ensure, nameof(Comment.AppUserId), Eval = "userAuthIntId()")] 71 | public class DeleteComment : IDeleteDb, IReturnVoid 72 | { 73 | public int Id { get; set; } 74 | } 75 | 76 | [Tag(Tag.Posts)] 77 | [ValidateIsAuthenticated] 78 | [AutoApply(Behavior.AuditCreate)] 79 | [AutoPopulate(nameof(Comment.AppUserId), Eval = "userAuthIntId()")] 80 | public class CreateThreadLike : ICreateDb, IReturnVoid 81 | { 82 | public int ThreadId { get; set; } 83 | [ValidateInclusiveBetween(-1, 1)] 84 | public int Vote { get; set; } 85 | } 86 | 87 | [Tag(Tag.Posts)] 88 | [ValidateIsAuthenticated] 89 | [AutoFilter(QueryTerm.Ensure, nameof(Comment.AppUserId), Eval = "userAuthIntId()")] 90 | public class DeleteThreadLike : IDeleteDb, IReturn 91 | { 92 | public int ThreadId { get; set; } 93 | } 94 | 95 | [Tag(Tag.Posts)] 96 | [ValidateIsAuthenticated] 97 | [AutoFilter(QueryTerm.Ensure, nameof(Comment.AppUserId), Eval = "userAuthIntId()")] 98 | public class QueryCommentVotes : QueryDb 99 | { 100 | public int ThreadId { get; set; } 101 | } 102 | 103 | [Tag(Tag.Posts)] 104 | [ValidateIsAuthenticated] 105 | [AutoApply(Behavior.AuditCreate)] 106 | [AutoPopulate(nameof(Comment.AppUserId), Eval = "userAuthIntId()")] 107 | public class CreateCommentVote : ICreateDb, IReturnVoid 108 | { 109 | public int CommentId { get; set; } 110 | [ValidateInclusiveBetween(-1, 1)] 111 | public int Vote { get; set; } 112 | } 113 | 114 | [Tag(Tag.Posts)] 115 | [ValidateIsAuthenticated] 116 | [AutoFilter(QueryTerm.Ensure, nameof(Comment.AppUserId), Eval = "userAuthIntId()")] 117 | public class DeleteCommentVote : IDeleteDb, IReturnVoid 118 | { 119 | public int CommentId { get; set; } 120 | } 121 | 122 | [Tag(Tag.Posts)] 123 | [ValidateIsAuthenticated, ValidateActiveUser] 124 | [AutoApply(Behavior.AuditCreate)] 125 | [AutoPopulate(nameof(CommentReport.AppUserId), Eval = "userAuthIntId()")] 126 | [AutoPopulate(nameof(CommentReport.Moderation), Value = ModerationDecision.None)] 127 | public class CreateCommentReport : ICreateDb, IReturnVoid 128 | { 129 | public int CommentId { get; set; } 130 | public PostReport PostReport { get; set; } 131 | public string? Description { get; set; } 132 | } 133 | 134 | 135 | public class ValidateActiveUserAttribute : ValidateRequestAttribute 136 | { 137 | public ValidateActiveUserAttribute() : base("ActiveUser()") { } 138 | } 139 | -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/RendererAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack; 3 | 4 | namespace CreatorKit.ServiceModel; 5 | 6 | [AttributeUsage(AttributeTargets.Class)] 7 | public class RendererAttribute(string template) : RendererAttribute(typeof(T)) {} 8 | 9 | /// 10 | /// Specify which renderer should be used to render emails 11 | /// 12 | [AttributeUsage(AttributeTargets.Class)] 13 | public class RendererAttribute(Type type) : AttributeBase 14 | { 15 | public Type Type { get; set; } = type; 16 | 17 | public string Layout { get; set; } = "basic"; 18 | public string? Template { get; set; } 19 | } -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/Tag.cs: -------------------------------------------------------------------------------- 1 | namespace CreatorKit.ServiceModel; 2 | 3 | public class Tag 4 | { 5 | public const string Admin = nameof(Admin); 6 | public const string Posts = nameof(Posts); 7 | public const string Emails = nameof(Emails); 8 | public const string Mail = nameof(Mail); 9 | public const string Archive = nameof(Archive); 10 | public const string Mq = nameof(Mq); 11 | } -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/Types/Mail.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ServiceStack; 4 | using ServiceStack.DataAnnotations; 5 | 6 | namespace CreatorKit.ServiceModel.Types; 7 | 8 | [Icon(Svg = Icons.Contact)] 9 | public class Contact 10 | { 11 | [AutoIncrement] 12 | public int Id { get; set; } 13 | public string Email { get; set; } 14 | public string FirstName { get; set; } 15 | public string LastName { get; set; } 16 | public Source Source { get; set; } 17 | [FormatEnumFlags(nameof(MailingList))] 18 | public MailingList MailingLists { get; set; } 19 | public string Token { get; set; } 20 | [Index(Unique = true)] 21 | public string EmailLower { get; set; } 22 | [Index] 23 | public string NameLower { get; set; } 24 | [Index(Unique = true)] 25 | public string ExternalRef { get; set; } 26 | public int? AppUserId { get; set; } 27 | public DateTime CreatedDate { get; set; } 28 | public DateTime? VerifiedDate { get; set; } 29 | public DateTime? DeletedDate { get; set; } 30 | public DateTime? UnsubscribedDate { get; set; } 31 | } 32 | 33 | public enum InvalidEmailStatus 34 | { 35 | Invalid, 36 | /// 37 | /// Email servers for these domains do not provide a definitive verification response 38 | /// 39 | AcceptAll, 40 | Unknown, 41 | Disposable, 42 | } 43 | 44 | public class InvalidEmail 45 | { 46 | [AutoIncrement] 47 | public int Id { get; set; } 48 | public string Email { get; set; } 49 | public string EmailLower { get; set; } 50 | public InvalidEmailStatus Status { get; set; } 51 | } 52 | 53 | [Icon(Svg = Icons.Mail)] 54 | public class MailMessage 55 | { 56 | [AutoIncrement] 57 | public int Id { get; set; } 58 | public string Email { get; set; } 59 | public string? Layout { get; set; } 60 | public string? Template { get; set; } 61 | public string Renderer { get; set; } 62 | public Dictionary RendererArgs { get; set; } 63 | public EmailMessage Message { get; set; } 64 | public bool Draft { get; set; } 65 | public string ExternalRef { get; set; } 66 | public DateTime CreatedDate { get; set; } 67 | public DateTime? StartedDate { get; set; } 68 | public DateTime? CompletedDate { get; set; } 69 | public ResponseStatus? Error { get; set; } 70 | } 71 | 72 | [Icon(Svg = Icons.MailRun)] 73 | public class MailRun 74 | { 75 | [AutoIncrement] 76 | public int Id { get; set; } 77 | [FormatEnumFlags(nameof(MailingList))] 78 | public MailingList MailingList { get; set; } 79 | public string Generator { get; set; } 80 | public Dictionary GeneratorArgs { get; set; } 81 | public string Layout { get; set; } 82 | public string Template { get; set; } 83 | public string ExternalRef { get; set; } 84 | public DateTime CreatedDate { get; set; } 85 | public DateTime? GeneratedDate { get; set; } 86 | public DateTime? SentDate { get; set; } 87 | public DateTime? CompletedDate { get; set; } 88 | public int EmailsCount { get; set; } 89 | } 90 | 91 | [Icon(Svg = Icons.Mail)] 92 | [UniqueConstraint(nameof(MailRunId), nameof(ContactId))] 93 | public class MailMessageRun 94 | { 95 | [AutoIncrement] 96 | public int Id { get; set; } 97 | [ForeignKey(typeof(MailRun), OnDelete = "CASCADE")] 98 | public int MailRunId { get; set; } 99 | [Ref(Model = nameof(Contact), RefId = "Id", RefLabel = "Email")] 100 | public int ContactId { get; set; } 101 | // [Reference] 102 | // [Format(FormatMethods.Hidden)] 103 | // public Contact Contact { get; set; } 104 | public string Renderer { get; set; } 105 | public Dictionary RendererArgs { get; set; } 106 | public string ExternalRef { get; set; } 107 | public EmailMessage Message { get; set; } 108 | public DateTime CreatedDate { get; set; } 109 | public DateTime? StartedDate { get; set; } 110 | public DateTime? CompletedDate { get; set; } 111 | public ResponseStatus? Error { get; set; } 112 | } 113 | 114 | public enum Source 115 | { 116 | Unknown, 117 | UI, 118 | Website, 119 | System, 120 | Migration, 121 | } 122 | 123 | public class MailTo 124 | { 125 | public string Email { get; set; } 126 | public string Name { get; set; } 127 | } 128 | public class EmailMessage 129 | { 130 | public List To { get; set; } 131 | public List Cc { get; set; } 132 | public List Bcc { get; set; } 133 | public MailTo? From { get; set; } 134 | public string Subject { get; set; } 135 | public string? Body { get; set; } 136 | public string? BodyHtml { get; set; } 137 | public string? BodyText { get; set; } 138 | } 139 | -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/Types/MailingList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack.DataAnnotations; 3 | 4 | namespace CreatorKit.ServiceModel.Types; 5 | 6 | [Flags] 7 | public enum MailingList 8 | { 9 | [Description("None")] 10 | None = 0, //0 11 | [Description("Test Group")] 12 | TestGroup = 1 << 0, //1 13 | [Description("Monthly Newsletter")] 14 | MonthlyNewsletter = 1 << 1, //2 15 | [Description("New Blog Posts")] 16 | BlogPostReleases = 1 << 2, //4 17 | [Description("New Videos")] 18 | VideoReleases = 1 << 3, //8 19 | [Description("New Product Releases")] 20 | ProductReleases = 1 << 4, //16 21 | [Description("Yearly Updates")] 22 | YearlyUpdates = 1 << 5, //32 23 | } 24 | -------------------------------------------------------------------------------- /CreatorKit.ServiceModel/Types/Post.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack; 3 | using ServiceStack.DataAnnotations; 4 | 5 | namespace CreatorKit.ServiceModel.Types; 6 | 7 | [Icon(Svg = Icons.Thread)] 8 | [AutoPopulate(nameof(ExternalRef), Eval = "nguid")] 9 | public class Thread 10 | { 11 | [AutoIncrement] 12 | public int Id { get; set; } 13 | [Index(Unique = true)] 14 | public string Url { get; set; } 15 | public string Description { get; set; } 16 | public string ExternalRef { get; set; } 17 | public int ViewCount { get; set; } 18 | [Default(1)] 19 | public long LikesCount { get; set; } 20 | public long CommentsCount { get; set; } 21 | public long? RefId { get; set; } 22 | public string RefIdStr { get; set; } 23 | public DateTime CreatedDate { get; set; } 24 | public DateTime? ClosedDate { get; set; } 25 | public DateTime? DeletedDate { get; set; } 26 | } 27 | 28 | [Icon(Svg = Icons.Comment)] 29 | public class Comment : AuditBase 30 | { 31 | [AutoIncrement] 32 | public int Id { get; set; } 33 | public int ThreadId { get; set; } 34 | public int? ReplyId { get; set; } 35 | public string Content { get; set; } 36 | [Default(0)] 37 | public int UpVotes { get; set; } 38 | [Default(0)] 39 | public int DownVotes { get; set; } 40 | public int Votes { get; set; } 41 | public string? FlagReason { get; set; } 42 | public string? Notes { get; set; } 43 | public int AppUserId { get; set; } 44 | } 45 | 46 | [UniqueConstraint(nameof(ThreadId), nameof(AppUserId))] 47 | public class ThreadLike 48 | { 49 | [AutoIncrement] 50 | public long Id { get; set; } 51 | 52 | [References(typeof(Thread))] 53 | public int ThreadId { get; set; } 54 | [References(typeof(AppUser))] 55 | public int AppUserId { get; set; } 56 | public DateTime CreatedDate { get; set; } 57 | } 58 | 59 | [Icon(Svg = Icons.Vote)] 60 | [UniqueConstraint(nameof(CommentId), nameof(AppUserId))] 61 | public class CommentVote 62 | { 63 | [AutoIncrement] 64 | public long Id { get; set; } 65 | 66 | [Ref(None = true)] 67 | [References(typeof(Comment))] 68 | public int CommentId { get; set; } 69 | [References(typeof(AppUser))] 70 | public int AppUserId { get; set; } 71 | public int Vote { get; set; } // -1 / 1 72 | public DateTime CreatedDate { get; set; } 73 | } 74 | 75 | [Icon(Svg = Icons.Report)] 76 | public class CommentReport 77 | { 78 | [AutoIncrement] 79 | public long Id { get; set; } 80 | 81 | [References(typeof(Comment))] 82 | public int CommentId { get; set; } 83 | 84 | [Reference] 85 | public Comment Comment { get; set; } 86 | 87 | [References(typeof(AppUser))] 88 | public int AppUserId { get; set; } 89 | 90 | public PostReport PostReport { get; set; } 91 | public string Description { get; set; } 92 | 93 | public DateTime CreatedDate { get; set; } 94 | public ModerationDecision Moderation { get; set; } 95 | public string? Notes { get; set; } 96 | } 97 | 98 | public enum PostReport 99 | { 100 | Offensive, 101 | Spam, 102 | Nudity, 103 | Illegal, 104 | Other, 105 | } 106 | 107 | public enum ModerationDecision 108 | { 109 | None, 110 | [Description("Allow Comment")] 111 | Allow, 112 | [Description("Flag Comment")] 113 | Flag, 114 | [Description("Delete Comment")] 115 | Delete, 116 | [Description("Ban User for a day")] 117 | Ban1Day, 118 | [Description("Ban User for a week")] 119 | Ban1Week, 120 | [Description("Ban User for a month")] 121 | Ban1Month, 122 | [Description("Permanently Ban User")] 123 | PermanentBan, 124 | } 125 | 126 | public class CommentResult 127 | { 128 | public int Id { get; set; } 129 | public int ThreadId { get; set; } 130 | public int? ReplyId { get; set; } 131 | public string Content { get; set; } 132 | public int UpVotes { get; set; } 133 | public int DownVotes { get; set; } 134 | public int Votes { get; set; } 135 | public string? FlagReason { get; set; } 136 | public string? Notes { get; set; } 137 | public int AppUserId { get; set; } 138 | public string DisplayName { get; set; } 139 | public string? Handle { get; set; } 140 | public string? ProfileUrl { get; set; } 141 | public string? Avatar { get; set; } //overrides ProfileUrl 142 | public DateTime CreatedDate { get; set; } 143 | public DateTime ModifiedDate { get; set; } 144 | } 145 | -------------------------------------------------------------------------------- /CreatorKit.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. -------------------------------------------------------------------------------- /CreatorKit.Tests/CreatorKit.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 | 22 | 23 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /CreatorKit.Tests/DbTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CreatorKit.ServiceInterface; 4 | using CreatorKit.ServiceModel.Types; 5 | using NUnit.Framework; 6 | using ServiceStack; 7 | using ServiceStack.Data; 8 | using ServiceStack.OrmLite; 9 | 10 | namespace CreatorKit.Tests; 11 | 12 | [TestFixture, Explicit] 13 | public class DbTests 14 | { 15 | IDbConnectionFactory ResolveDbFactory() => new ConfigureDb().ConfigureAndResolve(); 16 | 17 | [Test] 18 | public void Does_not_return_invalid_emails_in_ActiveSubscribers() 19 | { 20 | using var db = ResolveDbFactory().OpenDbConnection(); 21 | 22 | OrmLiteUtils.PrintSql(); 23 | var subs = db.GetActiveSubscribers(MailingList.MonthlyNewsletter); 24 | Console.WriteLine($"Subscribers: {subs.Count}"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CreatorKit.Tests/IntegrationTest.cs: -------------------------------------------------------------------------------- 1 | using Funq; 2 | using ServiceStack; 3 | using NUnit.Framework; 4 | using CreatorKit.ServiceInterface; 5 | using CreatorKit.ServiceModel; 6 | 7 | namespace CreatorKit.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 | DefaultScriptContext.ScriptAssemblies.Add(typeof(Hello).Assembly); 21 | DefaultScriptContext.ScriptMethods.Add(new ValidationScripts()); 22 | DefaultScriptContext.Args[nameof(AppData)] = AppData.Instance; 23 | } 24 | } 25 | 26 | public IntegrationTest() 27 | { 28 | CreatorKit.AppHost.RegisterLicense(); 29 | appHost = new AppHost() 30 | .Init() 31 | .Start(BaseUri); 32 | } 33 | 34 | [OneTimeTearDown] 35 | public void OneTimeTearDown() => appHost.Dispose(); 36 | 37 | public IServiceClient CreateClient() => new JsonServiceClient(BaseUri); 38 | 39 | [Test] 40 | public void Can_call_Hello_Service() 41 | { 42 | var client = CreateClient(); 43 | 44 | var response = client.Get(new Hello { Name = "World" }); 45 | 46 | Assert.That(response.Result, Is.EqualTo("Hello, World!")); 47 | } 48 | } -------------------------------------------------------------------------------- /CreatorKit.Tests/MigrationTasks.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ServiceStack.OrmLite; 3 | using ServiceStack; 4 | using ServiceStack.Data; 5 | using CreatorKit.Migrations; 6 | 7 | namespace CreatorKit.Tests; 8 | 9 | [TestFixture, Explicit, Category(nameof(MigrationTasks))] 10 | public class MigrationTasks 11 | { 12 | IDbConnectionFactory ResolveDbFactory() => new ConfigureDb().ConfigureAndResolve(); 13 | Migrator CreateMigrator() => new(ResolveDbFactory(), typeof(Migration1000).Assembly); 14 | 15 | [Test] 16 | public void Migrate() 17 | { 18 | var migrator = CreateMigrator(); 19 | var result = migrator.Run(); 20 | Assert.That(result.Succeeded); 21 | } 22 | 23 | [Test] 24 | public void Revert_All() 25 | { 26 | var migrator = CreateMigrator(); 27 | var result = migrator.Revert(Migrator.All); 28 | Assert.That(result.Succeeded); 29 | } 30 | 31 | [Test] 32 | public void Revert_Last() 33 | { 34 | var migrator = CreateMigrator(); 35 | var result = migrator.Revert(Migrator.Last); 36 | Assert.That(result.Succeeded); 37 | } 38 | 39 | [Test] 40 | public void Rerun_Last_Migration() 41 | { 42 | Revert_Last(); 43 | Migrate(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CreatorKit.Tests/SeedDataTasks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using CreatorKit.ServiceInterface; 5 | using CreatorKit.ServiceModel; 6 | using CreatorKit.ServiceModel.Types; 7 | using NUnit.Framework; 8 | using ServiceStack; 9 | using ServiceStack.Auth; 10 | using ServiceStack.Data; 11 | using ServiceStack.OrmLite; 12 | 13 | namespace CreatorKit.Tests; 14 | 15 | [TestFixture, Explicit, Category(nameof(MigrationTasks))] 16 | public class SeedDataTasks 17 | { 18 | IDbConnectionFactory ResolveDbFactory() => new ConfigureDb().ConfigureAndResolve(); 19 | 20 | public string GetHostDir() 21 | { 22 | var appSettings = (Dictionary) JSON.parse(Path.GetFullPath("appsettings.json").ReadAllText()); 23 | return Path.GetFullPath((string) appSettings["HostDir"]); 24 | } 25 | 26 | [Test] 27 | public void Serialize_Users() 28 | { 29 | var hostDir = GetHostDir(); 30 | 31 | using var db = ResolveDbFactory().Open(); 32 | var users = db.Select(db.From()); 33 | var roleLookup = db.Lookup(db.From() 34 | .Select(x => new { x.Id, x.Role })); 35 | 36 | foreach (var user in users) 37 | { 38 | user.Roles = roleLookup.TryGetValue(user.Id, out var roles) 39 | ? roles.ToArray() 40 | : null; 41 | } 42 | File.WriteAllText(Path.Combine(hostDir, "Migrations/seed/users.csv"), users.ToCsv()); 43 | } 44 | 45 | [Test] 46 | public void Serialize_Contacts() 47 | { 48 | var hostDir = GetHostDir(); 49 | 50 | using var db = ResolveDbFactory().Open(); 51 | var contacts = db.Select(db.From()); 52 | File.WriteAllText(Path.Combine(hostDir, "Migrations/seed/subscribers.csv"), contacts.ToCsv()); 53 | } 54 | 55 | [Test] 56 | public void Save_MailingLists_Enum() 57 | { 58 | var hostDir = GetHostDir(); 59 | 60 | EmailRenderer.SaveMailingListEnum(seedPath:Path.Combine(hostDir, "Migrations/seed/mailinglists.csv"), 61 | savePath:Path.Combine(hostDir, "../CreatorKit.ServiceModel/Types/MailingList.cs")); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /CreatorKit.Tests/UnitTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ServiceStack; 3 | using ServiceStack.Testing; 4 | using CreatorKit.ServiceInterface; 5 | using CreatorKit.ServiceModel; 6 | 7 | namespace CreatorKit.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 | } -------------------------------------------------------------------------------- /CreatorKit.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "HostDir": "../../../../CreatorKit" 3 | } 4 | -------------------------------------------------------------------------------- /CreatorKit.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}") = "CreatorKit", "CreatorKit\CreatorKit.csproj", "{5F817400-1A3A-48DF-98A6-E7E5A3DC762F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreatorKit.ServiceInterface", "CreatorKit.ServiceInterface\CreatorKit.ServiceInterface.csproj", "{5B8FFF01-1E0B-477D-9D7F-93016C128B23}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreatorKit.ServiceModel", "CreatorKit.ServiceModel\CreatorKit.ServiceModel.csproj", "{0127B6CA-1B79-46A6-8307-B36836D107F0}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreatorKit.Tests", "CreatorKit.Tests\CreatorKit.Tests.csproj", "{455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreatorKit.Extensions", "CreatorKit.Extensions\CreatorKit.Extensions.csproj", "{920FC022-8F2A-4BB9-AF48-6810B13598C5}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreatorKit.Extensions.ServiceModel", "CreatorKit.Extensions.ServiceModel\CreatorKit.Extensions.ServiceModel.csproj", "{50C95DAF-7621-4428-9364-80C531085528}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {5F817400-1A3A-48DF-98A6-E7E5A3DC762F}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {5B8FFF01-1E0B-477D-9D7F-93016C128B23}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {0127B6CA-1B79-46A6-8307-B36836D107F0}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {920FC022-8F2A-4BB9-AF48-6810B13598C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {920FC022-8F2A-4BB9-AF48-6810B13598C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {920FC022-8F2A-4BB9-AF48-6810B13598C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {920FC022-8F2A-4BB9-AF48-6810B13598C5}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {50C95DAF-7621-4428-9364-80C531085528}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {50C95DAF-7621-4428-9364-80C531085528}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {50C95DAF-7621-4428-9364-80C531085528}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {50C95DAF-7621-4428-9364-80C531085528}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {02854F2A-8EF4-468E-80A3-CD64BBAF5D15} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /CreatorKit/Configure.AppHost.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.Extensions; 2 | using Funq; 3 | using CreatorKit.ServiceInterface; 4 | using CreatorKit.ServiceModel; 5 | 6 | [assembly: HostingStartup(typeof(CreatorKit.AppHost))] 7 | 8 | namespace CreatorKit; 9 | 10 | public class AppHost : AppHostBase, IHostingStartup 11 | { 12 | public void Configure(IWebHostBuilder builder) => builder 13 | .ConfigureServices((context,services) => { 14 | AppData.Set(context.Configuration); 15 | services.AddSingleton(AppData.Instance); 16 | services.AddSingleton(); 17 | services.AddPlugin(new CleanUrlsFeature()); 18 | 19 | var scripts = InitOptions.ScriptContext; 20 | scripts.ScriptAssemblies.Add(typeof(Hello).Assembly); 21 | scripts.ScriptMethods.Add(new ValidationScripts()); 22 | scripts.Args[nameof(AppData)] = AppData.Instance; 23 | }); 24 | 25 | public AppHost() : base("Creator Kit", typeof(MyServices).Assembly, typeof(CustomEmailServices).Assembly) {} 26 | 27 | public override void Configure(Container container) 28 | { 29 | SetConfig(new HostConfig { 30 | AddRedirectParamsToQueryString = true, 31 | UseSameSiteCookies = false, 32 | AllowFileExtensions = { "json" } 33 | }); 34 | 35 | ConfigurePlugin(x => 36 | x.MetadataTypesConfig.GlobalNamespace = nameof(CreatorKit)); 37 | 38 | MarkdownConfig.Transformer = new MarkdigTransformer(); 39 | container.Resolve().Load(this, 40 | ContentRootDirectory.GetDirectory("emails"), RootDirectory.GetDirectory("img/mail")); 41 | } 42 | 43 | public static void RegisterLicense() => 44 | Licensing.RegisterLicense("OSS BSD-3-Clause 2024 https://github.com/NetCoreApps/CreatorKit TsHchf3Hat2/T1AeeLIMaHp3JR0hgfO96KjlH208d15NqUYwm0G/qcI76DtTQzWDwsezh7CEuaZ5QEmow0ZoP3VNm/M9J2V8kaj2bRQ+00PjwVYwwFQgxwDP3g6QvTKCwbfkiPqO1cTeaQxCqGFpND3Ky9b8CQapMBI21XuES+s="); 45 | } 46 | 47 | public class MarkdigTransformer : IMarkdownTransformer 48 | { 49 | private Markdig.MarkdownPipeline Pipeline { get; } = 50 | Markdig.MarkdownExtensions.UseAdvancedExtensions(new Markdig.MarkdownPipelineBuilder()).Build(); 51 | public string Transform(string markdown) => Markdig.Markdown.ToHtml(markdown, Pipeline); 52 | } -------------------------------------------------------------------------------- /CreatorKit/Configure.AutoQuery.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack; 2 | 3 | [assembly: HostingStartup(typeof(CreatorKit.ConfigureAutoQuery))] 4 | 5 | namespace CreatorKit; 6 | 7 | public class ConfigureAutoQuery : IHostingStartup 8 | { 9 | public void Configure(IWebHostBuilder builder) => builder 10 | .ConfigureAppHost(appHost => { 11 | appHost.Plugins.Add(new AutoQueryFeature { 12 | MaxLimit = 1000, 13 | //IncludeTotal = true, 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /CreatorKit/Configure.BackgroundJobs.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Identity.UI.Services; 3 | using ServiceStack; 4 | using ServiceStack.Jobs; 5 | using CreatorKit.Data; 6 | using CreatorKit.ServiceInterface; 7 | using CreatorKit.ServiceModel; 8 | 9 | [assembly: HostingStartup(typeof(CreatorKit.ConfigureBackgroundJobs))] 10 | 11 | namespace CreatorKit; 12 | 13 | public class ConfigureBackgroundJobs : IHostingStartup 14 | { 15 | public void Configure(IWebHostBuilder builder) => builder 16 | .ConfigureServices((context,services) => { 17 | var smtpConfig = context.Configuration.GetSection(nameof(SmtpConfig))?.Get(); 18 | if (smtpConfig is not null) 19 | { 20 | services.AddSingleton(smtpConfig); 21 | } 22 | // Lazily register SendEmailCommand to allow SmtpConfig to only be required if used 23 | services.AddTransient(c => new SendEmailCommand( 24 | c.GetRequiredService>(), 25 | c.GetRequiredService(), 26 | c.GetRequiredService())); 27 | services.AddTransient(c => new EmailMessageCommand( 28 | c.GetRequiredService())); 29 | 30 | services.AddPlugin(new CommandsFeature()); 31 | services.AddPlugin(new BackgroundsJobFeature()); 32 | services.AddHostedService(); 33 | }).ConfigureAppHost(afterAppHostInit: appHost => { 34 | var services = appHost.GetApplicationServices(); 35 | 36 | // Log if EmailSender is enabled and SmtpConfig missing 37 | var log = services.GetRequiredService>(); 38 | var emailSender = services.GetRequiredService>(); 39 | if (emailSender is EmailSender) 40 | { 41 | var smtpConfig = services.GetService(); 42 | if (smtpConfig is null) 43 | { 44 | log.LogWarning("SMTP is not configured, please configure SMTP to enable sending emails"); 45 | } 46 | else 47 | { 48 | log.LogWarning("SMTP is configured with <{FromEmail}> {FromName}", smtpConfig.FromEmail, smtpConfig.FromName); 49 | } 50 | } 51 | 52 | var jobs = services.GetRequiredService(); 53 | // Example of registering a Recurring Job to run Every Hour 54 | //jobs.RecurringCommand(Schedule.Hourly); 55 | }); 56 | } 57 | 58 | public class JobsHostedService(ILogger log, IBackgroundJobs jobs) : BackgroundService 59 | { 60 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 61 | { 62 | await jobs.StartAsync(stoppingToken); 63 | 64 | using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3)); 65 | while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) 66 | { 67 | await jobs.TickAsync(); 68 | } 69 | } 70 | } 71 | 72 | /// 73 | /// Sends emails by executing SendEmailCommand in a background job where it's serially processed by 'smtp' worker 74 | /// 75 | public class EmailSender(IBackgroundJobs jobs) : IEmailSender 76 | { 77 | public Task SendEmailAsync(string email, string subject, string htmlMessage) 78 | { 79 | jobs.EnqueueCommand(new SendEmail { 80 | To = email, 81 | Subject = subject, 82 | BodyHtml = htmlMessage, 83 | }); 84 | return Task.CompletedTask; 85 | } 86 | 87 | public Task SendConfirmationLinkAsync(AppUser user, string email, string confirmationLink) => 88 | SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here."); 89 | 90 | public Task SendPasswordResetLinkAsync(AppUser user, string email, string resetLink) => 91 | SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here."); 92 | 93 | public Task SendPasswordResetCodeAsync(AppUser user, string email, string resetCode) => 94 | SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}"); 95 | } 96 | 97 | // Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation. 98 | internal sealed class IdentityNoOpEmailSender : IEmailSender 99 | { 100 | private readonly IEmailSender emailSender = new NoOpEmailSender(); 101 | 102 | public Task SendConfirmationLinkAsync(AppUser user, string email, string confirmationLink) => 103 | emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here."); 104 | 105 | public Task SendPasswordResetLinkAsync(AppUser user, string email, string resetLink) => 106 | emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here."); 107 | 108 | public Task SendPasswordResetCodeAsync(AppUser user, string email, string resetCode) => 109 | emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}"); 110 | } 111 | -------------------------------------------------------------------------------- /CreatorKit/Configure.Cors.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.ServiceInterface; 2 | 3 | [assembly: HostingStartup(typeof(CreatorKit.ConfigureCors))] 4 | 5 | namespace CreatorKit; 6 | 7 | public class ConfigureCors : IHostingStartup 8 | { 9 | public void Configure(IWebHostBuilder builder) => builder 10 | .ConfigureServices(services => 11 | { 12 | var allowOrigins = AppData.Instance.AllowOrigins ?? []; 13 | allowOrigins.AddIfNotExists(AppData.Instance.WebsiteBaseUrl); 14 | 15 | services.AddCors(options => { 16 | options.AddDefaultPolicy(policy => { 17 | policy.WithOrigins(allowOrigins.ToArray()) 18 | .AllowAnyMethod() 19 | .AllowCredentials() 20 | .WithHeaders(["Content-Type", "Allow", "Authorization"]) 21 | .SetPreflightMaxAge(TimeSpan.FromHours(1)); 22 | }); 23 | }); 24 | services.AddTransient(); 25 | }); 26 | 27 | public class StartupFilter : IStartupFilter 28 | { 29 | public Action Configure(Action next) => app => 30 | { 31 | app.UseCors(); 32 | next(app); 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CreatorKit/Configure.Db.Migrations.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.EntityFrameworkCore; 3 | using ServiceStack.Data; 4 | using ServiceStack.OrmLite; 5 | using CreatorKit.Data; 6 | using CreatorKit.Migrations; 7 | using CreatorKit.ServiceInterface; 8 | using CreatorKit.ServiceModel; 9 | using ServiceStack.Configuration; 10 | 11 | [assembly: HostingStartup(typeof(CreatorKit.ConfigureDbMigrations))] 12 | 13 | namespace CreatorKit; 14 | 15 | // Code-First DB Migrations: https://docs.servicestack.net/ormlite/db-migrations 16 | public class ConfigureDbMigrations : IHostingStartup 17 | { 18 | public void Configure(IWebHostBuilder builder) => builder 19 | .ConfigureAppHost(appHost => { 20 | var migrator = new Migrator(appHost.Resolve(), typeof(Migration1000).Assembly); 21 | AppTasks.Register("migrate", _ => 22 | { 23 | var log = appHost.GetApplicationServices().GetRequiredService>(); 24 | 25 | log.LogInformation("Running EF Migrations..."); 26 | var scopeFactory = appHost.GetApplicationServices().GetRequiredService(); 27 | using (var scope = scopeFactory.CreateScope()) 28 | { 29 | using var db = scope.ServiceProvider.GetRequiredService(); 30 | db.Database.EnsureCreated(); 31 | db.Database.Migrate(); 32 | 33 | // Only seed users if DB was just created 34 | if (!db.Users.Any()) 35 | { 36 | log.LogInformation("Adding Seed Users..."); 37 | AddSeedUsers(scope.ServiceProvider).Wait(); 38 | } 39 | } 40 | 41 | log.LogInformation("Running OrmLite Migrations..."); 42 | migrator.Run(); 43 | }); 44 | AppTasks.Register("migrate.revert", args => migrator.Revert(args[0])); 45 | AppTasks.Run(); 46 | }); 47 | 48 | private async Task AddSeedUsers(IServiceProvider services) 49 | { 50 | //initializing custom roles 51 | var roleManager = services.GetRequiredService>(); 52 | var userManager = services.GetRequiredService>(); 53 | string[] allRoles = [RoleNames.Admin]; 54 | 55 | void assertResult(IdentityResult result) 56 | { 57 | if (!result.Succeeded) 58 | throw new Exception(result.Errors.First().Description); 59 | } 60 | 61 | async Task EnsureUserAsync(AppUser user, string password, string[]? roles = null) 62 | { 63 | var existingUser = await userManager.FindByEmailAsync(user.Email!); 64 | if (existingUser != null) return; 65 | 66 | await userManager!.CreateAsync(user, password); 67 | if (roles?.Length > 0) 68 | { 69 | var newUser = await userManager.FindByEmailAsync(user.Email!); 70 | assertResult(await userManager.AddToRolesAsync(user, roles)); 71 | } 72 | } 73 | 74 | foreach (var roleName in allRoles) 75 | { 76 | var roleExist = await roleManager.RoleExistsAsync(roleName); 77 | if (!roleExist) 78 | { 79 | //Create the roles and seed them to the database 80 | assertResult(await roleManager.CreateAsync(new AppRole(roleName))); 81 | } 82 | } 83 | 84 | var password = "p@55wOrd"; 85 | var seedUsers = (await File.ReadAllTextAsync("Migrations/seed/users.csv")).FromCsv>(); 86 | foreach (var user in seedUsers) 87 | { 88 | var name = $"{user.FirstName} {user.LastName}"; 89 | await EnsureUserAsync(new AppUser 90 | { 91 | DisplayName = name, 92 | Email = user.Email, 93 | UserName = user.Email, 94 | FirstName = user.FirstName, 95 | LastName = user.LastName, 96 | EmailConfirmed = true, 97 | ProfileUrl = ImageCreator.Instance.CreateSvgDataUri(char.ToUpper(name[0])), 98 | }, password, user.Roles); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /CreatorKit/Configure.Db.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.Data; 2 | using CreatorKit.ServiceModel.Types; 3 | using Microsoft.EntityFrameworkCore; 4 | using ServiceStack.Data; 5 | using ServiceStack.OrmLite; 6 | using ServiceStack.OrmLite.Converters; 7 | using ServiceStack.Text; 8 | 9 | [assembly: HostingStartup(typeof(CreatorKit.ConfigureDb))] 10 | 11 | namespace CreatorKit; 12 | 13 | public class SeedUser 14 | { 15 | public int Id { get; set; } 16 | public string Email { get; set; } 17 | public string FirstName { get; set; } 18 | public string LastName { get; set; } 19 | public string[]? Roles { get; set; } 20 | } 21 | public class SeedContact 22 | { 23 | public string Email { get; set; } 24 | public string FirstName { get; set; } 25 | public string LastName { get; set; } 26 | public MailingList MailingLists { get; set; } 27 | } 28 | 29 | public class ConfigureDb : IHostingStartup 30 | { 31 | public void Configure(IWebHostBuilder builder) => builder 32 | .ConfigureServices((context, services) => 33 | { 34 | var dbPath = "App_Data/app.db"; 35 | var connectionString = context.Configuration.GetConnectionString("DefaultConnection") 36 | ?? $"DataSource={dbPath};Cache=Shared"; 37 | 38 | var dialect = SqliteDialect.Instance; 39 | dialect.StringSerializer = new JsonStringSerializer(); 40 | dialect.EnableForeignKeys = true; 41 | ((DateTimeConverter)dialect.GetConverter()).DateStyle = DateTimeKind.Utc; 42 | 43 | var dbFactory = new OrmLiteConnectionFactory(dbPath, dialect); 44 | services.AddSingleton(dbFactory); 45 | 46 | // $ dotnet ef migrations add CreateIdentitySchema 47 | // $ dotnet ef database update 48 | services.AddDbContext(options => 49 | options.UseSqlite(connectionString, b => b.MigrationsAssembly(nameof(CreatorKit)))); 50 | 51 | // Enable built-in Database Admin UI at /admin-ui/database 52 | services.AddPlugin(new AdminDatabaseFeature()); 53 | }) 54 | .ConfigureAppHost(appHost => { 55 | // Enable built-in Database Admin UI at /admin-ui/database 56 | // appHost.Plugins.Add(new AdminDatabaseFeature()); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /CreatorKit/Configure.Extensions.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.ServiceInterface; 2 | 3 | [assembly: HostingStartup(typeof(CreatorKit.ConfigureExtensions))] 4 | 5 | namespace CreatorKit; 6 | 7 | public class ConfigureExtensions : IHostingStartup 8 | { 9 | public void Configure(IWebHostBuilder builder) => builder 10 | .ConfigureServices((context, services) => { 11 | services.AddSingleton(); 12 | }) 13 | .ConfigureAppHost(appHost => { 14 | // Configure ServiceStack AppHost 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /CreatorKit/Configure.HealthChecks.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Diagnostics.HealthChecks; 2 | 3 | [assembly: HostingStartup(typeof(CreatorKit.HealthChecks))] 4 | 5 | namespace CreatorKit; 6 | 7 | public class HealthChecks : IHostingStartup 8 | { 9 | public class HealthCheck : IHealthCheck 10 | { 11 | public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken token = default) 12 | { 13 | // Perform health check logic here 14 | return HealthCheckResult.Healthy(); 15 | } 16 | } 17 | 18 | public void Configure(IWebHostBuilder builder) 19 | { 20 | builder.ConfigureServices(services => 21 | { 22 | services.AddHealthChecks() 23 | .AddCheck("HealthCheck"); 24 | 25 | services.AddTransient(); 26 | }); 27 | } 28 | 29 | public class StartupFilter : IStartupFilter 30 | { 31 | public Action Configure(Action next) 32 | => app => { 33 | app.UseHealthChecks("/up"); 34 | next(app); 35 | }; 36 | } 37 | } -------------------------------------------------------------------------------- /CreatorKit/Configure.Mail.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.ServiceInterface; 2 | 3 | [assembly: HostingStartup(typeof(CreatorKit.ConfigureMail))] 4 | 5 | namespace CreatorKit; 6 | 7 | public class ConfigureMail : IHostingStartup 8 | { 9 | public void Configure(IWebHostBuilder builder) => builder 10 | .ConfigureServices((context, services) => { 11 | services.AddSingleton(); 12 | }) 13 | .ConfigureAppHost(appHost => 14 | { 15 | var services = appHost.GetApplicationServices(); 16 | var mail = (MailProvider)services.GetRequiredService(); 17 | mail.InitSchema(); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /CreatorKit/CreatorKit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | DefaultContainer 8 | 9 | 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 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /CreatorKit/Migrations/Migration1000.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.DataAnnotations; 2 | using ServiceStack.OrmLite; 3 | using CreatorKit.ServiceModel; 4 | 5 | namespace CreatorKit.Migrations; 6 | 7 | public class Migration1000 : MigrationBase 8 | { 9 | [Icon(Svg = Icons.Thread)] 10 | [AutoPopulate(nameof(ExternalRef), Eval = "nguid")] 11 | public class Thread 12 | { 13 | [AutoIncrement] 14 | public int Id { get; set; } 15 | [Index(Unique = true)] 16 | public string Url { get; set; } 17 | public string Description { get; set; } 18 | public string ExternalRef { get; set; } 19 | public int ViewCount { get; set; } 20 | [Default(1)] 21 | public long LikesCount { get; set; } 22 | public long CommentsCount { get; set; } 23 | public long? RefId { get; set; } 24 | public string RefIdStr { get; set; } 25 | public DateTime CreatedDate { get; set; } 26 | public DateTime? ClosedDate { get; set; } 27 | public DateTime? DeletedDate { get; set; } 28 | } 29 | 30 | [Icon(Svg = Icons.Comment)] 31 | public class Comment : AuditBase 32 | { 33 | [AutoIncrement] 34 | public int Id { get; set; } 35 | public int ThreadId { get; set; } 36 | public int? ReplyId { get; set; } 37 | public string Content { get; set; } 38 | [Default(0)] 39 | public int UpVotes { get; set; } 40 | [Default(0)] 41 | public int DownVotes { get; set; } 42 | public int Votes { get; set; } 43 | public string? FlagReason { get; set; } 44 | public string? Notes { get; set; } 45 | public int AppUserId { get; set; } 46 | } 47 | 48 | [UniqueConstraint(nameof(ThreadId), nameof(AppUserId))] 49 | public class ThreadLike 50 | { 51 | [AutoIncrement] 52 | public long Id { get; set; } 53 | 54 | [References(typeof(Thread))] 55 | public int ThreadId { get; set; } 56 | [References(typeof(AppUser))] 57 | public int AppUserId { get; set; } 58 | public DateTime CreatedDate { get; set; } 59 | } 60 | 61 | [Icon(Svg = Icons.Vote)] 62 | [UniqueConstraint(nameof(CommentId), nameof(AppUserId))] 63 | public class CommentVote 64 | { 65 | [AutoIncrement] 66 | public long Id { get; set; } 67 | 68 | [References(typeof(Comment))] 69 | public int CommentId { get; set; } 70 | [References(typeof(AppUser))] 71 | public int AppUserId { get; set; } 72 | public int Vote { get; set; } // -1 / 1 73 | public DateTime CreatedDate { get; set; } 74 | } 75 | 76 | [Icon(Svg = Icons.Report)] 77 | public class CommentReport 78 | { 79 | [AutoIncrement] 80 | public long Id { get; set; } 81 | 82 | [References(typeof(Comment))] 83 | public int CommentId { get; set; } 84 | 85 | [Reference] 86 | public Comment Comment { get; set; } 87 | 88 | [References(typeof(AppUser))] 89 | public int AppUserId { get; set; } 90 | 91 | public PostReport PostReport { get; set; } 92 | public string Description { get; set; } 93 | 94 | public DateTime CreatedDate { get; set; } 95 | public ModerationDecision Moderation { get; set; } 96 | public string? Notes { get; set; } 97 | } 98 | 99 | public enum PostReport 100 | { 101 | Offensive, 102 | Spam, 103 | Nudity, 104 | Illegal, 105 | Other, 106 | } 107 | 108 | public enum ModerationDecision 109 | { 110 | [Description("Allow Comment")] 111 | Allow, 112 | [Description("Flag Comment")] 113 | Flag, 114 | [Description("Delete Comment")] 115 | Delete, 116 | [Description("Ban User for a day")] 117 | Ban1Day, 118 | [Description("Ban User for a week")] 119 | Ban1Week, 120 | [Description("Ban User for a month")] 121 | Ban1Month, 122 | [Description("Permanently Ban User")] 123 | PermanentBan, 124 | } 125 | 126 | public override void Up() 127 | { 128 | Db.CreateTable(); 129 | Db.CreateTable(); 130 | Db.CreateTable(); 131 | Db.CreateTable(); 132 | Db.CreateTable(); 133 | } 134 | 135 | public override void Down() 136 | { 137 | Db.DropTable(); 138 | Db.DropTable(); 139 | Db.DropTable(); 140 | Db.DropTable(); 141 | Db.DropTable(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /CreatorKit/Migrations/Migration1001.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.ServiceInterface; 2 | using ServiceStack.DataAnnotations; 3 | using ServiceStack.OrmLite; 4 | using CreatorKit.ServiceModel; 5 | 6 | namespace CreatorKit.Migrations; 7 | 8 | public class Migration1001 : MigrationBase 9 | { 10 | [Icon(Svg = Icons.Contact)] 11 | public class Contact 12 | { 13 | [AutoIncrement] 14 | public int Id { get; set; } 15 | public string Email { get; set; } 16 | public string FirstName { get; set; } 17 | public string LastName { get; set; } 18 | public Source Source { get; set; } 19 | [FormatEnumFlags(nameof(MailingList))] 20 | public MailingList MailingLists { get; set; } 21 | public string Token { get; set; } 22 | [Index(Unique = true)] 23 | public string EmailLower { get; set; } 24 | [Index] 25 | public string NameLower { get; set; } 26 | [Index(Unique = true)] 27 | public string ExternalRef { get; set; } 28 | public int? AppUserId { get; set; } 29 | public DateTime CreatedDate { get; set; } 30 | public DateTime? VerifiedDate { get; set; } 31 | public DateTime? DeletedDate { get; set; } 32 | public DateTime? UnsubscribedDate { get; set; } 33 | } 34 | 35 | public enum Source 36 | { 37 | UI, 38 | } 39 | 40 | [Flags] 41 | public enum MailingList { None = 0 } 42 | 43 | Contact CreateContact(string email, string firstName, string lastName, MailingList mailingList) 44 | { 45 | return new Contact { 46 | Email = email, 47 | FirstName = firstName, 48 | LastName = lastName, 49 | EmailLower = email.ToLower(), 50 | NameLower = $"{firstName} {lastName}".ToLower(), 51 | MailingLists = mailingList, 52 | ExternalRef = Guid.NewGuid().ToString("N"), 53 | CreatedDate = DateTime.UtcNow, 54 | VerifiedDate = DateTime.UtcNow, 55 | Source = Source.UI, 56 | }; 57 | } 58 | 59 | public enum InvalidEmailStatus {} 60 | public class InvalidEmail 61 | { 62 | [AutoIncrement] 63 | public int Id { get; set; } 64 | public string Email { get; set; } 65 | public string EmailLower { get; set; } 66 | public InvalidEmailStatus Status { get; set; } 67 | } 68 | 69 | public override void Up() 70 | { 71 | Db.CreateTable(); 72 | Db.CreateTable(); 73 | 74 | // Only run during development 75 | // EmailRenderer.SaveMailingListEnum(seedPath: "Migrations/seed/mailinglists.csv", 76 | // savePath: "../CreatorKit.ServiceModel/Types/MailingList.cs"); 77 | 78 | var seedContacts = File.ReadAllText("Migrations/seed/subscribers.csv").FromCsv>(); 79 | foreach (var contact in seedContacts) 80 | { 81 | Db.Insert(CreateContact(contact.Email, contact.FirstName, contact.LastName, (MailingList)(int)contact.MailingLists)); 82 | } 83 | } 84 | 85 | public override void Down() 86 | { 87 | Db.DropTable(); 88 | Db.DropTable(); 89 | } 90 | } -------------------------------------------------------------------------------- /CreatorKit/Migrations/seed/mailinglists.csv: -------------------------------------------------------------------------------- 1 | Name,Description 2 | None,None 3 | TestGroup,Test Group 4 | MonthlyNewsletter,Monthly Newsletter 5 | BlogPostReleases,New Blog Posts 6 | VideoReleases,New Videos 7 | ProductReleases,New Product Releases 8 | YearlyUpdates,Yearly Updates 9 | -------------------------------------------------------------------------------- /CreatorKit/Migrations/seed/subscribers.csv: -------------------------------------------------------------------------------- 1 | Email,FirstName,LastName,MailingLists 2 | test@subscriber.com,Test,Subscriber,3 3 | -------------------------------------------------------------------------------- /CreatorKit/Migrations/seed/users.csv: -------------------------------------------------------------------------------- 1 | Id,Email,FirstName,LastName,Roles 2 | 1,admin@email.com,Admin,User,"[Admin]" 3 | 2,test@user.com,Test,User, 4 | -------------------------------------------------------------------------------- /CreatorKit/Program.cs: -------------------------------------------------------------------------------- 1 | using CreatorKit.Data; 2 | using CreatorKit.ServiceInterface; 3 | using CreatorKit.ServiceModel; 4 | using Microsoft.AspNetCore.HttpOverrides; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Server.IISIntegration; 7 | 8 | AppHost.RegisterLicense(); 9 | 10 | var builder = WebApplication.CreateBuilder(args); 11 | var services = builder.Services; 12 | var config = builder.Configuration; 13 | 14 | services.Configure(options => 15 | { 16 | // This lambda determines whether user consent for non-essential cookies is needed for a given request. 17 | options.CheckConsentNeeded = context => true; 18 | options.MinimumSameSitePolicy = SameSiteMode.Strict; 19 | }); 20 | 21 | services.AddIdentity(options => { 22 | //options.User.AllowedUserNameCharacters = null; 23 | //options.SignIn.RequireConfirmedAccount = true; 24 | }) 25 | .AddEntityFrameworkStores() 26 | .AddDefaultTokenProviders(); 27 | 28 | services.AddAuthentication(IISDefaults.AuthenticationScheme) 29 | .AddFacebook(options => { /* Create App https://developers.facebook.com/apps */ 30 | options.AppId = config["oauth.facebook.AppId"]!; 31 | options.AppSecret = config["oauth.facebook.AppSecret"]!; 32 | options.SaveTokens = true; 33 | options.Scope.Clear(); 34 | config.GetSection("oauth.facebook.Permissions").GetChildren() 35 | .Each(x => options.Scope.Add(x.Value!)); 36 | }) 37 | .AddGoogle(options => { /* Create App https://console.developers.google.com/apis/credentials */ 38 | options.ClientId = config["oauth.google.ConsumerKey"]!; 39 | options.ClientSecret = config["oauth.google.ConsumerSecret"]!; 40 | options.SaveTokens = true; 41 | }) 42 | .AddMicrosoftAccount(options => { /* Create App https://apps.dev.microsoft.com */ 43 | options.ClientId = config["oauth.microsoft.AppId"]!; 44 | options.ClientSecret = config["oauth.microsoft.AppSecret"]!; 45 | options.SaveTokens = true; 46 | }); 47 | 48 | services.Configure(options => { 49 | //https://github.com/aspnet/IISIntegration/issues/140#issuecomment-215135928 50 | options.ForwardedHeaders = ForwardedHeaders.XForwardedProto; 51 | }); 52 | 53 | services.Configure(options => 54 | { 55 | options.Password.RequireDigit = true; 56 | options.Password.RequiredLength = 8; 57 | options.Password.RequireNonAlphanumeric = false; 58 | options.Password.RequireUppercase = true; 59 | options.Password.RequireLowercase = false; 60 | options.Password.RequiredUniqueChars = 6; 61 | 62 | // Lockout settings 63 | options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); 64 | options.Lockout.MaxFailedAccessAttempts = 10; 65 | options.Lockout.AllowedForNewUsers = true; 66 | 67 | // User settings 68 | options.User.RequireUniqueEmail = true; 69 | }); 70 | 71 | services.ConfigureApplicationCookie(options => 72 | { 73 | // Cookie settings 74 | options.Cookie.HttpOnly = true; 75 | options.ExpireTimeSpan = TimeSpan.FromDays(150); 76 | // If the LoginPath isn't set, ASP.NET Core defaults 77 | // the path to /Account/Login. 78 | options.LoginPath = "/Account/Login"; 79 | // If the AccessDeniedPath isn't set, ASP.NET Core defaults 80 | // the path to /Account/AccessDenied. 81 | options.AccessDeniedPath = "/Account/AccessDenied"; 82 | options.SlidingExpiration = true; 83 | }); 84 | 85 | // services.AddSingleton, IdentityNoOpEmailSender>(); 86 | // Uncomment to send emails with SMTP, configure SMTP with "SmtpConfig" in appsettings.json 87 | services.AddSingleton, EmailSender>(); 88 | services.AddScoped, AdditionalUserClaimsPrincipalFactory>(); 89 | 90 | // Register all services 91 | services.AddServiceStack(typeof(MyServices).Assembly); 92 | 93 | var app = builder.Build(); 94 | 95 | // Configure the HTTP request pipeline. 96 | if (!app.Environment.IsDevelopment()) 97 | { 98 | app.UseExceptionHandler("/Error"); 99 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 100 | app.UseHsts(); 101 | app.UseHttpsRedirection(); 102 | } 103 | app.UseStaticFiles(); 104 | app.UseServiceStack(new AppHost()); 105 | 106 | app.Run(); -------------------------------------------------------------------------------- /CreatorKit/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "https://localhost:5003/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "CreatorKit": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5003/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /CreatorKit/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | }, 9 | "AppData": { 10 | "PublicBaseUrl": "https://creatorkit.netcore.io", 11 | "BaseUrl": "https://localhost:5003", 12 | "WebsiteBaseUrl": "https://localhost:5002", 13 | "allowOrigins": [ 14 | "https://localhost:5001", 15 | "http://localhost:5000", 16 | "http://localhost:8080" 17 | ] 18 | }, 19 | "oauth.facebook.Permissions": [ "email", "public_profile" ], 20 | "oauth.facebook.AppId": "231464590266507", 21 | "oauth.facebook.AppSecret": "9dd6ce54b4405dd1325d271d2419bc34", 22 | "oauth.google.ConsumerKey": "871587245318-ikriaoce2578d67qosmf19eg8m44mh2b.apps.googleusercontent.com", 23 | "oauth.google.ConsumerSecret": "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", 24 | "oauth.microsoft.AppId": "ba0d5aec-5e6e-4a7e-b02f-636ffd0111b5", 25 | "oauth.microsoft.AppSecret": "1Qm7Q~9JeytqR7UdKK~USDC7lbk0BOeOUnuks" 26 | } 27 | -------------------------------------------------------------------------------- /CreatorKit/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "AppData": { 10 | "PublicBaseUrl": "https://creatorkit.netcore.io", 11 | "BaseUrl": "https://creatorkit.netcore.io", 12 | "WebsiteBaseUrl": "https://razor-ssg.web-templates.io", 13 | "allowOrigins": [] 14 | }, 15 | "oauth.facebook.Permissions": [ "email", "public_profile" ], 16 | "oauth.facebook.AppId": "231464590266507", 17 | "oauth.facebook.AppSecret": "9dd6ce54b4405dd1325d271d2419bc34", 18 | "oauth.google.ConsumerKey": "274592649256-nmvuiu5ri7s1nghilbo6nmfd6h8j71sc.apps.googleusercontent.com", 19 | "oauth.google.ConsumerSecret": "aKOJngvq0USp3kyA_mkFH8Il", 20 | "oauth.microsoft.AppId": "ba0d5aec-5e6e-4a7e-b02f-636ffd0111b5", 21 | "oauth.microsoft.AppSecret": "1Qm7Q~9JeytqR7UdKK~USDC7lbk0BOeOUnuks", 22 | "SmtpConfig": {} 23 | } 24 | -------------------------------------------------------------------------------- /CreatorKit/docs/docs.md: -------------------------------------------------------------------------------- 1 | CreatorKit is a simple alternative solution to using Disqus to enhance websites with threading and commenting 2 | and Mailchimp for accepting and managing website newsletters and mailing lists. 3 | 4 | It's an ideal companion for JAMStack or statically generated branded websites like 5 | [Razor SSG](https://razor-ssg.web-templates.io/posts/razor-ssg) 6 | enabling you to seamlessly integrate features such as comments, voting, moderation, newsletter subscriptions, 7 | and email management into your existing websites. 8 | 9 | No longer do you need to rely on complex content management systems to manage your blog's interactions with your readers. 10 | With CreatorKit, you can enjoy the convenience of managing your blog's comments, votes, and subscriptions directly 11 | from your own hosted [CreatorKit Portal](/portal/). 12 | 13 | Additionally, CreatorKit makes it easy to send emails and templates to different mailing lists, making it the perfect 14 | tool for managing your email campaigns. Whether you're a blogger, marketer, or entrepreneur, CreatorKit is a great 15 | solution for maximizing your blog's functionality and engagement. 16 | 17 | CreatorKit is a FREE customizable .NET App included with [ServiceStack](https://servicestack.net) which is 18 | [FREE for Individuals and Open Source projects](https://servicestack.net/free). 19 | 20 | To get started, clone [CreatorKit from GitHub](https://github.com/NetCoreApps/CreatorKit) and follow the 21 | [installation instructions](#install) to configure it with your settings. -------------------------------------------------------------------------------- /CreatorKit/docs/index.md: -------------------------------------------------------------------------------- 1 | CreatorKit is a simple alternative solution to using Disqus to enhance websites with threading and commenting 2 | and Mailchimp for accepting and managing website newsletters and mailing lists. 3 | 4 | It's an ideal companion for JAMStack or statically generated branded websites like 5 | [Razor SSG](https://razor-ssg.web-templates.io/posts/razor-ssg) 6 | enabling you to seamlessly integrate features such as comments, voting, moderation, newsletter subscriptions, 7 | and email management into your existing websites. 8 | 9 | No longer do you need to rely on complex content management systems to manage your blog's interactions with your readers. 10 | With CreatorKit, you can enjoy the convenience of managing your blog's comments, votes, and subscriptions directly 11 | from your own hosted [CreatorKit Portal](/portal/). 12 | 13 | Additionally, CreatorKit makes it easy to send emails and templates to different mailing lists, making it the perfect 14 | tool for managing your email campaigns. Whether you're a blogger, marketer, or entrepreneur, CreatorKit is a great 15 | solution for maximizing your blog's functionality and engagement. 16 | 17 | CreatorKit is a FREE customizable .NET App included with [ServiceStack](https://servicestack.net) which is 18 | [FREE for Individuals and Open Source projects](https://servicestack.net/free). 19 | 20 | To get started, clone [CreatorKit from GitHub](https://github.com/NetCoreApps/CreatorKit) and follow the 21 | [installation instructions](#install) to configure it with your settings. -------------------------------------------------------------------------------- /CreatorKit/docs/install.md: -------------------------------------------------------------------------------- 1 | To install -------------------------------------------------------------------------------- /CreatorKit/emails/empty.html: -------------------------------------------------------------------------------- 1 | {{ body |> emailmarkdown }} 2 | -------------------------------------------------------------------------------- /CreatorKit/emails/layouts/empty.html: -------------------------------------------------------------------------------- 1 | {{ page }} -------------------------------------------------------------------------------- /CreatorKit/emails/newsletter-welcome.html: -------------------------------------------------------------------------------- 1 | {{#capture markdown}} 2 | Welcome to the {{info.Company}} newsletter. The team is busy working on the next edition, and it will be in your email soon. 3 | 4 | In the meanwhile, make sure you check out our [YouTube Videos]({{urls.YouTube}}) and 5 | the [servicestack.net]({{urls.Website}}) website to find out more about building beautiful, 6 | applications for mobile, web, and desktop from a single codebase. 7 | 8 | {{body}} 9 |

           

          10 | 11 | Thank you,
          12 | **{{info.SignOffTeam}}** 13 | {{/capture}} 14 | 15 |

          Welcome to {{info.Company}}

          16 | 17 |
          18 | {{markdown |> emailmarkdown}} 19 |
          20 | -------------------------------------------------------------------------------- /CreatorKit/emails/newsletter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | {{#if header}} 12 | {{ 'divider' |> partial }} 13 | {{ 'section-icon' |> partial({ iconSrc: images.mail_48x48 }) }} 14 | 15 | 18 | 19 | {{/if}} 20 | 21 | {{#if meta.WhatsNew.Count > 0 }} 22 | {{ 'divider' |> partial }} 23 | {{ 'section' |> partial({ iconSrc:images.speaker_48x48, title:'New Release' }) }} 24 | {{ var first = meta.WhatsNew[0] }} 25 | {{ 'title' |> partial({ title:first.Group, fontSize:60 }) }} 26 | 27 | 38 | 39 | {{/if}} 40 | 41 | {{#if meta.Videos.Count > 0 }} 42 | {{ 'divider' |> partial }} 43 | {{ 'section' |> partial({ iconSrc:images.video_48x48, title:'New Videos' }) }} 44 | 45 | 58 | 59 | {{/if}} 60 | 61 | {{#if meta.Posts.Count > 0 }} 62 | {{ 'divider' |> partial }} 63 | {{ 'section' |> partial({ iconSrc:images.blog_48x48, title:'New Posts' }) }} 64 | 65 | 71 | 72 | {{/if}} 73 | 74 | {{#if footer}} 75 | {{ 'divider' |> partial }} 76 | 77 | 80 | 81 | {{/if}} 82 | 83 |
          5 |

          ServiceStack Newsletter

          6 |

          7 | {{title}} 8 |

          9 |
          16 | {{ header |> emailmarkdown }} 17 |
          28 | {{#each meta.WhatsNew.where(x => x.Group == first.Group) }} 29 |
          30 |
          31 | 32 |
          33 |

          {{ it.title }} →

          34 | {{ it.Content |> emailmarkdown }} 35 |
          36 | {{/each}} 37 |
          46 | {{#each meta.Videos }} 47 |
          48 |
          49 | 50 | 51 | 52 |
          53 |

          {{ it.title }} →

          54 | {{ it.Content |> emailmarkdown }} 55 |
          56 | {{/each}} 57 |
          66 | {{#each meta.Posts }} 67 |

          {{ it.title }} →

          68 |

          {{ it.summary }}

          69 | {{/each}} 70 |
          78 | {{ footer |> emailmarkdown }} 79 |
          84 | -------------------------------------------------------------------------------- /CreatorKit/emails/partials/button-centered.html: -------------------------------------------------------------------------------- 1 | {{var color = `${it.color ?? '#4F46E5'}`}} 2 | 3 | 4 | 5 | 17 | 18 | 19 |
          6 | 7 | 8 | 9 | 13 | 14 | 15 |
          10 | 11 | {{ it.label }}  → 12 |
          16 |
          20 | -------------------------------------------------------------------------------- /CreatorKit/emails/partials/divider.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
          4 | 5 | 6 | 7 | 18 | 19 | 20 |
          8 |
          9 | 10 | 11 | 12 | 13 | 14 | 15 |

          16 |
          17 |
          21 |
          22 | 23 | 24 | -------------------------------------------------------------------------------- /CreatorKit/emails/partials/image-centered.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 |
          5 | 6 | 7 | 8 | 12 | 13 | 14 |
          9 |
          10 | 11 |
          -------------------------------------------------------------------------------- /CreatorKit/emails/partials/section-icon.html: -------------------------------------------------------------------------------- 1 |  2 | 3 |
          4 | 5 | 6 | 7 | 17 | 18 | 19 |
          8 | 9 | 10 | 11 | 13 | 14 | 15 |
          12 |
          16 |
          20 |
          21 | 22 | 23 | -------------------------------------------------------------------------------- /CreatorKit/emails/partials/section.html: -------------------------------------------------------------------------------- 1 | {{ 'section-icon' |> partial({ iconSrc: it.iconSrc }) }} 2 | 3 | 4 |
          5 | 6 | 7 | 8 | 13 | 14 | 15 |
          9 |
          10 |

          {{ it.title }}

          11 |
          12 |
          16 |
          17 | 18 | 19 | -------------------------------------------------------------------------------- /CreatorKit/emails/partials/title.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
          4 | 5 | 6 | 7 | 12 | 13 | 14 |
          8 |
          9 |

          {{ it.title }}

          10 |
          11 |
          15 |
          16 | 17 | 18 | -------------------------------------------------------------------------------- /CreatorKit/emails/vars/info.txt: -------------------------------------------------------------------------------- 1 | Company ServiceStack 2 | CompanyOfficial ServiceStack, Inc. 3 | Domain servicestack.net 4 | MailingAddress 470 Schooleys Mt Road #636, Hackettstown, NJ 07840-4096 5 | MailPreferences Mail Preferences 6 | Unsubscribe Unsubscribe 7 | Contact Contact 8 | Privacy Privacy policy 9 | OurAddress Our mailing address: 10 | MailReason You received this email because you are subscribed to ServiceStack news and announcements. 11 | SignOffTeam The ServiceStack Team 12 | NewsletterFmt ServiceStack Newsletter, {0} 13 | SocialUrls Website,Twitter,YouTube 14 | SocialImages website_24x24,twitter_24x24,youtube_24x24 -------------------------------------------------------------------------------- /CreatorKit/emails/vars/urls.txt: -------------------------------------------------------------------------------- 1 | BaseUrl {{BaseUrl}} 2 | PublicBaseUrl {{PublicBaseUrl}} 3 | WebsiteBaseUrl {{WebsiteBaseUrl}} 4 | Website {{WebsiteBaseUrl}} 5 | MailPreferences {{WebsiteBaseUrl}}/mail-preferences 6 | Unsubscribe {{WebsiteBaseUrl}}/mail-preferences 7 | Privacy {{WebsiteBaseUrl}}/privacy 8 | Contact {{WebsiteBaseUrl}}/#contact 9 | SignupConfirmed {{WebsiteBaseUrl}}/signup-confirmed 10 | Twitter https://twitter.com/ServiceStack 11 | YouTube https://www.youtube.com/channel/UC0kXKGVU4NHcwNdDdRiAJSA 12 | -------------------------------------------------------------------------------- /CreatorKit/emails/verify-email.html: -------------------------------------------------------------------------------- 1 | {{#capture markdown}} 2 | ## Verify your email address 3 | 4 | Click on the link below to verify your email address and complete Sign Up. 5 | 6 | --- 7 | 8 | {{/capture}} 9 | 10 | {{#capture footer}} 11 | --- 12 | 13 | {{body}} 14 | 15 | Thank you,
          16 | **{{info.SignOffTeam}}** 17 | {{/capture}} 18 | 19 |

           

          20 | {{ 'image-centered' |> partial({ src:images.email_100x100, width:100, height:100 }) }} 21 |

           

          22 | 23 |
          24 | {{markdown |> emailmarkdown}} 25 | {{ 'button-centered' |> partial({ label:'Verify your email address', href:`${urls.BaseUrl}/verify/email/${ExternalRef}` }) }} 26 | 27 |

           

          28 |

          If you did not make this request, you can ignore this email.

          29 |

           

          30 | 31 | {{footer |> emailmarkdown}} 32 |
          33 | -------------------------------------------------------------------------------- /CreatorKit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "postinstall": "node postinstall.js && npm run migrate", 4 | "dtos": "x mjs", 5 | "dev": "dotnet watch", 6 | "ui:dev": "npx tailwindcss@v3 -i ./tailwind.input.css -o ./wwwroot/css/app.css --watch", 7 | "ui:build": "npx tailwindcss@v3 -i ./tailwind.input.css -o ./wwwroot/css/app.css --minify && node ./postbuild.js", 8 | "migrate": "dotnet run --AppTasks=migrate", 9 | "revert:last": "dotnet run --AppTasks=migrate.revert:last", 10 | "revert:all": "dotnet run --AppTasks=migrate.revert:all" 11 | }, 12 | "dependencies": { 13 | "@servicestack/client": "^2.0.10", 14 | "@servicestack/vue": "^3.0.80", 15 | "vue": "^3.3.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CreatorKit/postbuild.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const writeTo = './wwwroot/tailwind/all.components.txt' 4 | const fromDir = './wwwroot/mjs/components' 5 | 6 | if (fs.existsSync(writeTo)) { 7 | fs.unlinkSync(writeTo) 8 | } 9 | fs.closeSync(fs.openSync(writeTo, 'w')) 10 | fs.readdirSync(fromDir).forEach(f => { 11 | const file = fromDir + '/' + f 12 | if (fs.lstatSync(file).isFile()) { 13 | fs.appendFileSync(writeTo, fs.readFileSync(file).toString()) 14 | } 15 | }) -------------------------------------------------------------------------------- /CreatorKit/postinstall.js: -------------------------------------------------------------------------------- 1 | // Usage: npm install 2 | 3 | const writeTo = './wwwroot/lib' 4 | const defaultPrefix = 'https://unpkg.com' 5 | const files = { 6 | mjs: { 7 | 'vue.mjs': '/vue@3/dist/vue.esm-browser.js', 8 | 'vue.min.mjs': '/vue@3/dist/vue.esm-browser.prod.js', 9 | 'servicestack-client.mjs': '/@servicestack/client@2/dist/servicestack-client.mjs', 10 | 'servicestack-client.min.mjs': '/@servicestack/client@2/dist/servicestack-client.min.mjs', 11 | 'servicestack-vue.mjs': '/@servicestack/vue@3/dist/servicestack-vue.mjs', 12 | 'servicestack-vue.min.mjs': '/@servicestack/vue@3/dist/servicestack-vue.min.mjs', 13 | }, 14 | } 15 | 16 | const js = ['servicestack-vue.mjs','servicestack-vue.min.mjs'] 17 | 18 | const defaultHostPrefix = 'https://raw.githubusercontent.com/NetCoreTemplates/razor-ssg/main/MyApp/' 19 | const hostFiles = [ 20 | ] 21 | 22 | hostFiles.forEach(file => { 23 | const url = file.includes('://') 24 | ? file 25 | : defaultHostPrefix + file 26 | 27 | const toDir = path.dirname(file) 28 | if (!fs.existsSync(toDir)) { 29 | fs.mkdirSync(toDir, { recursive: true }) 30 | } 31 | fetchDownload(url, file, 5) 32 | }) 33 | 34 | const path = require('path') 35 | const fs = require('fs').promises 36 | const http = require('http') 37 | const https = require('https') 38 | 39 | const requests = [] 40 | Object.keys(files).forEach(dir => { 41 | const dirFiles = files[dir] 42 | Object.keys(dirFiles).forEach(name => { 43 | let url = dirFiles[name] 44 | if (url.startsWith('/')) 45 | url = defaultPrefix + url 46 | const toFile = path.join(writeTo, dir, name) 47 | requests.push(fetchDownload(url, toFile, 5)) 48 | }) 49 | }) 50 | 51 | ;(async () => { 52 | await Promise.all(requests) 53 | js.forEach(file => { 54 | const fromFile = path.join(writeTo,'mjs', file) 55 | fs.copyFile(fromFile, path.join('./wwwroot/js', file)) 56 | }) 57 | })() 58 | 59 | async function fetchDownload(url, toFile, retries) { 60 | const toDir = path.dirname(toFile) 61 | await fs.mkdir(toDir, { recursive: true }) 62 | for (let i=retries; i>=0; --i) { 63 | try { 64 | let r = await fetch(url) 65 | if (!r.ok) { 66 | throw new Error(`${r.status} ${r.statusText}`); 67 | } 68 | let txt = await r.text() 69 | console.log(`writing ${url} to ${toFile}`) 70 | await fs.writeFile(toFile, txt) 71 | return 72 | } catch (e) { 73 | console.log(`get ${url} failed: ${e}${i > 0 ? `, ${i} retries remaining...` : ''}`) 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /CreatorKit/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./wwwroot/**/*.{html,js,mjs,md,cshtml,razor}","./Pages/**/*.{cshtml,razor}","../CreatorKit.ServiceModel/**/*.cs"], 3 | darkMode: 'class', 4 | plugins: [], 5 | } 6 | -------------------------------------------------------------------------------- /CreatorKit/tailwind.input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | 4 | /* override element defaults */ 5 | b, strong { font-weight:600; } 6 | 7 | /*vue*/ 8 | [v-cloak] {display:none} 9 | 10 | /* @tailwindcss/forms css */ 11 | [type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='week'],[type='search'],[type='tel'],[type='time'],[type='color'],[multiple],textarea,select 12 | {-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:0.5rem 0.75rem;font-size:1rem;line-height:1.5rem} 13 | [type='text']:focus,[type='email']:focus,[type='url']:focus,[type='password']:focus,[type='number']:focus,[type='date']:focus,[type='datetime-local']:focus,[type='month']:focus,[type='week']:focus,[type='search']:focus,[type='tel']:focus,[type='time']:focus,[type='color']:focus,[multiple]:focus,textarea:focus,select:focus{ 14 | outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/); 15 | --tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb; 16 | --tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 17 | --tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); 18 | box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);border-color:#2563eb} 19 | input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1} 20 | input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#6b7280;opacity:1} 21 | input::placeholder,textarea::placeholder{color:#6b7280;opacity:1} 22 | select{ 23 | background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); 24 | background-position:right 0.5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;color-adjust:exact} 25 | [multiple]{ 26 | background-image:initial;background-position:initial;background-repeat:unset;background-size:initial;padding-right:0.75rem;-webkit-print-color-adjust:unset;color-adjust:unset;} 27 | [type='checkbox'],[type='radio']{ 28 | -webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;color-adjust:exact;display:inline-block; 29 | vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none; 30 | flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px} 31 | [type='checkbox']{border-radius:0} 32 | [type='radio']{border-radius:100%} 33 | [type='checkbox']:focus,[type='radio']:focus{ 34 | outline:2px solid transparent;outline-offset:2px; 35 | --tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb; 36 | --tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 37 | --tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); 38 | box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)} 39 | [type='checkbox']:checked,[type='radio']:checked{ 40 | border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat} 41 | [type='checkbox']:checked{ 42 | background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")} 43 | [type='radio']:checked{ 44 | background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")} 45 | [type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus{ 46 | border-color:transparent;background-color:currentColor} 47 | [type='checkbox']:indeterminate{ 48 | background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); 49 | border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat} 50 | [type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus{border-color:transparent;background-color:currentColor} 51 | [type='file']{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit} 52 | [type='file']:focus{outline:1px auto -webkit-focus-ring-color;} 53 | [type='color']{height:2.4rem;padding:2px 3px} 54 | [type='range']{height:2.4rem} 55 | 56 | /* dark mode autocomplete fields */ 57 | .dark input:-webkit-autofill, 58 | .dark input:-webkit-autofill:hover, 59 | .dark input:-webkit-autofill:focus, 60 | .dark input:-webkit-autofill:active { 61 | transition: background-color 5000s ease-in-out 0s; 62 | -webkit-text-fill-color: #ffffff; 63 | } 64 | .dark input[data-autocompleted] { 65 | background-color: transparent !important; 66 | } 67 | 68 | /* @tailwindcss/aspect css */ 69 | .aspect-h-9 { 70 | --tw-aspect-h: 9; 71 | } 72 | .aspect-w-16 { 73 | position: relative; 74 | padding-bottom: calc(var(--tw-aspect-h) / var(--tw-aspect-w) * 100%); 75 | --tw-aspect-w: 16; 76 | } 77 | .aspect-w-16 > * { 78 | position: absolute; 79 | height: 100%; 80 | width: 100%; 81 | top: 0; 82 | right: 0; 83 | bottom: 0; 84 | left: 0; 85 | } 86 | 87 | [role=dialog].z-10 { 88 | z-index: 60; 89 | } 90 | 91 | @tailwind utilities; -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/blog_48x48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/blog_48x48@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/chat_48x48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/chat_48x48@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/email_100x100@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/email_100x100@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/logo_72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/logo_72x72@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/logofull_350x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/logofull_350x60@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/mail_48x48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/mail_48x48@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/speaker_48x48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/speaker_48x48@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/twitter_24x24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/twitter_24x24@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/video_48x48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/video_48x48@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/website_24x24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/website_24x24@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/welcome_650x487.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/welcome_650x487.jpg -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/youtube_24x24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/youtube_24x24@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/mail/youtube_48x48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/mail/youtube_48x48@2x.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/img/portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreApps/CreatorKit/f1ce9ebe710144ce37aed61bd81550a38ed2a792/CreatorKit/wwwroot/img/portal.png -------------------------------------------------------------------------------- /CreatorKit/wwwroot/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Admin Portal 4 | 5 | 6 | 7 | 18 | 19 | 20 |
          21 |
          22 |
          23 |

          My Profile

          24 | 25 |
          {{ user.displayName }}
          26 |
          {{ user.userName }}
          27 |
          28 | 30 | {{ role }} 31 | 32 |
          33 | Sign Out 34 |
          35 | 36 |
          37 |
          38 | 39 | -------------------------------------------------------------------------------- /CreatorKit/wwwroot/mjs/app.mjs: -------------------------------------------------------------------------------- 1 | import { createApp, nextTick, reactive } from "vue" 2 | import { JsonApiClient, $1, $$ } from "@servicestack/client" 3 | import ServiceStackVue, { useAuth } from "@servicestack/vue" 4 | import { Authenticate } from "./dtos.mjs" 5 | import { EmailInput, MarkdownEmailInput } from "./components/Inputs.mjs" 6 | 7 | let client = null, Apps = [] 8 | let AppData = { 9 | init:false 10 | } 11 | export { client, Apps } 12 | 13 | /** Shared Components */ 14 | const Components = { 15 | } 16 | 17 | const alreadyMounted = el => el.__vue_app__ 18 | 19 | /** Mount Vue3 Component 20 | * @param {string|Element} sel - Element or Selector where component should be mounted 21 | * @param component 22 | * @param {any} props= */ 23 | export function mount(sel, component, props) { 24 | if (!AppData.init) { 25 | init(globalThis) 26 | } 27 | const el = $1(sel) 28 | if (alreadyMounted(el)) return 29 | const app = createApp(component, props) 30 | app.provide('client', client) 31 | Object.keys(Components).forEach(name => { 32 | app.component(name, Components[name]) 33 | }) 34 | app.use(ServiceStackVue) 35 | app.component('RouterLink', ServiceStackVue.component('RouterLink')) 36 | if (component.components) { 37 | Object.keys(component.components).forEach(name => app.component(name, component.components[name])) 38 | } 39 | 40 | app.mount(el) 41 | Apps.push(app) 42 | // Register custom input components 43 | ServiceStackVue.component('EmailInput', EmailInput) 44 | ServiceStackVue.component('MarkdownEmailInput', MarkdownEmailInput) 45 | return app 46 | } 47 | 48 | export function mountAll() { 49 | $$('[data-component]').forEach(el => { 50 | if (alreadyMounted(el)) return 51 | let componentName = el.getAttribute('data-component') 52 | if (!componentName) return 53 | let component = Components[componentName] || ServiceStackVue.component(componentName) 54 | if (!component) { 55 | console.error(`Could not create component ${componentName}`) 56 | return 57 | } 58 | 59 | let propsStr = el.getAttribute('data-props') 60 | let props = propsStr && new Function(`return (${propsStr})`)() || {} 61 | mount(el, component, props) 62 | }) 63 | } 64 | 65 | /** @param {any} [exports] */ 66 | export function init(exports) { 67 | if (AppData.init) return 68 | client = JsonApiClient.create() 69 | AppData = reactive(AppData) 70 | AppData.init = true 71 | mountAll() 72 | 73 | nextTick(async () => { 74 | const { signIn, signOut } = useAuth() 75 | const api = await client.api(new Authenticate()) 76 | if (api.succeeded) { 77 | signIn(api.response) 78 | } else { 79 | signOut() 80 | } 81 | }) 82 | 83 | if (exports) { 84 | exports.client = client 85 | exports.Apps = Apps 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /CreatorKit/wwwroot/mjs/components/init.mjs: -------------------------------------------------------------------------------- 1 | import { $$, JsonApiClient, leftPart } from "@servicestack/client" 2 | import { createApp, reactive } from "vue" 3 | import ServiceStackVue from "@servicestack/vue" 4 | 5 | export const BaseUrl = leftPart(import.meta.url, '/mjs') 6 | 7 | let AppData = { 8 | init: false, 9 | Auth: null, 10 | UserData: null, 11 | } 12 | let client = null, store = null, Apps = [] 13 | export { client, AppData } 14 | 15 | 16 | /** @param {any} [exports] */ 17 | export function init(exports) { 18 | if (AppData.init) return 19 | client = JsonApiClient.create(BaseUrl) 20 | AppData = reactive(AppData) 21 | AppData.init = true 22 | 23 | if (exports) { 24 | exports.client = client 25 | exports.AppData = AppData 26 | exports.Apps = Apps 27 | } 28 | } 29 | 30 | const alreadyMounted = el => el.__vue_app__ 31 | 32 | /** Mount Vue3 Component 33 | * @param sel {string|Element} - Element or Selector where component should be mounted 34 | * @param component 35 | * @param [props] {any} 36 | * @param {{ mount?:(app, { client, AppData }) => void }} options= */ 37 | export function mount(sel, component, props, options) { 38 | if (!AppData.init) { 39 | init(globalThis) 40 | } 41 | const els = $$(sel) 42 | els.forEach(el => { 43 | if (alreadyMounted(el)) return 44 | const elProps = el.getAttribute('data-props') 45 | const useProps = elProps ? { ...props, ...(new Function(`return (${elProps})`)()) } : props 46 | const app = createApp(component, useProps) 47 | app.provide('client', client) 48 | 49 | app.use(ServiceStackVue) 50 | if (options?.mount) { 51 | options.mount(app, { client, AppData }) 52 | } 53 | app.mount(el) 54 | Apps.push(app) 55 | }) 56 | 57 | return Apps.length === 1 ? Apps[0] : Apps 58 | } 59 | -------------------------------------------------------------------------------- /CreatorKit/wwwroot/modules/locode/custom.js: -------------------------------------------------------------------------------- 1 | // function expandEnumFlags(value,options) { 2 | // if (!options.type) { 3 | // console.error(`enumDescriptions missing {type:EnumType} options`) 4 | // return value 5 | // } 6 | // const enumType = window.Server.api.types.find(x => x.name === options.type) 7 | // if (!enumType) { 8 | // console.error(`Could not find metadata for ${options.type}`) 9 | // return value 10 | // } 11 | // 12 | // const to = [] 13 | // for (let i=0; i 0 && (enumValue & value) === enumValue) { 16 | // to.push(enumType.enumDescriptions?.[i] || enumType.enumNames?.[i] || `${value}`) 17 | // } 18 | // } 19 | // return to.join(', ') 20 | // } 21 | // 22 | // function enumFlagsConverter(type) { 23 | // return x => expandEnumFlags(x,{type}) 24 | // } -------------------------------------------------------------------------------- /CreatorKit/wwwroot/modules/shared/custom-body.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CreatorKit/wwwroot/stats/projects.json: -------------------------------------------------------------------------------- 1 | {"cachedAt":"2023-04-22T03:08:14.4444829Z","results":{"web":24638,"mvc":6737,"selfhost":6142,"angular-spa":4474,"vue-spa":3931,"razor":3793,"blazor-tailwind":3592,"react-spa":3545,"blazor-wasm":2868,"winservice":2283,"vue-nuxt":2199,"vuetify-spa":1964,"svelte-spa":1875,"nextjs":1773,"aurelia-spa":1734,"empty":1696,"script":1493,"vuetify-nuxt":1413,"vue-ssg":1346,"angular-lite-spa":1288,"mvcauth":1277,"grpc":1138,"mvcidentity":1124,"vue-vite":1096,"blazor-server":1051,"vue-desktop":978,"vue-lite":965,"mvcidentityserver":949,"react-lite":919,"aws-lambda":672,"worker-rabbitmq":632,"worker-redismq":614,"nukedbit/blazor-wasm-servicestack":565,"parcel":551,"worker-servicebus":461,"react-desktop-apps":452,"worker-sqs":437,"bare-app":291,"vue-mjs":223,"web-tailwind":169,"mvc-tailwind":131,"razor-tailwind":130,"parcel-app":97,"razor-pages":79,"webapp":66}} -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007-present, Demis Bellot, ServiceStack, Inc. 2 | https://servicestack.net 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the ServiceStack nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://servicestack.net/img/pages/creatorkit/creatorkit-screenshot-intro.png)](https://servicestack.net/creatorkit/) 2 | 3 | ### CreatorKit Links 4 | 5 | - [Documentation](https://servicestack.net/creatorkit/) 6 | 7 | ### Live Demos 8 | 9 | - [Website Integration](https://razor-ssg.web-templates.io) 10 | - [CreatorKit Portal](https://creatorkit.netcore.io) 11 | -------------------------------------------------------------------------------- /config/deploy.yml: -------------------------------------------------------------------------------- 1 | # Name of your application. Used to uniquely configure containers. 2 | service: creatorkit 3 | 4 | # Name of the container image. 5 | image: netcoreapps/creatorkit 6 | 7 | # Required for use of ASP.NET Core with Kamal-Proxy. 8 | env: 9 | ASPNETCORE_FORWARDEDHEADERS_ENABLED: true 10 | 11 | # Deploy to these servers. 12 | servers: 13 | # IP address of server, optionally use env variable. 14 | web: 15 | - 5.78.77.149 16 | # - <%= ENV['KAMAL_DEPLOY_IP'] %> 17 | 18 | 19 | # Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server). 20 | # If using something like Cloudflare, it is recommended to set encryption mode 21 | # in Cloudflare's SSL/TLS setting to "Full" to enable end-to-end encryption. 22 | proxy: 23 | ssl: true 24 | host: creatorkit.netcore.io 25 | # kamal-proxy connects to your container over port 80, use `app_port` to specify a different port. 26 | app_port: 8080 27 | 28 | # Credentials for your image host. 29 | registry: 30 | # Specify the registry server, if you're not using Docker Hub 31 | server: ghcr.io 32 | username: 33 | - KAMAL_REGISTRY_USERNAME 34 | 35 | # Always use an access token rather than real password (pulled from .kamal/secrets). 36 | password: 37 | - KAMAL_REGISTRY_PASSWORD 38 | 39 | # Configure builder setup. 40 | builder: 41 | arch: amd64 42 | 43 | volumes: 44 | - "/opt/docker/CreatorKit/App_Data:/app/App_Data" 45 | 46 | #accessories: 47 | # litestream: 48 | # roles: ["web"] 49 | # image: litestream/litestream 50 | # files: ["config/litestream.yml:/etc/litestream.yml"] 51 | # volumes: ["/opt/docker/CreatorKit/App_Data:/data"] 52 | # cmd: replicate 53 | # env: 54 | # secret: 55 | # - ACCESS_KEY_ID 56 | # - SECRET_ACCESS_KEY 57 | --------------------------------------------------------------------------------