diff --git a/apps/app/mix.exs b/apps/app/mix.exs index 6ce144b5..3c8b99b8 100644 --- a/apps/app/mix.exs +++ b/apps/app/mix.exs @@ -44,6 +44,7 @@ defmodule App.MixProject do {:core, in_umbrella: true}, {:ecto_sql, "~> 3.4"}, {:excoveralls, "~> 0.10", only: [:dev, :test]}, + {:oban, "~> 2.1"}, {:phoenix, "~> 1.5.8"}, {:phoenix_ecto, "~> 4.0"}, {:phoenix_html, "~> 2.11"}, diff --git a/apps/core/guides/features/admin.md b/apps/core/guides/features/admin.md index aad94154..38a7ec7d 100644 --- a/apps/core/guides/features/admin.md +++ b/apps/core/guides/features/admin.md @@ -1 +1,11 @@ # Admin + +The admin interface is generated through [Kaffy](https://aesmail.github.io/kaffy/). +You can find extensive documentation on the Kaffy site regarding how to use and +customize your admin interface. + +Legendary specific notes: + +- The configuration for Kaffy in your Legendary app is located in config/admin.exs. +- Many of the built-in schemas provide admin modules. You shouldn't generally +need to change these, but you may want to do so if you are changing built-in schema modules. diff --git a/apps/core/guides/features/auth.md b/apps/core/guides/features/auth.md index e97b0679..7c530cbf 100644 --- a/apps/core/guides/features/auth.md +++ b/apps/core/guides/features/auth.md @@ -1 +1,71 @@ # Authentication and Authorization + +Legendary provides a set of authentication and authorization features out of the +box. + +# Authentication + +Legendary comes with authentication powered by [Pow](https://powauth.com/) out +of the box. The default configuration: + +- supports sign in and registration with an email and password +- allows password resets +- requires users to confirm their email address before logging in +- emails for email confirmation and password reset will be nicely styled using your app's +email styles + +> Tip: in development mode, emails your app sends will be visible at http://localhost:4000/sent_emails. + +Your Pow configuration can be customized in config/config.exs. + +By default, users can be administrated in the admin interface. + +# Roles and Authorization + +Users have an array of roles. By default, a user has no roles, but they can have +as many as you need. Roles in Legendary are arbitrary strings that you tag a user +with to give them certain privileges. + +For example, here's a typical admin user created by the `mix legendary.create_admin` command: + +```elixir +%Legendary.Auth.User{ + email: "legendary@example.com", + homepage_url: nil, + id: 1, + inserted_at: ~N[2021-02-25 22:14:40], + # This user has one role-- admin! + roles: ["admin"], + updated_at: ~N[2021-02-25 22:14:40] +} +``` + +`admin` happens to be a role that the framework cares about-- via the `mix legendary.create_admin` command and the `:require_admin` pipeline that protects +the admin interface. However, you can use any string you want as a role and check +for it in your code. For example, your app might give some users a `paid_customer` +role and use it to protect certain features. You don't have to declare that in advance with the framework. + +In some cases, you may want "resourceful roles"-- a role that corresponds to a +specific resource record in your app. We suggest the following convention for those +role names: `:role_name/:resource_type/:id`. So that could be `owner/home/3` to +indicate the user is the owner of the Home with the id of 3. An authorized guest +to the same home might be `guest/home/3`. + +You can check whether a user has a role by calling Legendary.Auth.Roles.has_role?/2: + +```elixir +Legendary.Auth.Roles.has_role?(user, "admin") +``` + +And you can always access the `user.roles` field directly. + +# Protected routes + +## Signed-In Only Routes + +You can require that a given route requires a user by piping through the `:require_auth` pipeline. See apps/app/lib/app_web/router.ex for examples. + +## Admin Only Routes + +You can lock down a route to the app to only admin users by using the `:require_admin` pipeline. For example, the /admin area of your app is protected +that way. See apps/app/lib/app_web/router.ex for examples. diff --git a/apps/core/guides/features/background-jobs.md b/apps/core/guides/features/background-jobs.md index e6291265..44ae2d97 100644 --- a/apps/core/guides/features/background-jobs.md +++ b/apps/core/guides/features/background-jobs.md @@ -1 +1,13 @@ # Background Jobs + +Background jobs and periodic jobs in Legendary are powered by [Oban](https://github.com/sorentwo/oban). See the Oban documentation for extensive information +on using Oban in your application, including: + +- queue configuration +- worker configuration +- unique job constraints +- periodic jobs + +The framework itself uses Oban for recurring tasks such as generating sitemaps. + +Your app's Oban configuration is available in config/config.exs. diff --git a/apps/core/guides/features/content-management.md b/apps/core/guides/features/content-management.md index 1a420e8e..68289805 100644 --- a/apps/core/guides/features/content-management.md +++ b/apps/core/guides/features/content-management.md @@ -1 +1,70 @@ # Content Management + +Your app includes a basic content management system including a simple blog +(including optional user comments), dynamic pages, and static pages. + +Pages and blog posts can be managed from the admin interface. Posts and pages +support content in [Markdown](https://www.markdownguide.org/). + +# Blog Posts + +Your app has a blog at /blog. Your can create and manage posts from "Content > Pages and Posts" +in the /admin area. You can write your post body in Markdown. + +By default, posts have a few fields: + +- Type: for a blog post, this will be "Blog Post" +- Slug: this is the url path of your post. For example, a post with slug "hello-world" +would be available at /hello-world. +- Title: the human-readable title of your post. +- Content: this is the body of your post as Markdown. The admin provides a nice +editor in case you don't know Markdown syntax yet or don't want to bother. +- Status: this is Publish if you want your post to be visible to everyone, or draft +if you aren't ready to share it with the world. +- Author: this will normally be you, but we do allow admins to ghost-write for other +users. +- Excerpt: A short summary of your blog post that may show up in search engine results. +- Sticky: sticky posts will always show up first on your blog. They are generally used +for important announcements and community rules. +- Comment status: "open" will allow comments on your post. "closed" hides comments and +does not allow new comments to be entered. +- Ping status: whether the post supports (pingbacks)[https://en.wikipedia.org/wiki/Pingback]. +**Coming soon:** we don't currently show pingbacks anywhere or notify anyone when a +pingback is received, but we may in the future. +- Menu order: **Coming soon:** the relative order this blog post will show up in +dynamic menus. Menu management is currently in development. The lower this number, +the higher the post will appear in the menu. + +# Dynamic Pages + +Dynamic pages are very similar to blog posts. The only differences are that their +type is "Page" instead of "Blog Post" and they do not appear in your blog feed. + +They are intended for simple pages that will be updated by your admins, but don't +make sense as a blog post-- for example, terms of service or FAQ pages. + +The fields of dynamic pages are the same as blog posts. + +# Static Pages + +Legendary also supports static pages. Static pages are not editable from the admin. +However, they provide an easy way for developers on a Legendary app to create +and serve a content page without defining custom controllers and routes. This is +a good fit for pages that are more complex than what can be done with Markdown +in dynamic pages. + +Static pages are eex templates located in apps/content/lib/content_web/templates/posts/static_pages/. +For example, the home page of your app is a static page called index.html.eex. +The filename, less the .html.eex part, serves as the slug. In other words, a +static page called pricing.html.eex would have the url path /pricing in your app. + +> Note: if a static page and a dynamic page have the same slug, the dynamic page +> will "win." This allows you to provide a default version of the page as a fallback +> in code, while allowing admins to create an updated version of the same page. + +# Comments + +As mentioned above, blog posts can optionally have comments enabled. On these posts, +there will be a feed of comments as well as a comment form at the bottom of the page. + +Comments can be managed by admins in the admin interface under "Content > Comments." diff --git a/apps/core/guides/features/devops-templates.md b/apps/core/guides/features/devops-templates.md index 3e40f583..dea54d0a 100644 --- a/apps/core/guides/features/devops-templates.md +++ b/apps/core/guides/features/devops-templates.md @@ -1 +1,55 @@ # DevOps Templates + +Legendary includes a full set of DevOps templates designed to make it easy to +test, build, and deploy your app. + +# Overview + +The setup we provide is an opinionated setup based on years of experience building +Phoenix applications and deploying them at scale. It's meant to be efficient and +easy for small teams while scaling to big teams. It's meant to be lean enough +for low-traffic apps while scaling quite well to apps receiving thousands of +requests a second. + +Here's the process overview: + +1. You make commits using [conventional commit messages](https://www.conventionalcommits.org/). +2. The CI runs tests and builds a Docker image unique to your latest commit. +3. Should your tests pass, that Docker image is labeled with a [semantic version](https://semver.org/) +driven by your commit messages. We also tag that commit with that version so that +you can refer to it. You never need to manually tag Docker image or a commit, so +long as you follow the commit message convention. +4. You can deploy that Docker image to Kubernetes, or any other Docker-friendly hosting environment. + - CI generates a Kubernetes manifest pointing at that new docker image. You can + apply that manifest to your cluster manually, or use a tool like flux2 to + automate that. + - If you don't use Kubernetes, you can tell your Docker-ized host to pull the new image in the method provided by that host. + +# CI Configuration + +Legendary comes with GitLab CI settings which should work for you with minimal +setup. This config is located in .gitlab-ci.yml. + +The CI script will automatically tag successful builds. To do this, you will +need to configure a [CI variable](https://docs.gitlab.com/ee/ci/variables/) named +`GITLAB_TOKEN`. This token should be a +[personal access token](https://gitlab.com/-/profile/personal_access_tokens) with +`read_repository, write_repository` permissions. + +This CI configuration provides a few nice features: + +- Parallel build steps. The tests run while the Docker image builds, so you don't +have to wait for one then the other. +- Fast Docker build configuration. We use Docker BuildKit and a heavily tuned Dockerfile to reduce builld times from 15+ minutes to ~3 minutes. +- Fast Elixir compile times. Out of the box, Elixir compilation can be quite +slow in CI. We employ a few tricks to reduce the compilation time by over 75% +over default CI configuration. +- Automated semantic versioning. So long as you use conventional commit messages, +we will automatically bump the version number appropriately. + +# Kubernetes Manifests + +We also automatically generate a Kubernetes manifest for your app on each successful build. The generate manifest is commited back to your repo at infrastructure/. You can use a tool like flux2 to automatically update the configuration in your Kubernetes cluster from there. Or you could manually apply +it whenever you choose. + +The template used to generate the manifest is located in infrastructure_templates. Feel free to customize it if your application needs different Kubernetes config. diff --git a/apps/core/guides/features/email.md b/apps/core/guides/features/email.md new file mode 100644 index 00000000..27d2a8bc --- /dev/null +++ b/apps/core/guides/features/email.md @@ -0,0 +1,80 @@ +# Email + +# Fluid Email Templates + +We provide an email template based on +[Cerberus's Fluid Template](https://tedgoas.github.io/Cerberus/#fluid). This +is a template well-suited for transactional email that has been well-tested on +a wide variety of email clients. It should let you send nice looking email from +your app without having to think about it a lot. + +# Branding / Theming + +Of course, you might want to customize the style of your emails to match your app's +unique look or brand. The trick is that for emails to really work across a broad +set of common clients, they need to _inline their CSS_. We take care of this for +you. + +You can customize the variables (colors, sizes, etc) in config/email_styles.exs +and we'll apply them to your emails. + +# Mailer + +Of course, you may want to send your own emails. We provide two modules to help: + +- Legendary.CoreEmail: responsible for generating emails to your specifications +- Legendary.CoreMailer: responsible for sending emails per your configuration + +Both are powered by [Bamboo](https://github.com/thoughtbot/bamboo) so you +can follow the Bamboo documentation to learn more about customizing and using +email in your app. + +Here's an example: + +```elixir +defmodule App.HelloEmail do + import Bamboo.Email + use Bamboo.Phoenix, view: AppWeb.EmailView + + def send_hello_email(to) do + to_address + |> hello_email() + |> Legendary.CoreMailer.deliver_later() + end + + def hello_email(to_address) do + Legendary.CoreEmail.base_email() + |> to(to_address) + |> render(:hello, to_address: to_address) + end +end +``` + +> Tip: in development mode, any email you send can be viewed at localhost:4000/sent_emails. + +# Email Helpers + +Fluid email templates don't do any good if the content of your HTML emails isn't also as fluid and well-tested. We provide email tag helpers so that you don't +have to hand-craft email-friendly HTML. See `Legendary.CoreWeb.EmailHelpers`. + +For example, your hello.html.eex might look something like this: + +```eex +<%= preview do %> + Have you heard of our awesome app? +<% end %> + +<%= h1 do %> + Hello, <%= to_address %> +<% end %> +<%= p do %> + We hope you'll join us. +<% end %> + +<%= styled_button href: "http://example.com/" do %> + Join us! +<% end %> +``` + +We'll handle generating all of the nested tags and inline CSS needed to make the +email look good. diff --git a/apps/core/guides/features/fluid-email-templates.md b/apps/core/guides/features/fluid-email-templates.md deleted file mode 100644 index 59585663..00000000 --- a/apps/core/guides/features/fluid-email-templates.md +++ /dev/null @@ -1 +0,0 @@ -# Fluid Email Templates diff --git a/apps/core/guides/features/i18n.md b/apps/core/guides/features/i18n.md index 05b5b619..dbdb29b1 100644 --- a/apps/core/guides/features/i18n.md +++ b/apps/core/guides/features/i18n.md @@ -1 +1,24 @@ -# I18n +# Strings File and I18n + +It's a good idea to extract any human-readable strings in your application out +into a configuration file. The reason is two-fold: + +1. It makes it easier for developers to update "[copy](https://en.wikipedia.org/wiki/Copy_(written))" in the application and even +allows non-developers on a team to make copy changes. +2. When your application supports multiple languages, it is easy for translators +to provide translations for all of your copy at once. + +In Legendary, we provide a set of tools for doing this. + +- (English) strings are stored in config/i18n/en.yml. +- You can call `Legendary.I18n.t!/2` to get a string by its key. For example: `Legendary.I18n.t! "en", "site.title"` retrieves the english version of the +string labeled "title" under the section "site" on en.yml. + +> Tip: if you use t! a lot (good job!), you can import it in your view module +> to save some typing like `import Legendary.I18n, only: [t!: 2]` and then use it like `<%= t! "en", "site.title" %>` in your templates. + +Note that the first argument is a two-letter language code. In order to support +other languages, you can provide more yml files in config/i18n (example, config/i18n/fr.yml for French) and call t!/2 with that language code instead. + +**On the roadmap:** in the future, we intend to provide a mechanism for detecting +and managing each visitor's language and providing those strings if available. diff --git a/apps/core/guides/features/tasks-and-scripts.md b/apps/core/guides/features/tasks-and-scripts.md index adde2d85..ba1c4875 100644 --- a/apps/core/guides/features/tasks-and-scripts.md +++ b/apps/core/guides/features/tasks-and-scripts.md @@ -1 +1,22 @@ # Tasks and Scripts + +Legendary follows the [scripts to rule them all pattern](https://github.com/github/scripts-to-rule-them-all). This allows any developers familiar with the pattern, +either from other Legendary projects, or from other projects that use the pattern, +to immediately pick up a project and get it running. + +Here's a summary of the scripts you'll mostly use: + +1. **bootstrap** installs all the dependencies needed to run the project. +2. **update** is used to update dependencies. +3. **server** runs the server. +4. **console** runs the interactive console. +5. **test** runs the test suite. + +When you run server, console, or test, the script will make sure all the right +dependencies are in place (by running bootstrap or update). This means you can +go straight to running script/server and it should just work. + +We encourage you to customize these scripts to the needs of your project as it grows. A developer should only _ever_ have to run script/server to run the server, +and should not need to remember anything beyond that. script/bootstrap should always install everything you need to set up the project from scratch. If you +find yourself updating setup steps in your project's README.md, consider how you +might automate away that setup in your scripts. diff --git a/apps/core/guides/overview.md b/apps/core/guides/overview.md index 0ff8f53b..30f64e32 100644 --- a/apps/core/guides/overview.md +++ b/apps/core/guides/overview.md @@ -66,3 +66,11 @@ need to configure a [CI variable](https://docs.gitlab.com/ee/ci/variables/) name `GITLAB_TOKEN`. This token should be a [personal access token](https://gitlab.com/-/profile/personal_access_tokens) with `read_repository, write_repository` permissions. + +## DevOps + +The preconfigured CI pipeline generates semantically versioned docker images that +you can deploy in your choice of dockerized hosting. We also provide a manifest +for Kubernetes that is automatically updated with each version (see infrastructure/ +for the generated result and infrastructure_templates/ for the templates used to +generate the manifest). diff --git a/apps/core/mix.exs b/apps/core/mix.exs index 4fb14ee3..82878f24 100644 --- a/apps/core/mix.exs +++ b/apps/core/mix.exs @@ -57,7 +57,7 @@ defmodule Legendary.Core.MixProject do "guides/features/background-jobs.md", "guides/features/content-management.md", "guides/features/devops-templates.md", - "guides/features/fluid-email-templates.md", + "guides/features/email.md", "guides/features/i18n.md", "guides/features/tasks-and-scripts.md", ] diff --git a/config/config.exs b/config/config.exs index 669b11f0..b39ed293 100644 --- a/config/config.exs +++ b/config/config.exs @@ -66,6 +66,13 @@ config :content, {"0 * * * *", Legendary.Content.Sitemaps}, ] +config :app, + Oban, + repo: App.Repo, + queues: [default: 10], + crontab: [ + ] + import_config "email_styles.exs" import_config "admin.exs"