├── .github ├── FUNDING.yml └── workflows │ └── maven.yml ├── .gitignore ├── LICENSE ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── changenode │ │ └── frisson │ │ ├── SupabaseDemoApplication.java │ │ ├── controller │ │ ├── Identity.java │ │ ├── Index.java │ │ └── Secure.java │ │ ├── data │ │ ├── ToDo.java │ │ └── User.java │ │ ├── query │ │ ├── TodosEntityQuery.java │ │ └── UserQuery.java │ │ └── websecurity │ │ ├── CurrentSupabaseUser.java │ │ ├── JwtAuthorizeFilter.java │ │ ├── PrincipalSupabaseConverter.java │ │ ├── SupabaseAuthService.java │ │ ├── SupabaseUser.java │ │ └── WebSecurityConfiguration.java ├── resources │ ├── META-INF │ │ └── additional-spring-configuration-metadata.json │ ├── application-h2.yaml │ ├── application.yaml │ ├── favicon.ico │ ├── favicon.png │ ├── logback.xml │ └── public │ │ ├── _pginfo │ │ └── fonts.json │ │ ├── assets │ │ ├── identity │ │ │ └── identity.js │ │ ├── index.css │ │ ├── js │ │ │ └── popper.min.js │ │ └── svg │ │ │ ├── loader.svg │ │ │ ├── logo-email.svg │ │ │ ├── logo-github.svg │ │ │ ├── logo-twitter.svg │ │ │ └── person-outline.svg │ │ ├── blocks.css │ │ ├── bootstrap │ │ ├── css │ │ │ ├── bootstrap.css │ │ │ └── bootstrap.min.css │ │ └── js │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.js.map │ │ │ ├── bootstrap.min.js │ │ │ └── bootstrap.min.js.map │ │ ├── favicon.ico │ │ ├── favicon.png │ │ ├── identity │ │ ├── create-account.html │ │ ├── forgot-password.html │ │ ├── sign-in.html │ │ ├── sign-out.html │ │ └── update-password.html │ │ ├── index.html │ │ ├── layout.html │ │ ├── pinegrow.json │ │ ├── secure.html │ │ └── style.css └── site │ ├── create-account.png │ ├── forgot-password.png │ ├── home-screen.png │ ├── intellij-web-module-setup.png │ ├── logged-in-user.png │ ├── pinegrow-create-account.png │ ├── pinegrow-master-layout.png │ ├── pinegrow-mobile-bootstrap-blocks.png │ ├── sign-in.png │ └── site.xml ├── site └── site.xml └── test ├── java └── com │ └── changenode │ └── frisson │ ├── DataAccessTests.java │ ├── JwtDecoderTest.java │ ├── SupabaseDemoApplicationTests.java │ └── TestSecurityConfiguration.java └── resources └── test-logback.xml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: wiverson 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '17' 23 | distribution: 'adopt' 24 | cache: maven 25 | - name: Restore Maven cache 26 | uses: skjolber/maven-cache-github-action@v1 27 | with: 28 | step: restore 29 | - name: Build with Maven 30 | env: # Pass GitHub Secrets to the job 31 | SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} 32 | SUPABASE_DATABASE_PASSWORD: ${{ secrets.SUPABASE_DATABASE_PASSWORD }} 33 | SUPABASE_DATABASE_URL: ${{ secrets.SUPABASE_DATABASE_URL }} 34 | SUPABASE_DATABASE_USER: ${{ secrets.SUPABASE_DATABASE_USER }} 35 | SUPABASE_JWT_SIGNER: ${{ secrets.SUPABASE_JWT_SIGNER }} 36 | SUPABASE_URL: ${{ secrets.SUPABASE_URL }} 37 | run: mvn -B verify --file pom.xml 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | **/.DS_Store 35 | **/_pgbackup/** 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ChangeNode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy, Powerful Full Stack Spring Boot 2 | 3 | Build slick, fast Spring Boot full stack web applications easily as a solo developer. 4 | 5 | This project is specifically designed to make it easy to build a modern web application with Spring Boot. 6 | 7 | Here's a list of features: 8 | 9 | - Uses [Supabase.io](https://supabase.io) as the core for RDBMS, auth, storage. 10 | - [Supabase is an open source platform](https://github.com/supabase/supabase) that offers 11 | hosted [Postgres](https://www.postgresql.org/) with built-in integration with a variety of services, including 12 | [simplified authentication](https://supabase.io/docs/guides/auth) 13 | and [storage](https://supabase.io/docs/guides/storage). 14 | - This project specifically uses Supabase for auth (including seamless Spring Security support!) and as a hosted 15 | Postgres instance. 16 | - There's a lot more on Supabase later in this document. :) 17 | - Because Supabase is just Postgres, that means you can use all of [IntelliJ's](https://www.jetbrains.com/idea/) RDBMS 18 | features, including [JPA Buddy](https://www.jpa-buddy.com/). 19 | - This means that everything is type-aware (auto-complete FTW!) from the RDBMS through the Java code and into the 20 | Thymeleaf templates! 21 | - Reconfigured the [Thymeleaf](https://www.thymeleaf.org/) settings for compatibility 22 | with [Pinegrow](https://pinegrow.com/) visual HTML builder 23 | - Uses [Bootstrap](https://getbootstrap.com/) as a default CSS framework 24 | - Drop in a [new Bootstrap theme](https://github.com/thomaspark/bootswatch) with minimal fuss! 25 | - If you want to switch to TailwindCSS instead, no big deal. 26 | - Use [HTMX](https://htmx.org/) and Thymeleaf Fragments to provide 27 | [rich, dynamic partial page updates](https://changenode.com/articles/easy-full-stack-java) 28 | without using any complicated JavaScript frameworks. 29 | - Stateless by default - uses [Supabase JWT](https://supabase.io/docs/learn/auth-deep-dive/auth-deep-dive-jwts) for 30 | authorization, so the project defaults to turning off Java sessions to improve ease of scaling. 31 | - TIP: Use service level [Spring Boot caching](https://spring.io/guides/gs/caching/) instead of the antiquated 32 | session API to take the load of a database instead the session API. 33 | 34 | # Screenshots 35 | 36 | The default theme is the open source Bootstrap 37 | theme [Darkly](https://github.com/thomaspark/bootswatch/tree/v5/dist/darkly), which just happens to be very similar to 38 | the default Supabase theme. You can swap in another theme (e.g. another [Bootswatch](https://bootswatch.com/) theme) or 39 | build your own via SASS or the Pinegrow Design editor. 40 | 41 | ## Basic Features 42 | 43 | ![Home Screen](src/main/site/home-screen.png) 44 | 45 | The default home screen displayed to the user. 46 | 47 | ![Sign In](src/main/site/sign-in.png) 48 | 49 | Log in with either a social provider or email/password. Supabase supports 50 | many [other providers](https://supabase.io/auth)! 51 | 52 | ![Create Account](src/main/site/create-account.png) 53 | 54 | Create an account quickly and easily. Supabase sends the various emails (confirmation, forgot password) for you. If you 55 | want to use your own SMTP server, pop in the SMTP credentials into Supabase. 56 | 57 | ![Forgot Password](src/main/site/forgot-password.png) 58 | 59 | This project includes the JavaScript to handle the forgot password flow. 60 | 61 | ![Logged In User](src/main/site/logged-in-user.png) 62 | 63 | Logged in Supabase information is available both via standard Spring Security (via principal) and via a bean wrapper ( 64 | makes it easier to work with in Thymeleaf). 65 | 66 | ## Thymeleaf Visual Editing 67 | 68 | ![Create Account](src/main/site/pinegrow-create-account.png) 69 | 70 | Shows how the Create Account page can be viewed visually. Use the Pinegrow built-in HTML code editor or tab back and 71 | forth with IntelliJ. 72 | 73 | ![Master Layout](src/main/site/pinegrow-master-layout.png) 74 | 75 | This is the master layout used throughout the template. Also can be edited visual and/or via code editor. 76 | 77 | ![Mobile and Bootstrap Blocks](src/main/site/pinegrow-mobile-bootstrap-blocks.png) 78 | 79 | Pinegrow handles responsive design quickly and easily. Just press a single keystroke to view the different breakpoints. 80 | Visually assign styling based on breakpoints. 81 | 82 | # Getting Started 83 | 84 | ## Basics 85 | 86 | You'll need Java 16+ and Maven. 87 | 88 | ## Supabase 89 | 90 | You will need to set up a new [Supabase.io](https://supabase.io/) project. You can start with the free starter version. 91 | Eventually you can switch to either a [paid account](https://supabase.io/pricing) or set up your 92 | own [self-hosted version](https://supabase.io/docs/guides/self-hosting). 93 | 94 | ## Configuration 95 | 96 | Set the following environment values so Maven and Spring Boot can find them. Tip: if you declare them in a .profile on 97 | macOS, IntelliJ will pick them up. 98 | 99 | | VALUE | Typical Values | 100 | | ----- | --- | 101 | | `SUPABASE_DATABASE_URL` | jdbc:postgresql://db.PROJECT.supabase.co/postgres | 102 | | `SUPABASE_DATABASE_USER` | postgres | 103 | | `SUPABASE_DATABASE_PASSWORD` | Same as your Supabase login password. | 104 | | `SUPABASE_URL` | https://PROJECT.supabase.co | 105 | | `SUPABASE_ANON_KEY` | A JWT with the role of anon. Verify it at https://jwt.io/ | 106 | | `SUPABASE_JWT_SIGNER` | The TOP SECRET key used for signing JWT from Supabase. DO NOT SHARE THIS - anyone who has this can create new identity JWTs - basically, this is a super password that would allow anyone to impersonate anyone on the site! | 107 | 108 | ## Database 109 | 110 | By default, this template points to the Supabase table and expects to find a user table and a todo table. If you are 111 | playing around, you might want to try creating a matching table. Otherwise, just go ahead and delete 112 | the [todo entity and query files](https://spring.io/guides/gs/accessing-data-jpa/): 113 | 114 | `src/main/java/com/changenode/frisson/data/ToDo.java` 115 | `src/main/java/com/changenode/frisson/query/TodosEntityQuery.java` 116 | 117 | ## IntelliJ Setup 118 | 119 | This project uses the src/main/resources/public directory to store the html files. 120 | 121 | ![IntelliJ Web Module Setup](src/main/site/intellij-web-module-setup.png) 122 | 123 | In IntelliJ, make sure you set up the Web module to point at src/main/resources/public directory. Otherwise you will get 124 | errors related to paths in IntelliJ. 125 | 126 | # Help 127 | 128 | This entire project is built on top of a large number of well-documented open source projects, such as Spring Boot, 129 | Bootstrap, Postgres, Thymeleaf, Supabase, and HTMX, just to name a few. Most of your issues or questions are probably 130 | going to be solved by the usual combination of the project documentation, Google, Stack Overflow, etc. 131 | 132 | That said, here are some options specific to this project: 133 | 134 | - Check out the [discussion board](https://github.com/ChangeNode/frisson/discussions). 135 | - File an [issue](https://github.com/ChangeNode/frisson/issues). 136 | 137 | If you need consulting support, [feel free to reach out](https://changenode.com/contact). 138 | 139 | # Additional Supabase Information 140 | 141 | ## Supabase PostREST and Spring Boot 142 | 143 | By default, Supabase makes data available to the browser using [PostREST](https://postgrest.org/) - an application that 144 | automatically generates REST endpoints for a relational database. 145 | 146 | Java web frameworks, on the other hand, usually connect directly to the database. 147 | 148 | Which is better? If you are a Java developer used to working with Spring Boot and Spring data repositories, just use 149 | that. Keep your tables private in Supabase connect just like you would with any ordinary Postgres instance. 150 | 151 | If you want to use PostREST, that's fine - just be absolutely sure you are setting up row-level security correctly! 152 | 153 | # Remember Me 154 | 155 | The "Remember Me" setting in the login user interface, if checked, will store the JWT in a cookie, which will then allow 156 | the server to immediately render the logged-in user as long as the JWT has not expired. 157 | 158 | The default for JWT tokens on Supabase is 3600 seconds (1 hour). As long as the user is actively clicking around on the 159 | website, the Supabase.js client will automatically refresh with new JWT tokens. 160 | 161 | This means, however, that the "Remember Me" feature will only work for up to 1 hour. If you want to extend this, go to 162 | the Authentication -> Settings -> JWT Expiry and change it to something longer. The maximum setting allowed (one week) 163 | means that as long as the user logs into the site at least once a week, they will effectively never have to log in 164 | again. 165 | 166 | The only downside is that you can't easily revoke a JWT once issued. So, if you extend the JWT session, that's the login 167 | time. If you want to implement some kind of instant user ban, you can still use JWT but you will need to add additional 168 | logic. 169 | 170 | # More Information 171 | 172 | ### Bootstrap Themes 173 | 174 | This project uses the Bootstrap 5.1 theme from: 175 | 176 | https://github.com/thomaspark/bootswatch/tree/v5/dist/darkly 177 | 178 | You can drop in other themes from: 179 | 180 | https://github.com/thomaspark/bootswatch/ 181 | 182 | Or you can use the built-in Pinegrow SASS compiler to build your own Bootstrap themes. 183 | 184 | ### SVG Icons 185 | 186 | The SVG Icons in this project are from: 187 | 188 | https://ionic.io/ionicons 189 | 190 | ### Reference Documentation 191 | 192 | For further reference, please consider the following sections: 193 | 194 | * [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) 195 | * [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.5.3/maven-plugin/reference/html/) 196 | * [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.5.3/maven-plugin/reference/html/#build-image) 197 | * [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/2.5.3/reference/htmlsingle/#using-boot-devtools) 198 | * [Spring Web](https://docs.spring.io/spring-boot/docs/2.5.3/reference/htmlsingle/#boot-features-developing-web-applications) 199 | * [Thymeleaf](https://docs.spring.io/spring-boot/docs/2.5.3/reference/htmlsingle/#boot-features-spring-mvc-template-engines) 200 | * [Spring Data JPA](https://docs.spring.io/spring-boot/docs/2.5.3/reference/htmlsingle/#boot-features-jpa-and-spring-data) 201 | 202 | ### Guides 203 | 204 | The following guides illustrate how to use some features concretely: 205 | 206 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) 207 | * [Handling Form Submission](https://spring.io/guides/gs/handling-form-submission/) 208 | * [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) 209 | 210 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.4 9 | 10 | 11 | com.changenode 12 | spring-boot-supabase 13 | 0.0.1-SNAPSHOT 14 | ChangeNode Spring Boot Supabase 15 | Modern Java web application template 16 | 17 | UTF-8 18 | UTF-8 19 | 17 20 | 21 | ${env.SUPABASE_DATABASE_URL} 22 | 23 | ${env.SUPABASE_DATABASE_USER} 24 | 25 | ${env.SUPABASE_DATABASE_PASSWORD} 26 | 27 | ${env.SUPABASE_URL} 28 | 29 | ${env.SUPABASE_ANON_KEY} 30 | 31 | http://localhost:8080/ 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-data-jpa 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-thymeleaf 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-web 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-configuration-processor 50 | true 51 | 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-security 57 | 58 | 59 | 60 | org.thymeleaf.extras 61 | thymeleaf-extras-springsecurity5 62 | 63 | 64 | org.springframework.security 65 | spring-security-test 66 | test 67 | 68 | 69 | 70 | 71 | org.postgresql 72 | postgresql 73 | 42.5.0 74 | 75 | 76 | 77 | 78 | 79 | nz.net.ultraq.thymeleaf 80 | thymeleaf-layout-dialect 81 | 3.1.0 82 | 83 | 84 | 85 | org.springframework.boot 86 | spring-boot-devtools 87 | true 88 | 89 | 90 | 91 | org.springframework.boot 92 | spring-boot-starter-test 93 | test 94 | 95 | 96 | 97 | org.junit.vintage 98 | junit-vintage-engine 99 | 100 | 101 | 102 | 103 | 104 | org.assertj 105 | assertj-core 106 | 3.23.1 107 | test 108 | 109 | 110 | 111 | 112 | io.jsonwebtoken 113 | jjwt-api 114 | 0.11.5 115 | 116 | 117 | io.jsonwebtoken 118 | jjwt-impl 119 | 0.11.5 120 | runtime 121 | 122 | 123 | io.jsonwebtoken 124 | jjwt-jackson 125 | 0.11.5 126 | runtime 127 | 128 | 129 | 130 | 131 | org.webjars 132 | bootstrap 133 | 5.2.2 134 | 135 | 136 | org.webjars.npm 137 | bootstrap-icons 138 | 1.9.1 139 | 140 | 141 | org.webjars 142 | webjars-locator-core 143 | 0.52 144 | 145 | 146 | org.webjars.npm 147 | htmx.org 148 | 1.8.0 149 | 150 | 151 | org.webjars.npm 152 | hyperscript.org 153 | 0.9.7 154 | 155 | 156 | 157 | 158 | ${project.artifactId} 159 | 160 | 161 | 162 | org.apache.maven.plugins 163 | maven-site-plugin 164 | 3.10.0 165 | 166 | 167 | org.apache.maven.plugins 168 | maven-surefire-plugin 169 | 3.0.0-M5 170 | 171 | 172 | org.apache.maven.plugins 173 | maven-jar-plugin 174 | 3.2.0 175 | 176 | 177 | org.apache.maven.plugins 178 | maven-antrun-plugin 179 | 1.8 180 | 181 | 182 | org.apache.maven.plugins 183 | maven-assembly-plugin 184 | 3.3.0 185 | 186 | 187 | org.apache.maven.plugins 188 | maven-release-plugin 189 | 3.0.0-M4 190 | 191 | 192 | org.apache.maven.plugins 193 | maven-project-info-reports-plugin 194 | 3.1.2 195 | 196 | 197 | org.apache.maven.plugins 198 | maven-dependency-plugin 199 | 3.2.0 200 | 201 | 202 | org.apache.maven.plugins 203 | maven-clean-plugin 204 | 3.1.0 205 | 206 | 207 | org.apache.maven.plugins 208 | maven-resources-plugin 209 | 3.2.0 210 | 211 | 212 | org.apache.maven.plugins 213 | maven-deploy-plugin 214 | 3.0.0-M2 215 | 216 | 217 | org.apache.maven.plugins 218 | maven-install-plugin 219 | 3.0.0-M1 220 | 221 | 222 | org.apache.maven.plugins 223 | maven-failsafe-plugin 224 | 3.0.0-M5 225 | 226 | 227 | org.apache.maven.plugins 228 | maven-shade-plugin 229 | 3.2.4 230 | 231 | 232 | org.codehaus.mojo 233 | versions-maven-plugin 234 | 2.10.0 235 | 236 | 237 | 238 | 239 | 240 | org.springframework.boot 241 | spring-boot-maven-plugin 242 | 2.6.4 243 | 244 | 245 | org.apache.maven.plugins 246 | maven-resources-plugin 247 | 3.2.0 248 | 249 | ${project.build.sourceEncoding} 250 | 251 | 252 | 253 | org.apache.maven.plugins 254 | maven-compiler-plugin 255 | 3.8.1 256 | 257 | 258 | 259 | 260 | org.springframework.boot.configurationprocessor.ConfigurationMetadataAnnotationProcessor 261 | 262 | 263 | 17 264 | 265 | 266 | 267 | 268 | 269 | src/main/resources 270 | 271 | 272 | **/_pgbackup/** 273 | **/_pginfo/** 274 | **//pinegrow.json 275 | 276 | /public/assets/identity/** 277 | 278 | 279 | 280 | 281 | src/main/resources/public/assets/identity 282 | true 283 | ${project.build.directory}/classes/public/assets/identity 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | org.apache.maven.plugins 293 | maven-project-info-reports-plugin 294 | 3.1.2 295 | 296 | 297 | 298 | dependencies 299 | index 300 | summary 301 | 302 | 303 | 304 | 305 | 306 | 307 | org.apache.maven.plugins 308 | maven-surefire-report-plugin 309 | 3.0.0-M5 310 | 311 | 312 | 313 | report-only 314 | 315 | 316 | 317 | 318 | false 319 | 320 | 321 | 322 | 323 | 324 | 325 | org.codehaus.mojo 326 | versions-maven-plugin 327 | 2.9.0 328 | 329 | 330 | 331 | dependency-updates-report 332 | plugin-updates-report 333 | property-updates-report 334 | 335 | 336 | 337 | 338 | false 339 | 340 | 341 | 342 | 343 | 344 | 345 | postgres 346 | 347 | false 348 | 349 | 350 | 351 | 352 | org.apache.maven.plugins 353 | maven-surefire-plugin 354 | 355 | 356 | postgres 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | spring-boot-debug 367 | 368 | false 369 | 370 | 371 | 372 | 373 | org.springframework.boot 374 | spring-boot-maven-plugin 375 | 376 | 377 | 378 | org.springframework.boot 379 | spring-boot-configuration-processor 380 | 381 | 382 | 383 | -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/SupabaseDemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson; 2 | 3 | import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan; 7 | import org.springframework.cache.annotation.EnableCaching; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.scheduling.annotation.EnableAsync; 10 | 11 | @SpringBootApplication 12 | @EnableCaching 13 | @EnableAsync 14 | @ConfigurationPropertiesScan 15 | public class SupabaseDemoApplication { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(SupabaseDemoApplication.class, args); 19 | } 20 | 21 | @Bean 22 | public LayoutDialect layoutDialect() { 23 | return new LayoutDialect(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/controller/Identity.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.ui.Model; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | 9 | import javax.servlet.http.Cookie; 10 | import javax.servlet.http.HttpServletResponse; 11 | 12 | @Controller 13 | @RequestMapping("/public") 14 | public class Identity { 15 | 16 | @GetMapping("/sign-in") 17 | public String signIn(HttpServletResponse response, Model model, 18 | @RequestParam(value = "access_token", required = false) String accessToken, 19 | @RequestParam(value = "login", required = false) String login, 20 | @RequestParam(value = "error_description", required = false) String error) { 21 | if (error != null) 22 | model.addAttribute("error", error); 23 | 24 | if (accessToken != null || login != null) 25 | model.addAttribute("hasToken", true); 26 | else 27 | model.addAttribute("hasToken", false); 28 | if (accessToken != null) { 29 | Cookie accessTokenCookie = new Cookie("access-token", null); 30 | accessTokenCookie.setMaxAge(-1); 31 | accessTokenCookie.setPath("/"); 32 | response.addCookie(accessTokenCookie); 33 | } 34 | 35 | return "identity/sign-in"; 36 | } 37 | 38 | @GetMapping("/update-password") 39 | public String updatePassword( 40 | @RequestParam(name = "error_description", required = false) String errorDescription, 41 | Model model) { 42 | 43 | if (errorDescription != null) 44 | model.addAttribute("error", errorDescription); 45 | 46 | return "identity/update-password"; 47 | } 48 | 49 | @GetMapping("/create-account") 50 | public String createAccount() { 51 | return "identity/create-account"; 52 | } 53 | 54 | @GetMapping("/forgot-password") 55 | public String forgotPassword() { 56 | return "identity/forgot-password"; 57 | } 58 | 59 | @GetMapping("/sign-out") 60 | public String logout(HttpServletResponse response) { 61 | Cookie accessToken = new Cookie("access-token", null); 62 | accessToken.setMaxAge(-1); 63 | accessToken.setPath("/"); 64 | response.addCookie(accessToken); 65 | return "identity/sign-out"; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/controller/Index.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | import javax.servlet.http.HttpServletRequest; 7 | 8 | @Controller 9 | public class Index { 10 | 11 | @GetMapping("/") 12 | public String overview(HttpServletRequest request) { 13 | return "index"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/controller/Secure.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.controller; 2 | 3 | import com.changenode.frisson.websecurity.SupabaseUser; 4 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | 9 | @Controller 10 | public class Secure { 11 | @GetMapping("/secure") 12 | public String secure(Model model, @AuthenticationPrincipal SupabaseUser user) { 13 | model.addAttribute("user", user); 14 | return "secure"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/data/ToDo.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.data; 2 | 3 | import javax.persistence.*; 4 | import java.time.Instant; 5 | import java.util.Objects; 6 | import java.util.UUID; 7 | 8 | @Entity 9 | @Table(name = "todos", schema = "public") 10 | public class ToDo { 11 | private Long id; 12 | private UUID userId; 13 | private String task; 14 | private Boolean isComplete; 15 | private Instant insertedAt; 16 | 17 | @Id() 18 | @Column(name = "id", nullable = false) 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | public Long getId() { 21 | return id; 22 | } 23 | 24 | public void setId(Long id) { 25 | this.id = id; 26 | } 27 | 28 | @Basic 29 | @Column(name = "user_id", nullable = false) 30 | public UUID getUserId() { 31 | return userId; 32 | } 33 | 34 | public void setUserId(UUID userId) { 35 | this.userId = userId; 36 | } 37 | 38 | @Basic 39 | @Column(name = "task") 40 | public String getTask() { 41 | return task; 42 | } 43 | 44 | public void setTask(String task) { 45 | this.task = task; 46 | } 47 | 48 | @Basic 49 | @Column(name = "is_complete") 50 | public Boolean getComplete() { 51 | return isComplete; 52 | } 53 | 54 | public void setComplete(Boolean complete) { 55 | isComplete = complete; 56 | } 57 | 58 | @Basic 59 | @Column(name = "inserted_at", nullable = false) 60 | public Instant getInsertedAt() { 61 | return insertedAt; 62 | } 63 | 64 | public void setInsertedAt(Instant insertedAt) { 65 | this.insertedAt = insertedAt; 66 | } 67 | 68 | @Override 69 | public boolean equals(Object o) { 70 | if (this == o) return true; 71 | if (o == null || getClass() != o.getClass()) return false; 72 | ToDo that = (ToDo) o; 73 | return id == that.id && Objects.equals(userId, that.userId) && Objects.equals(task, that.task) && Objects.equals(isComplete, that.isComplete) && Objects.equals(insertedAt, that.insertedAt); 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | return Objects.hash(id, userId, task, isComplete, insertedAt); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/data/User.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.data; 2 | 3 | import javax.persistence.*; 4 | import java.util.Objects; 5 | import java.util.UUID; 6 | 7 | @Entity 8 | @Table(name = "users", schema = "auth") 9 | public class User { 10 | private UUID id; 11 | private String role; 12 | 13 | @Id 14 | @Column(name = "id", nullable = false) 15 | @GeneratedValue 16 | public UUID getId() { 17 | return id; 18 | } 19 | 20 | public void setId(UUID id) { 21 | this.id = id; 22 | } 23 | 24 | @Override 25 | public boolean equals(Object o) { 26 | if (this == o) return true; 27 | if (o == null || getClass() != o.getClass()) return false; 28 | User users = (User) o; 29 | return Objects.equals(id, users.id) && Objects.equals(role, users.role); 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | return Objects.hash(id, role); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/query/TodosEntityQuery.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.query; 2 | 3 | import com.changenode.frisson.data.ToDo; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface TodosEntityQuery extends JpaRepository { 7 | } -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/query/UserQuery.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.query; 2 | 3 | import com.changenode.frisson.data.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface UserQuery extends JpaRepository { 7 | } -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/websecurity/CurrentSupabaseUser.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.websecurity; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.security.core.Authentication; 5 | import org.springframework.security.core.context.SecurityContextHolder; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class CurrentSupabaseUser { 10 | 11 | @Bean(name = "supabaseUser") 12 | public SupabaseUserLookup user() { 13 | return new SupabaseUserLookupImpl(); 14 | } 15 | 16 | interface SupabaseUserLookup { 17 | SupabaseUser user(); 18 | 19 | String getAvatarUrl(); 20 | 21 | String getProvider(); 22 | 23 | String getUsername(); 24 | } 25 | 26 | public class SupabaseUserLookupImpl implements SupabaseUserLookup { 27 | 28 | @Override 29 | public SupabaseUser user() { 30 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 31 | if (authentication == null) return null; 32 | if (authentication.getPrincipal() == null) return null; 33 | if (authentication.getPrincipal() instanceof SupabaseUser) 34 | return (SupabaseUser) authentication.getPrincipal(); 35 | return null; 36 | } 37 | 38 | @Override 39 | public String getAvatarUrl() { 40 | return user().getAvatarUrl(); 41 | } 42 | 43 | @Override 44 | public String getProvider() { 45 | return user().getProvider(); 46 | } 47 | 48 | @Override 49 | public String getUsername() { 50 | return user().getUsername(); 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/websecurity/JwtAuthorizeFilter.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.websecurity; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.security.authentication.AuthenticationManager; 6 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; 9 | 10 | import javax.servlet.FilterChain; 11 | import javax.servlet.ServletException; 12 | import javax.servlet.http.Cookie; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.io.IOException; 16 | import java.util.ArrayList; 17 | 18 | public class JwtAuthorizeFilter extends BasicAuthenticationFilter { 19 | 20 | private final SupabaseAuthService supabaseAuthService; 21 | private final Logger logger = LoggerFactory.getLogger(JwtAuthorizeFilter.class); 22 | 23 | public JwtAuthorizeFilter(AuthenticationManager authManager, SupabaseAuthService supabaseAuthService) { 24 | super(authManager); 25 | this.supabaseAuthService = supabaseAuthService; 26 | } 27 | 28 | @Override 29 | protected void doFilterInternal(HttpServletRequest req, 30 | HttpServletResponse res, 31 | FilterChain chain) throws IOException, ServletException { 32 | if (req == null) 33 | return; 34 | 35 | UsernamePasswordAuthenticationToken authentication = getAuthentication(req); 36 | 37 | SecurityContextHolder.getContext().setAuthentication(authentication); 38 | chain.doFilter(req, res); 39 | } 40 | 41 | // Reads the JWT from the Authorization header, and then uses JWT to validate the token 42 | private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { 43 | if (request == null) 44 | return null; 45 | if (request.getCookies() == null) 46 | return null; 47 | for (Cookie cookie : request.getCookies()) 48 | if ("access-token".compareToIgnoreCase(cookie.getName()) == 0) { 49 | SupabaseUser user = supabaseAuthService.user(cookie.getValue()); 50 | if (user == null) 51 | return null; 52 | return new UsernamePasswordAuthenticationToken(user, user.getPassword(), new ArrayList<>()); 53 | } 54 | 55 | return null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/websecurity/PrincipalSupabaseConverter.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.websecurity; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.format.FormatterRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | import java.security.Principal; 9 | 10 | @Configuration 11 | public class PrincipalSupabaseConverter implements WebMvcConfigurer { 12 | @Override 13 | public void addFormatters(FormatterRegistry registry) { 14 | registry.addConverter(new PSC()); 15 | } 16 | 17 | public class PSC implements Converter { 18 | @Override 19 | public SupabaseUser convert(Principal from) { 20 | return (SupabaseUser) from; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/websecurity/SupabaseAuthService.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.websecurity; 2 | 3 | import io.jsonwebtoken.*; 4 | import io.jsonwebtoken.security.SignatureException; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.security.core.AuthenticationException; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.Base64; 10 | 11 | @Service 12 | public class SupabaseAuthService { 13 | 14 | @Value("${supabase.jwt_secret}") 15 | String jwtSecret; 16 | 17 | public SupabaseUser user(String accessToken) throws AuthenticationException { 18 | String encoded = Base64.getEncoder().encodeToString(jwtSecret.getBytes()); 19 | 20 | try { 21 | Jwt parse = Jwts.parserBuilder().setSigningKey(encoded). 22 | requireAudience("authenticated"). 23 | build().parseClaimsJws(accessToken); 24 | return new SupabaseUser(parse.getBody(), accessToken); 25 | } catch (ExpiredJwtException | SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException expiredJwtException) { 26 | return null; 27 | } 28 | 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/websecurity/SupabaseUser.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.websecurity; 2 | 3 | import io.jsonwebtoken.Claims; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | 8 | import java.security.Principal; 9 | import java.util.*; 10 | 11 | import static java.util.Collections.unmodifiableMap; 12 | 13 | public class SupabaseUser implements UserDetails, Principal { 14 | private final List grantedAuthority = new ArrayList<>(); 15 | private final Claims claims; 16 | private final String id; 17 | private final String role; 18 | private final String email; 19 | private final String phone; 20 | private final Map appMetadata; 21 | private final Map userMetadata; 22 | private final String fullName; 23 | private final String userName; 24 | private final String provider; 25 | private final String avatarUrl; 26 | private final String emailConfirmedAt; 27 | private final String confirmedAt; 28 | private final String lastSignInAt; 29 | private final String createdAt; 30 | private final String updatedAt; 31 | private final String password; 32 | 33 | @SuppressWarnings("unchecked") 34 | public SupabaseUser(Claims claims, String accessToken) { 35 | this.claims = claims; 36 | this.password = accessToken; 37 | 38 | id = claims.get("sub", String.class); 39 | role = claims.get("role", String.class); 40 | 41 | grantedAuthority.clear(); 42 | grantedAuthority.add(new SimpleGrantedAuthority(id)); 43 | grantedAuthority.add(new SimpleGrantedAuthority(role)); 44 | 45 | email = claims.get("email", String.class); 46 | phone = claims.get("phone", String.class); 47 | 48 | appMetadata = (Map) unmodifiableMap(claims.get("app_metadata", HashMap.class)); 49 | userMetadata = (Map) unmodifiableMap(claims.get("user_metadata", HashMap.class)); 50 | 51 | if (userMetadata.get("avatar_url") != null) 52 | avatarUrl = userMetadata.get("avatar_url"); 53 | else 54 | avatarUrl = "/assets/svg/person-outline.svg"; 55 | fullName = userMetadata.get("full_name"); 56 | if (userMetadata.get("user_name") != null) 57 | userName = userMetadata.get("user_name"); 58 | else 59 | userName = email; 60 | 61 | emailConfirmedAt = userMetadata.get("email_confirmed_at"); 62 | confirmedAt = userMetadata.get("confirmed_ad"); 63 | lastSignInAt = userMetadata.get("last_sign_in_at"); 64 | createdAt = userMetadata.get("created_at"); 65 | updatedAt = userMetadata.get("updated_ad"); 66 | 67 | provider = appMetadata.getOrDefault("provider", ""); 68 | } 69 | 70 | public List getGrantedAuthority() { 71 | return grantedAuthority; 72 | } 73 | 74 | public Claims getClaims() { 75 | return claims; 76 | } 77 | 78 | public String getFullName() { 79 | return fullName; 80 | } 81 | 82 | public String getProvider() { 83 | return provider; 84 | } 85 | 86 | public String getAvatarUrl() { 87 | return avatarUrl; 88 | } 89 | 90 | public String getId() { 91 | return id; 92 | } 93 | 94 | public String getRole() { 95 | return role; 96 | } 97 | 98 | public String getEmail() { 99 | return email; 100 | } 101 | 102 | public String getPhone() { 103 | return phone; 104 | } 105 | 106 | public String getEmailConfirmedAt() { 107 | return emailConfirmedAt; 108 | } 109 | 110 | public String getConfirmedAt() { 111 | return confirmedAt; 112 | } 113 | 114 | public String getLastSignInAt() { 115 | return lastSignInAt; 116 | } 117 | 118 | public String getCreatedAt() { 119 | return createdAt; 120 | } 121 | 122 | public String getUpdatedAt() { 123 | return updatedAt; 124 | } 125 | 126 | public Map getAppMetadata() { 127 | return appMetadata; 128 | } 129 | 130 | public Map getUserMetadata() { 131 | return userMetadata; 132 | } 133 | 134 | 135 | @Override 136 | public Collection getAuthorities() { 137 | return grantedAuthority; 138 | } 139 | 140 | @Override 141 | public String getPassword() { 142 | return password; 143 | } 144 | 145 | @Override 146 | public String getUsername() { 147 | return userName; 148 | } 149 | 150 | @Override 151 | public boolean isAccountNonExpired() { 152 | return true; 153 | } 154 | 155 | @Override 156 | public boolean isAccountNonLocked() { 157 | return true; 158 | } 159 | 160 | @Override 161 | public boolean isCredentialsNonExpired() { 162 | return true; 163 | } 164 | 165 | @Override 166 | public boolean isEnabled() { 167 | return true; 168 | } 169 | 170 | @Override 171 | public String getName() { 172 | return getUsername(); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main/java/com/changenode/frisson/websecurity/WebSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson.websecurity; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 10 | import org.springframework.security.config.http.SessionCreationPolicy; 11 | 12 | @Configuration 13 | @EnableWebSecurity 14 | public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { 15 | 16 | private final SupabaseAuthService supabaseAuthService; 17 | private final Logger logger = LoggerFactory.getLogger(WebSecurityConfiguration.class); 18 | @Value("${spring.profiles.active:}") 19 | private String activeProfile; 20 | 21 | public WebSecurityConfiguration(SupabaseAuthService supabaseAuthService) { 22 | this.supabaseAuthService = supabaseAuthService; 23 | } 24 | 25 | @Override 26 | protected void configure(HttpSecurity http) throws Exception { 27 | // Only allow frames if using h2 as the database 28 | if (activeProfile.contains("h2")) { 29 | logger.info("h2 -> frame options disabled."); 30 | http.headers().frameOptions().disable(); 31 | } else { 32 | logger.info("h2 not found."); 33 | } 34 | 35 | http 36 | .addFilter(new JwtAuthorizeFilter(authenticationManager(), supabaseAuthService)) 37 | .authorizeRequests() 38 | .antMatchers("/**/*.html", "/pinegrow.json").denyAll() 39 | .antMatchers("/assets/**", "/blocks.css", "/public/**", "/bootstrap/**", "/webjars/**", "/", "/h2-console/**").permitAll() 40 | .anyRequest() 41 | .authenticated() 42 | .and() 43 | .formLogin().loginPage("/public/sign-in") 44 | .and() 45 | .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 46 | .and() 47 | ; 48 | logger.info("Security configured."); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "supabase.url", 5 | "type": "java.lang.String", 6 | "description": "Description for supabase.url." 7 | }, 8 | { 9 | "name": "supabase.anon", 10 | "type": "java.lang.String", 11 | "description": "Description for supabase.anon." 12 | }, 13 | { 14 | "name": "supabase.jwt_secret", 15 | "type": "java.lang.String", 16 | "description": "Description for supabase.jwt_secret." 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /src/main/resources/application-h2.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | server: 3 | address: "http://localhost:8080" 4 | spring: 5 | datasource: 6 | url: jdbc:h2:mem:test;INIT=create schema if not exists auth\;create schema if not exists public 7 | username: sa 8 | password: 9 | driverClassName: org.h2.Driver 10 | jpa: 11 | properties: 12 | hibernate: 13 | dialect: org.hibernate.dialect.H2Dialect 14 | jdbc: 15 | lob: 16 | non_contextual_creation: true 17 | open-in-view: true 18 | #show-sql: true 19 | hibernate: 20 | ddl-auto: create-drop 21 | h2: 22 | console: 23 | enabled: true 24 | path: /h2-console 25 | settings: 26 | web-allow-others: true 27 | allowed: 28 | resources: /h2-console/** 29 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | supabase: 2 | url: ${SUPABASE_URL} 3 | anon: ${SUPABASE_ANON_KEY} 4 | jwt_secret: ${SUPABASE_JWT_SIGNER} 5 | spring: 6 | application: 7 | name: Demo 8 | main: 9 | banner-mode: "off" 10 | datasource: 11 | url: ${SUPABASE_DATABASE_URL} 12 | username: ${SUPABASE_DATABASE_USER} 13 | password: ${SUPABASE_DATABASE_PASSWORD} 14 | driverClassName: org.postgresql.Driver 15 | thymeleaf: 16 | prefix: classpath:/public/ 17 | jpa: 18 | open-in-view: true 19 | properties: 20 | hibernate: 21 | dialect: org.hibernate.dialect.PostgreSQLDialect 22 | ddl-auto: validate 23 | jdbc: 24 | lob: 25 | non_contextual_creation: true 26 | #show-sql: true 27 | server: 28 | error: 29 | whitelabel: 30 | enabled: true 31 | include-stacktrace: always 32 | address: localhost 33 | port: ${PORT:8080} 34 | logging: 35 | pattern: 36 | console: "%d{HH:mm:ss.SSS} %highlight(%-5level) %yellow(%logger{40}.%M\\(%class{0}.java:%line\\)) - %msg%throwable%n" 37 | level: 38 | root: WARN 39 | org: 40 | hibernate: 41 | dialect: INFO 42 | springframework: 43 | boot: 44 | test: 45 | context: 46 | SpringBootTestContextBootstrapper: WARN 47 | test: 48 | context: 49 | support: 50 | AbstractContextLoader: OFF 51 | AnnotationConfigContextLoaderUtils: OFF 52 | zaxxer: 53 | hikari: 54 | HikariDataSource: INFO 55 | -------------------------------------------------------------------------------- /src/main/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/resources/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/resources/favicon.png -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/public/_pginfo/fonts.json: -------------------------------------------------------------------------------- 1 | { 2 | "activated": [ 3 | { 4 | "type": "system", 5 | "family": "Arial, Helvetica, sans-serif", 6 | "weights": [], 7 | "styles": [], 8 | "category": "sans", 9 | "usage": "all" 10 | }, 11 | { 12 | "type": "system", 13 | "family": "\"Arial Black\", Gadget, sans-serif", 14 | "weights": [], 15 | "styles": [], 16 | "category": "sans", 17 | "usage": "headings" 18 | }, 19 | { 20 | "type": "system", 21 | "family": "Impact, Charcoal, sans-serif", 22 | "weights": [], 23 | "styles": [], 24 | "category": "sans", 25 | "usage": "headings" 26 | }, 27 | { 28 | "type": "system", 29 | "family": "\"Lucida Sans Unicode\", \"Lucida Grande\", sans-serif", 30 | "weights": [], 31 | "styles": [], 32 | "category": "sans", 33 | "usage": "all" 34 | }, 35 | { 36 | "type": "system", 37 | "family": "Tahoma, Geneva, sans-serif", 38 | "weights": [], 39 | "styles": [], 40 | "category": "sans", 41 | "usage": "all" 42 | }, 43 | { 44 | "type": "system", 45 | "family": "Verdana, Geneva, sans-serif", 46 | "weights": [], 47 | "styles": [], 48 | "category": "sans", 49 | "usage": "all" 50 | }, 51 | { 52 | "type": "system", 53 | "family": "\"Times New Roman\", Times, serif", 54 | "weights": [], 55 | "styles": [], 56 | "category": "serif", 57 | "usage": "all" 58 | }, 59 | { 60 | "type": "system", 61 | "family": "\"Palatino Linotype\", \"Book Antiqua\", Palatino, serif", 62 | "weights": [], 63 | "styles": [], 64 | "category": "serif", 65 | "usage": "all" 66 | }, 67 | { 68 | "type": "system", 69 | "family": "Georgia, serif", 70 | "weights": [], 71 | "styles": [], 72 | "category": "serif", 73 | "usage": "all" 74 | }, 75 | { 76 | "type": "system", 77 | "family": "\"Courier New\", Courier, monospace", 78 | "weights": [], 79 | "styles": [], 80 | "category": "mono", 81 | "usage": "all" 82 | }, 83 | { 84 | "type": "system", 85 | "family": "\"Lucida Console\", Monaco, monospace", 86 | "weights": [], 87 | "styles": [], 88 | "category": "mono", 89 | "usage": "all" 90 | }, 91 | { 92 | "type": "google", 93 | "family": "Abril Fatface", 94 | "weights": [], 95 | "styles": [], 96 | "category": "display", 97 | "url": "https://fonts.googleapis.com/css?family=Abril+Fatface", 98 | "usage": "h1", 99 | "fallback": "sans-serif" 100 | }, 101 | { 102 | "type": "google", 103 | "family": "Alegreya", 104 | "weights": [ 105 | "400", 106 | "500", 107 | "600", 108 | "700", 109 | "800", 110 | "900" 111 | ], 112 | "styles": [ 113 | { 114 | "weight": "400" 115 | }, 116 | { 117 | "weight": "500" 118 | }, 119 | { 120 | "weight": "600" 121 | }, 122 | { 123 | "weight": "700" 124 | }, 125 | { 126 | "weight": "800" 127 | }, 128 | { 129 | "weight": "900" 130 | } 131 | ], 132 | "category": "serif", 133 | "url": "https://fonts.googleapis.com/css?family=Alegreya:400,500,600,700,800,900", 134 | "usage": "all", 135 | "fallback": "serif" 136 | }, 137 | { 138 | "type": "google", 139 | "family": "Arima Madurai", 140 | "weights": [ 141 | "100", 142 | "200", 143 | "300", 144 | "400", 145 | "500", 146 | "700", 147 | "800", 148 | "900" 149 | ], 150 | "styles": [ 151 | { 152 | "weight": "100" 153 | }, 154 | { 155 | "weight": "200" 156 | }, 157 | { 158 | "weight": "300" 159 | }, 160 | { 161 | "weight": "400" 162 | }, 163 | { 164 | "weight": "500" 165 | }, 166 | { 167 | "weight": "700" 168 | }, 169 | { 170 | "weight": "800" 171 | }, 172 | { 173 | "weight": "900" 174 | } 175 | ], 176 | "category": "display", 177 | "url": "https://fonts.googleapis.com/css?family=Arima+Madurai:100,200,300,400,500,700,800,900", 178 | "usage": "headings", 179 | "fallback": "sans-serif" 180 | }, 181 | { 182 | "type": "google", 183 | "family": "Arvo", 184 | "weights": [ 185 | "400", 186 | "700" 187 | ], 188 | "styles": [ 189 | { 190 | "weight": "400" 191 | }, 192 | { 193 | "weight": "700" 194 | } 195 | ], 196 | "category": "serif", 197 | "url": "https://fonts.googleapis.com/css?family=Arvo:400,700", 198 | "usage": "h1", 199 | "fallback": "serif" 200 | }, 201 | { 202 | "type": "google", 203 | "family": "Asap", 204 | "weights": [ 205 | "400", 206 | "500", 207 | "600", 208 | "700" 209 | ], 210 | "styles": [ 211 | { 212 | "weight": "400" 213 | }, 214 | { 215 | "weight": "500" 216 | }, 217 | { 218 | "weight": "600" 219 | }, 220 | { 221 | "weight": "700" 222 | } 223 | ], 224 | "category": "sans", 225 | "url": "https://fonts.googleapis.com/css?family=Asap:400,500,600,700", 226 | "usage": "all", 227 | "fallback": "sans-serif" 228 | }, 229 | { 230 | "type": "google", 231 | "family": "BioRhyme", 232 | "weights": [ 233 | "200", 234 | "300", 235 | "400", 236 | "700", 237 | "800" 238 | ], 239 | "styles": [ 240 | { 241 | "weight": "200" 242 | }, 243 | { 244 | "weight": "300" 245 | }, 246 | { 247 | "weight": "400" 248 | }, 249 | { 250 | "weight": "700" 251 | }, 252 | { 253 | "weight": "800" 254 | } 255 | ], 256 | "category": "serif", 257 | "url": "https://fonts.googleapis.com/css?family=BioRhyme:200,300,400,700,800", 258 | "usage": "all", 259 | "fallback": "serif" 260 | }, 261 | { 262 | "type": "google", 263 | "family": "Cabin", 264 | "weights": [ 265 | "400", 266 | "500", 267 | "600", 268 | "700" 269 | ], 270 | "styles": [ 271 | { 272 | "weight": "400" 273 | }, 274 | { 275 | "weight": "500" 276 | }, 277 | { 278 | "weight": "600" 279 | }, 280 | { 281 | "weight": "700" 282 | } 283 | ], 284 | "category": "sans", 285 | "url": "https://fonts.googleapis.com/css?family=Cabin:400,500,600,700", 286 | "usage": "all", 287 | "fallback": "sans-serif" 288 | }, 289 | { 290 | "type": "google", 291 | "family": "Catamaran", 292 | "weights": [ 293 | "100", 294 | "200", 295 | "300", 296 | "400", 297 | "500", 298 | "600", 299 | "700", 300 | "800", 301 | "900" 302 | ], 303 | "styles": [ 304 | { 305 | "weight": "100" 306 | }, 307 | { 308 | "weight": "200" 309 | }, 310 | { 311 | "weight": "300" 312 | }, 313 | { 314 | "weight": "400" 315 | }, 316 | { 317 | "weight": "500" 318 | }, 319 | { 320 | "weight": "600" 321 | }, 322 | { 323 | "weight": "700" 324 | }, 325 | { 326 | "weight": "800" 327 | }, 328 | { 329 | "weight": "900" 330 | } 331 | ], 332 | "category": "sans", 333 | "url": "https://fonts.googleapis.com/css?family=Catamaran:100,200,300,400,500,600,700,800,900", 334 | "usage": "headings", 335 | "fallback": "sans-serif" 336 | }, 337 | { 338 | "type": "google", 339 | "family": "Caveat", 340 | "weights": [ 341 | "400", 342 | "500", 343 | "600", 344 | "700" 345 | ], 346 | "styles": [ 347 | { 348 | "weight": "400" 349 | }, 350 | { 351 | "weight": "500" 352 | }, 353 | { 354 | "weight": "600" 355 | }, 356 | { 357 | "weight": "700" 358 | } 359 | ], 360 | "category": "display", 361 | "url": "https://fonts.googleapis.com/css?family=Caveat:400,500,600,700", 362 | "usage": "all", 363 | "fallback": "sans-serif" 364 | }, 365 | { 366 | "type": "google", 367 | "family": "Caveat Brush", 368 | "weights": [], 369 | "styles": [], 370 | "category": "display", 371 | "url": "https://fonts.googleapis.com/css?family=Caveat+Brush", 372 | "usage": "all", 373 | "fallback": "sans-serif" 374 | }, 375 | { 376 | "type": "google", 377 | "family": "Cormorant Garamond", 378 | "weights": [ 379 | "300", 380 | "400", 381 | "500", 382 | "600", 383 | "700" 384 | ], 385 | "styles": [ 386 | { 387 | "weight": "300" 388 | }, 389 | { 390 | "weight": "400" 391 | }, 392 | { 393 | "weight": "500" 394 | }, 395 | { 396 | "weight": "600" 397 | }, 398 | { 399 | "weight": "700" 400 | } 401 | ], 402 | "category": "serif", 403 | "url": "https://fonts.googleapis.com/css?family=Cormorant+Garamond:300,400,500,600,700", 404 | "usage": "h1", 405 | "fallback": "serif" 406 | }, 407 | { 408 | "type": "google", 409 | "family": "Dancing Script", 410 | "weights": [ 411 | "400", 412 | "500", 413 | "600", 414 | "700" 415 | ], 416 | "styles": [ 417 | { 418 | "weight": "400" 419 | }, 420 | { 421 | "weight": "500" 422 | }, 423 | { 424 | "weight": "600" 425 | }, 426 | { 427 | "weight": "700" 428 | } 429 | ], 430 | "category": "display", 431 | "url": "https://fonts.googleapis.com/css?family=Dancing+Script:400,500,600,700", 432 | "usage": "headings", 433 | "fallback": "sans-serif" 434 | }, 435 | { 436 | "type": "google", 437 | "family": "DM Sans", 438 | "weights": [ 439 | "400", 440 | "500", 441 | "700" 442 | ], 443 | "styles": [ 444 | { 445 | "weight": "400" 446 | }, 447 | { 448 | "weight": "500" 449 | }, 450 | { 451 | "weight": "700" 452 | } 453 | ], 454 | "category": "sans", 455 | "url": "https://fonts.googleapis.com/css?family=DM+Sans:400,500,700", 456 | "usage": "all", 457 | "fallback": "sans-serif" 458 | }, 459 | { 460 | "type": "google", 461 | "family": "Dosis", 462 | "weights": [ 463 | "200", 464 | "300", 465 | "400", 466 | "500", 467 | "600", 468 | "700", 469 | "800" 470 | ], 471 | "styles": [ 472 | { 473 | "weight": "200" 474 | }, 475 | { 476 | "weight": "300" 477 | }, 478 | { 479 | "weight": "400" 480 | }, 481 | { 482 | "weight": "500" 483 | }, 484 | { 485 | "weight": "600" 486 | }, 487 | { 488 | "weight": "700" 489 | }, 490 | { 491 | "weight": "800" 492 | } 493 | ], 494 | "category": "sans", 495 | "url": "https://fonts.googleapis.com/css?family=Dosis:200,300,400,500,600,700,800", 496 | "usage": "all", 497 | "fallback": "sans-serif" 498 | }, 499 | { 500 | "type": "google", 501 | "family": "Expletus Sans", 502 | "weights": [ 503 | "400", 504 | "500", 505 | "600", 506 | "700" 507 | ], 508 | "styles": [ 509 | { 510 | "weight": "400" 511 | }, 512 | { 513 | "weight": "500" 514 | }, 515 | { 516 | "weight": "600" 517 | }, 518 | { 519 | "weight": "700" 520 | } 521 | ], 522 | "category": "display", 523 | "url": "https://fonts.googleapis.com/css?family=Expletus+Sans:400,500,600,700", 524 | "usage": "headings", 525 | "fallback": "sans-serif" 526 | }, 527 | { 528 | "type": "google", 529 | "family": "Fira Sans", 530 | "weights": [ 531 | "100", 532 | "200", 533 | "300", 534 | "400", 535 | "500", 536 | "600", 537 | "700", 538 | "800", 539 | "900" 540 | ], 541 | "styles": [ 542 | { 543 | "weight": "100" 544 | }, 545 | { 546 | "weight": "200" 547 | }, 548 | { 549 | "weight": "300" 550 | }, 551 | { 552 | "weight": "400" 553 | }, 554 | { 555 | "weight": "500" 556 | }, 557 | { 558 | "weight": "600" 559 | }, 560 | { 561 | "weight": "700" 562 | }, 563 | { 564 | "weight": "800" 565 | }, 566 | { 567 | "weight": "900" 568 | } 569 | ], 570 | "category": "sans", 571 | "url": "https://fonts.googleapis.com/css?family=Fira+Sans:100,200,300,400,500,600,700,800,900", 572 | "usage": "all", 573 | "fallback": "sans-serif" 574 | }, 575 | { 576 | "type": "google", 577 | "family": "Fraunces", 578 | "weights": [ 579 | "100", 580 | "200", 581 | "300", 582 | "400", 583 | "500", 584 | "600", 585 | "700", 586 | "800", 587 | "900" 588 | ], 589 | "styles": [ 590 | { 591 | "weight": "100" 592 | }, 593 | { 594 | "weight": "200" 595 | }, 596 | { 597 | "weight": "300" 598 | }, 599 | { 600 | "weight": "400" 601 | }, 602 | { 603 | "weight": "500" 604 | }, 605 | { 606 | "weight": "600" 607 | }, 608 | { 609 | "weight": "700" 610 | }, 611 | { 612 | "weight": "800" 613 | }, 614 | { 615 | "weight": "900" 616 | } 617 | ], 618 | "category": "serif", 619 | "url": "https://fonts.googleapis.com/css?family=Fraunces:100,200,300,400,500,600,700,800,900", 620 | "usage": "all", 621 | "fallback": "serif" 622 | }, 623 | { 624 | "type": "google", 625 | "family": "IBM Plex Serif", 626 | "weights": [ 627 | "100", 628 | "200", 629 | "300", 630 | "400", 631 | "500", 632 | "600", 633 | "700" 634 | ], 635 | "styles": [ 636 | { 637 | "weight": "100" 638 | }, 639 | { 640 | "weight": "200" 641 | }, 642 | { 643 | "weight": "300" 644 | }, 645 | { 646 | "weight": "400" 647 | }, 648 | { 649 | "weight": "500" 650 | }, 651 | { 652 | "weight": "600" 653 | }, 654 | { 655 | "weight": "700" 656 | } 657 | ], 658 | "category": "serif", 659 | "url": "https://fonts.googleapis.com/css?family=IBM+Plex+Serif:100,200,300,400,500,600,700", 660 | "usage": "all", 661 | "fallback": "serif" 662 | }, 663 | { 664 | "type": "google", 665 | "family": "Inter", 666 | "weights": [ 667 | "100", 668 | "200", 669 | "300", 670 | "400", 671 | "500", 672 | "600", 673 | "700", 674 | "800", 675 | "900" 676 | ], 677 | "styles": [ 678 | { 679 | "weight": "100" 680 | }, 681 | { 682 | "weight": "200" 683 | }, 684 | { 685 | "weight": "300" 686 | }, 687 | { 688 | "weight": "400" 689 | }, 690 | { 691 | "weight": "500" 692 | }, 693 | { 694 | "weight": "600" 695 | }, 696 | { 697 | "weight": "700" 698 | }, 699 | { 700 | "weight": "800" 701 | }, 702 | { 703 | "weight": "900" 704 | } 705 | ], 706 | "category": "sans", 707 | "url": "https://fonts.googleapis.com/css?family=Inter:100,200,300,400,500,600,700,800,900", 708 | "usage": "all", 709 | "fallback": "sans-serif" 710 | }, 711 | { 712 | "type": "google", 713 | "family": "Josefin Sans", 714 | "weights": [ 715 | "200", 716 | "300", 717 | "400", 718 | "500", 719 | "600", 720 | "700" 721 | ], 722 | "styles": [ 723 | { 724 | "weight": "200" 725 | }, 726 | { 727 | "weight": "300" 728 | }, 729 | { 730 | "weight": "400" 731 | }, 732 | { 733 | "weight": "500" 734 | }, 735 | { 736 | "weight": "600" 737 | }, 738 | { 739 | "weight": "700" 740 | } 741 | ], 742 | "category": "sans", 743 | "url": "https://fonts.googleapis.com/css?family=Josefin+Sans:200,300,400,500,600,700", 744 | "usage": "headings", 745 | "fallback": "sans-serif" 746 | }, 747 | { 748 | "type": "google", 749 | "family": "Jost", 750 | "weights": [ 751 | "100", 752 | "200", 753 | "300", 754 | "400", 755 | "500", 756 | "600", 757 | "700", 758 | "800", 759 | "900" 760 | ], 761 | "styles": [ 762 | { 763 | "weight": "100" 764 | }, 765 | { 766 | "weight": "200" 767 | }, 768 | { 769 | "weight": "300" 770 | }, 771 | { 772 | "weight": "400" 773 | }, 774 | { 775 | "weight": "500" 776 | }, 777 | { 778 | "weight": "600" 779 | }, 780 | { 781 | "weight": "700" 782 | }, 783 | { 784 | "weight": "800" 785 | }, 786 | { 787 | "weight": "900" 788 | } 789 | ], 790 | "category": "sans", 791 | "url": "https://fonts.googleapis.com/css?family=Jost:100,200,300,400,500,600,700,800,900", 792 | "usage": "headings", 793 | "fallback": "sans-serif" 794 | }, 795 | { 796 | "type": "google", 797 | "family": "Lato", 798 | "weights": [ 799 | "100", 800 | "200", 801 | "300", 802 | "400", 803 | "500", 804 | "600", 805 | "700", 806 | "800", 807 | "900" 808 | ], 809 | "styles": [ 810 | { 811 | "weight": "100" 812 | }, 813 | { 814 | "weight": "200" 815 | }, 816 | { 817 | "weight": "300" 818 | }, 819 | { 820 | "weight": "400" 821 | }, 822 | { 823 | "weight": "500" 824 | }, 825 | { 826 | "weight": "600" 827 | }, 828 | { 829 | "weight": "700" 830 | }, 831 | { 832 | "weight": "800" 833 | }, 834 | { 835 | "weight": "900" 836 | } 837 | ], 838 | "category": "sans", 839 | "url": "https://fonts.googleapis.com/css?family=Lato:100,200,300,400,500,600,700,800,900", 840 | "usage": "all", 841 | "fallback": "sans-serif" 842 | }, 843 | { 844 | "type": "google", 845 | "family": "Libre Baskerville", 846 | "weights": [ 847 | "400", 848 | "700" 849 | ], 850 | "styles": [ 851 | { 852 | "weight": "400" 853 | }, 854 | { 855 | "weight": "700" 856 | } 857 | ], 858 | "category": "serif", 859 | "url": "https://fonts.googleapis.com/css?family=Libre+Baskerville:400,700", 860 | "usage": "headings", 861 | "fallback": "serif" 862 | }, 863 | { 864 | "type": "google", 865 | "family": "Lora", 866 | "weights": [ 867 | "400", 868 | "500", 869 | "600", 870 | "700" 871 | ], 872 | "styles": [ 873 | { 874 | "weight": "400" 875 | }, 876 | { 877 | "weight": "500" 878 | }, 879 | { 880 | "weight": "600" 881 | }, 882 | { 883 | "weight": "700" 884 | } 885 | ], 886 | "category": "serif", 887 | "url": "https://fonts.googleapis.com/css?family=Lora:400,500,600,700", 888 | "usage": "h1", 889 | "fallback": "serif" 890 | }, 891 | { 892 | "type": "google", 893 | "family": "Kalam", 894 | "weights": [ 895 | "300", 896 | "400", 897 | "700" 898 | ], 899 | "styles": [ 900 | { 901 | "weight": "300" 902 | }, 903 | { 904 | "weight": "400" 905 | }, 906 | { 907 | "weight": "700" 908 | } 909 | ], 910 | "category": "display", 911 | "url": "https://fonts.googleapis.com/css?family=Kalam:300,400,700", 912 | "usage": "all", 913 | "fallback": "sans-serif" 914 | }, 915 | { 916 | "type": "google", 917 | "family": "Kufam", 918 | "weights": [ 919 | "400", 920 | "500", 921 | "600", 922 | "700", 923 | "800", 924 | "900" 925 | ], 926 | "styles": [ 927 | { 928 | "weight": "400" 929 | }, 930 | { 931 | "weight": "500" 932 | }, 933 | { 934 | "weight": "600" 935 | }, 936 | { 937 | "weight": "700" 938 | }, 939 | { 940 | "weight": "800" 941 | }, 942 | { 943 | "weight": "900" 944 | } 945 | ], 946 | "category": "display", 947 | "url": "https://fonts.googleapis.com/css?family=Kufam:400,500,600,700,800,900", 948 | "usage": "headings", 949 | "fallback": "sans-serif" 950 | }, 951 | { 952 | "type": "google", 953 | "family": "Mali", 954 | "weights": [ 955 | "200", 956 | "300", 957 | "400", 958 | "500", 959 | "600", 960 | "700" 961 | ], 962 | "styles": [ 963 | { 964 | "weight": "200" 965 | }, 966 | { 967 | "weight": "300" 968 | }, 969 | { 970 | "weight": "400" 971 | }, 972 | { 973 | "weight": "500" 974 | }, 975 | { 976 | "weight": "600" 977 | }, 978 | { 979 | "weight": "700" 980 | } 981 | ], 982 | "category": "display", 983 | "url": "https://fonts.googleapis.com/css?family=Mali:200,300,400,500,600,700", 984 | "usage": "all", 985 | "fallback": "sans-serif" 986 | }, 987 | { 988 | "type": "google", 989 | "family": "Martel", 990 | "weights": [ 991 | "200", 992 | "300", 993 | "400", 994 | "600", 995 | "700", 996 | "800", 997 | "900" 998 | ], 999 | "styles": [ 1000 | { 1001 | "weight": "200" 1002 | }, 1003 | { 1004 | "weight": "300" 1005 | }, 1006 | { 1007 | "weight": "400" 1008 | }, 1009 | { 1010 | "weight": "600" 1011 | }, 1012 | { 1013 | "weight": "700" 1014 | }, 1015 | { 1016 | "weight": "800" 1017 | }, 1018 | { 1019 | "weight": "900" 1020 | } 1021 | ], 1022 | "category": "serif", 1023 | "url": "https://fonts.googleapis.com/css?family=Martel:200,300,400,600,700,800,900", 1024 | "usage": "headings", 1025 | "fallback": "serif" 1026 | }, 1027 | { 1028 | "type": "google", 1029 | "family": "Maven Pro", 1030 | "weights": [ 1031 | "400", 1032 | "500", 1033 | "600", 1034 | "700", 1035 | "800", 1036 | "900" 1037 | ], 1038 | "styles": [ 1039 | { 1040 | "weight": "400" 1041 | }, 1042 | { 1043 | "weight": "500" 1044 | }, 1045 | { 1046 | "weight": "600" 1047 | }, 1048 | { 1049 | "weight": "700" 1050 | }, 1051 | { 1052 | "weight": "800" 1053 | }, 1054 | { 1055 | "weight": "900" 1056 | } 1057 | ], 1058 | "category": "sans", 1059 | "url": "https://fonts.googleapis.com/css?family=Maven+Pro:400,500,600,700,800,900", 1060 | "usage": "all", 1061 | "fallback": "sans-serif" 1062 | }, 1063 | { 1064 | "type": "google", 1065 | "family": "Montserrat", 1066 | "weights": [ 1067 | "100", 1068 | "200", 1069 | "300", 1070 | "400", 1071 | "500", 1072 | "600", 1073 | "700", 1074 | "800", 1075 | "900" 1076 | ], 1077 | "styles": [ 1078 | { 1079 | "weight": "100" 1080 | }, 1081 | { 1082 | "weight": "200" 1083 | }, 1084 | { 1085 | "weight": "300" 1086 | }, 1087 | { 1088 | "weight": "400" 1089 | }, 1090 | { 1091 | "weight": "500" 1092 | }, 1093 | { 1094 | "weight": "600" 1095 | }, 1096 | { 1097 | "weight": "700" 1098 | }, 1099 | { 1100 | "weight": "800" 1101 | }, 1102 | { 1103 | "weight": "900" 1104 | } 1105 | ], 1106 | "category": "sans", 1107 | "url": "https://fonts.googleapis.com/css?family=Montserrat:100,200,300,400,500,600,700,800,900", 1108 | "usage": "headings", 1109 | "fallback": "sans-serif" 1110 | }, 1111 | { 1112 | "type": "google", 1113 | "family": "Mulish", 1114 | "weights": [ 1115 | "200", 1116 | "300", 1117 | "400", 1118 | "500", 1119 | "600", 1120 | "700", 1121 | "800", 1122 | "900" 1123 | ], 1124 | "styles": [ 1125 | { 1126 | "weight": "200" 1127 | }, 1128 | { 1129 | "weight": "300" 1130 | }, 1131 | { 1132 | "weight": "400" 1133 | }, 1134 | { 1135 | "weight": "500" 1136 | }, 1137 | { 1138 | "weight": "600" 1139 | }, 1140 | { 1141 | "weight": "700" 1142 | }, 1143 | { 1144 | "weight": "800" 1145 | }, 1146 | { 1147 | "weight": "900" 1148 | } 1149 | ], 1150 | "category": "sans", 1151 | "url": "https://fonts.googleapis.com/css?family=Mulish:200,300,400,500,600,700,800,900", 1152 | "usage": "all", 1153 | "fallback": "sans-serif" 1154 | }, 1155 | { 1156 | "type": "google", 1157 | "family": "Nunito Sans", 1158 | "weights": [ 1159 | "200", 1160 | "300", 1161 | "400", 1162 | "600", 1163 | "700", 1164 | "800", 1165 | "900" 1166 | ], 1167 | "styles": [ 1168 | { 1169 | "weight": "200" 1170 | }, 1171 | { 1172 | "weight": "300" 1173 | }, 1174 | { 1175 | "weight": "400" 1176 | }, 1177 | { 1178 | "weight": "600" 1179 | }, 1180 | { 1181 | "weight": "700" 1182 | }, 1183 | { 1184 | "weight": "800" 1185 | }, 1186 | { 1187 | "weight": "900" 1188 | } 1189 | ], 1190 | "category": "sans", 1191 | "url": "https://fonts.googleapis.com/css?family=Nunito+Sans:200,300,400,600,700,800,900", 1192 | "usage": "text", 1193 | "fallback": "sans-serif" 1194 | }, 1195 | { 1196 | "type": "google", 1197 | "family": "Open Sans", 1198 | "weights": [ 1199 | "300", 1200 | "400", 1201 | "600", 1202 | "700", 1203 | "800" 1204 | ], 1205 | "styles": [ 1206 | { 1207 | "weight": "300" 1208 | }, 1209 | { 1210 | "weight": "400" 1211 | }, 1212 | { 1213 | "weight": "600" 1214 | }, 1215 | { 1216 | "weight": "700" 1217 | }, 1218 | { 1219 | "weight": "800" 1220 | } 1221 | ], 1222 | "category": "sans", 1223 | "url": "https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,800", 1224 | "usage": "all", 1225 | "fallback": "sans-serif" 1226 | }, 1227 | { 1228 | "type": "google", 1229 | "family": "Oswald", 1230 | "weights": [ 1231 | "200", 1232 | "300", 1233 | "400", 1234 | "500", 1235 | "600", 1236 | "700" 1237 | ], 1238 | "styles": [ 1239 | { 1240 | "weight": "200" 1241 | }, 1242 | { 1243 | "weight": "300" 1244 | }, 1245 | { 1246 | "weight": "400" 1247 | }, 1248 | { 1249 | "weight": "500" 1250 | }, 1251 | { 1252 | "weight": "600" 1253 | }, 1254 | { 1255 | "weight": "700" 1256 | } 1257 | ], 1258 | "category": "sans", 1259 | "url": "https://fonts.googleapis.com/css?family=Oswald:200,300,400,500,600,700", 1260 | "usage": "headings", 1261 | "fallback": "sans-serif" 1262 | }, 1263 | { 1264 | "type": "google", 1265 | "family": "Overlock", 1266 | "weights": [ 1267 | "400", 1268 | "700", 1269 | "900" 1270 | ], 1271 | "styles": [ 1272 | { 1273 | "weight": "400" 1274 | }, 1275 | { 1276 | "weight": "700" 1277 | }, 1278 | { 1279 | "weight": "900" 1280 | } 1281 | ], 1282 | "category": "display", 1283 | "url": "https://fonts.googleapis.com/css?family=Overlock:400,700,900", 1284 | "usage": "all", 1285 | "fallback": "sans-serif" 1286 | }, 1287 | { 1288 | "type": "google", 1289 | "family": "Oxanium", 1290 | "weights": [ 1291 | "200", 1292 | "300", 1293 | "400", 1294 | "500", 1295 | "600", 1296 | "700", 1297 | "800" 1298 | ], 1299 | "styles": [ 1300 | { 1301 | "weight": "200" 1302 | }, 1303 | { 1304 | "weight": "300" 1305 | }, 1306 | { 1307 | "weight": "400" 1308 | }, 1309 | { 1310 | "weight": "500" 1311 | }, 1312 | { 1313 | "weight": "600" 1314 | }, 1315 | { 1316 | "weight": "700" 1317 | }, 1318 | { 1319 | "weight": "800" 1320 | } 1321 | ], 1322 | "category": "display", 1323 | "url": "https://fonts.googleapis.com/css?family=Oxanium:200,300,400,500,600,700,800", 1324 | "usage": "headings", 1325 | "fallback": "sans-serif" 1326 | }, 1327 | { 1328 | "type": "google", 1329 | "family": "Permanent Marker", 1330 | "weights": [ 1331 | "400" 1332 | ], 1333 | "styles": [ 1334 | { 1335 | "weight": "400" 1336 | } 1337 | ], 1338 | "category": "display", 1339 | "url": "https://fonts.googleapis.com/css?family=Permanent+Marker:400", 1340 | "usage": "h1", 1341 | "fallback": "sans-serif" 1342 | }, 1343 | { 1344 | "type": "google", 1345 | "family": "Playfair Display", 1346 | "weights": [ 1347 | "400", 1348 | "500", 1349 | "600", 1350 | "700", 1351 | "800", 1352 | "900" 1353 | ], 1354 | "styles": [ 1355 | { 1356 | "weight": "400" 1357 | }, 1358 | { 1359 | "weight": "500" 1360 | }, 1361 | { 1362 | "weight": "600" 1363 | }, 1364 | { 1365 | "weight": "700" 1366 | }, 1367 | { 1368 | "weight": "800" 1369 | }, 1370 | { 1371 | "weight": "900" 1372 | } 1373 | ], 1374 | "category": "sans", 1375 | "url": "https://fonts.googleapis.com/css?family=Playfair+Display:400,500,600,700,800,900", 1376 | "usage": "headings", 1377 | "fallback": "sans-serif" 1378 | }, 1379 | { 1380 | "type": "google", 1381 | "family": "Poppins", 1382 | "weights": [ 1383 | "100", 1384 | "200", 1385 | "300", 1386 | "400", 1387 | "500", 1388 | "600", 1389 | "700", 1390 | "800", 1391 | "900" 1392 | ], 1393 | "styles": [ 1394 | { 1395 | "weight": "100" 1396 | }, 1397 | { 1398 | "weight": "200" 1399 | }, 1400 | { 1401 | "weight": "300" 1402 | }, 1403 | { 1404 | "weight": "400" 1405 | }, 1406 | { 1407 | "weight": "500" 1408 | }, 1409 | { 1410 | "weight": "600" 1411 | }, 1412 | { 1413 | "weight": "700" 1414 | }, 1415 | { 1416 | "weight": "800" 1417 | }, 1418 | { 1419 | "weight": "900" 1420 | } 1421 | ], 1422 | "category": "sans", 1423 | "url": "https://fonts.googleapis.com/css?family=Poppins:100,200,300,400,500,600,700,800,900", 1424 | "usage": "headings", 1425 | "fallback": "sans-serif" 1426 | }, 1427 | { 1428 | "type": "google", 1429 | "family": "Prompt", 1430 | "weights": [ 1431 | "100", 1432 | "200", 1433 | "300", 1434 | "400", 1435 | "500", 1436 | "600", 1437 | "700", 1438 | "800", 1439 | "900" 1440 | ], 1441 | "styles": [ 1442 | { 1443 | "weight": "100" 1444 | }, 1445 | { 1446 | "weight": "200" 1447 | }, 1448 | { 1449 | "weight": "300" 1450 | }, 1451 | { 1452 | "weight": "400" 1453 | }, 1454 | { 1455 | "weight": "500" 1456 | }, 1457 | { 1458 | "weight": "600" 1459 | }, 1460 | { 1461 | "weight": "700" 1462 | }, 1463 | { 1464 | "weight": "800" 1465 | }, 1466 | { 1467 | "weight": "900" 1468 | } 1469 | ], 1470 | "category": "sans", 1471 | "url": "https://fonts.googleapis.com/css?family=Prompt:100,200,300,400,500,600,700,800,900", 1472 | "usage": "headings", 1473 | "fallback": "sans-serif" 1474 | }, 1475 | { 1476 | "type": "google", 1477 | "family": "Proza Libre", 1478 | "weights": [ 1479 | "400", 1480 | "500", 1481 | "600", 1482 | "700", 1483 | "800" 1484 | ], 1485 | "styles": [ 1486 | { 1487 | "weight": "400" 1488 | }, 1489 | { 1490 | "weight": "500" 1491 | }, 1492 | { 1493 | "weight": "600" 1494 | }, 1495 | { 1496 | "weight": "700" 1497 | }, 1498 | { 1499 | "weight": "800" 1500 | } 1501 | ], 1502 | "category": "sans", 1503 | "url": "https://fonts.googleapis.com/css?family=Proza+Libre:400,500,600,700,800", 1504 | "usage": "all", 1505 | "fallback": "sans-serif" 1506 | }, 1507 | { 1508 | "type": "google", 1509 | "family": "Quicksand", 1510 | "weights": [ 1511 | "300", 1512 | "400", 1513 | "500", 1514 | "600", 1515 | "700" 1516 | ], 1517 | "styles": [ 1518 | { 1519 | "weight": "300" 1520 | }, 1521 | { 1522 | "weight": "400" 1523 | }, 1524 | { 1525 | "weight": "500" 1526 | }, 1527 | { 1528 | "weight": "600" 1529 | }, 1530 | { 1531 | "weight": "700" 1532 | } 1533 | ], 1534 | "category": "sans", 1535 | "url": "https://fonts.googleapis.com/css?family=Quicksand:300,400,500,600,700", 1536 | "usage": "all", 1537 | "fallback": "sans-serif" 1538 | }, 1539 | { 1540 | "type": "google", 1541 | "family": "Rakkas", 1542 | "weights": [ 1543 | "400" 1544 | ], 1545 | "styles": [ 1546 | { 1547 | "weight": "400" 1548 | } 1549 | ], 1550 | "category": "display", 1551 | "url": "https://fonts.googleapis.com/css?family=Rakkas:400", 1552 | "usage": "h1", 1553 | "fallback": "sans-serif" 1554 | }, 1555 | { 1556 | "type": "google", 1557 | "family": "Raleway", 1558 | "weights": [ 1559 | "100", 1560 | "200", 1561 | "300", 1562 | "400", 1563 | "500", 1564 | "600", 1565 | "700", 1566 | "800", 1567 | "900" 1568 | ], 1569 | "styles": [ 1570 | { 1571 | "weight": "100" 1572 | }, 1573 | { 1574 | "weight": "200" 1575 | }, 1576 | { 1577 | "weight": "300" 1578 | }, 1579 | { 1580 | "weight": "400" 1581 | }, 1582 | { 1583 | "weight": "500" 1584 | }, 1585 | { 1586 | "weight": "600" 1587 | }, 1588 | { 1589 | "weight": "700" 1590 | }, 1591 | { 1592 | "weight": "800" 1593 | }, 1594 | { 1595 | "weight": "900" 1596 | } 1597 | ], 1598 | "category": "sans", 1599 | "url": "https://fonts.googleapis.com/css?family=Raleway:100,200,300,400,500,600,700,800,900", 1600 | "usage": "all", 1601 | "fallback": "sans-serif" 1602 | }, 1603 | { 1604 | "type": "google", 1605 | "family": "Rokkitt", 1606 | "weights": [ 1607 | "100", 1608 | "200", 1609 | "300", 1610 | "400", 1611 | "500", 1612 | "600", 1613 | "700", 1614 | "800", 1615 | "900" 1616 | ], 1617 | "styles": [ 1618 | { 1619 | "weight": "100" 1620 | }, 1621 | { 1622 | "weight": "200" 1623 | }, 1624 | { 1625 | "weight": "300" 1626 | }, 1627 | { 1628 | "weight": "400" 1629 | }, 1630 | { 1631 | "weight": "500" 1632 | }, 1633 | { 1634 | "weight": "600" 1635 | }, 1636 | { 1637 | "weight": "700" 1638 | }, 1639 | { 1640 | "weight": "800" 1641 | }, 1642 | { 1643 | "weight": "900" 1644 | } 1645 | ], 1646 | "category": "serif", 1647 | "url": "https://fonts.googleapis.com/css?family=Rokkitt:100,200,300,400,500,600,700,800,900", 1648 | "usage": "h1", 1649 | "fallback": "serif" 1650 | }, 1651 | { 1652 | "type": "google", 1653 | "family": "Ruda", 1654 | "weights": [ 1655 | "400", 1656 | "500", 1657 | "600", 1658 | "700", 1659 | "800", 1660 | "900" 1661 | ], 1662 | "styles": [ 1663 | { 1664 | "weight": "400" 1665 | }, 1666 | { 1667 | "weight": "500" 1668 | }, 1669 | { 1670 | "weight": "600" 1671 | }, 1672 | { 1673 | "weight": "700" 1674 | }, 1675 | { 1676 | "weight": "800" 1677 | }, 1678 | { 1679 | "weight": "900" 1680 | } 1681 | ], 1682 | "category": "sans", 1683 | "url": "https://fonts.googleapis.com/css?family=Ruda:400,500,600,700,800,900", 1684 | "usage": "all", 1685 | "fallback": "sans-serif" 1686 | }, 1687 | { 1688 | "type": "google", 1689 | "family": "Sansita", 1690 | "weights": [ 1691 | "400", 1692 | "700", 1693 | "800", 1694 | "900" 1695 | ], 1696 | "styles": [ 1697 | { 1698 | "weight": "400" 1699 | }, 1700 | { 1701 | "weight": "700" 1702 | }, 1703 | { 1704 | "weight": "800" 1705 | }, 1706 | { 1707 | "weight": "900" 1708 | } 1709 | ], 1710 | "category": "serif", 1711 | "url": "https://fonts.googleapis.com/css?family=Sansita:400,700,800,900", 1712 | "usage": "headings", 1713 | "fallback": "serif" 1714 | }, 1715 | { 1716 | "type": "google", 1717 | "family": "Sansita Swashed", 1718 | "weights": [ 1719 | "300", 1720 | "400", 1721 | "500", 1722 | "600", 1723 | "700", 1724 | "800", 1725 | "900" 1726 | ], 1727 | "styles": [ 1728 | { 1729 | "weight": "300" 1730 | }, 1731 | { 1732 | "weight": "400" 1733 | }, 1734 | { 1735 | "weight": "500" 1736 | }, 1737 | { 1738 | "weight": "600" 1739 | }, 1740 | { 1741 | "weight": "700" 1742 | }, 1743 | { 1744 | "weight": "800" 1745 | }, 1746 | { 1747 | "weight": "900" 1748 | } 1749 | ], 1750 | "category": "display", 1751 | "url": "https://fonts.googleapis.com/css?family=Sansita+Swashed:300,400,500,600,700,800,900", 1752 | "usage": "h1", 1753 | "fallback": "sans-serif" 1754 | }, 1755 | { 1756 | "type": "google", 1757 | "family": "Sora", 1758 | "weights": [ 1759 | "100", 1760 | "200", 1761 | "300", 1762 | "400", 1763 | "500", 1764 | "600", 1765 | "700", 1766 | "800" 1767 | ], 1768 | "styles": [ 1769 | { 1770 | "weight": "100" 1771 | }, 1772 | { 1773 | "weight": "200" 1774 | }, 1775 | { 1776 | "weight": "300" 1777 | }, 1778 | { 1779 | "weight": "400" 1780 | }, 1781 | { 1782 | "weight": "500" 1783 | }, 1784 | { 1785 | "weight": "600" 1786 | }, 1787 | { 1788 | "weight": "700" 1789 | }, 1790 | { 1791 | "weight": "800" 1792 | } 1793 | ], 1794 | "category": "sans", 1795 | "url": "https://fonts.googleapis.com/css?family=Sora:100,200,300,400,500,600,700,800", 1796 | "usage": "all", 1797 | "fallback": "sans-serif" 1798 | }, 1799 | { 1800 | "type": "google", 1801 | "family": "Space Grotesk", 1802 | "weights": [ 1803 | "300", 1804 | "400", 1805 | "500", 1806 | "600", 1807 | "700" 1808 | ], 1809 | "styles": [ 1810 | { 1811 | "weight": "300" 1812 | }, 1813 | { 1814 | "weight": "400" 1815 | }, 1816 | { 1817 | "weight": "500" 1818 | }, 1819 | { 1820 | "weight": "600" 1821 | }, 1822 | { 1823 | "weight": "700" 1824 | } 1825 | ], 1826 | "category": "sans", 1827 | "url": "https://fonts.googleapis.com/css?family=Space+Grotesk:300,400,500,600,700", 1828 | "usage": "all", 1829 | "fallback": "sans-serif" 1830 | }, 1831 | { 1832 | "type": "google", 1833 | "family": "Spectral", 1834 | "weights": [ 1835 | "200", 1836 | "300", 1837 | "400", 1838 | "500", 1839 | "600", 1840 | "700", 1841 | "800" 1842 | ], 1843 | "styles": [ 1844 | { 1845 | "weight": "200" 1846 | }, 1847 | { 1848 | "weight": "300" 1849 | }, 1850 | { 1851 | "weight": "400" 1852 | }, 1853 | { 1854 | "weight": "500" 1855 | }, 1856 | { 1857 | "weight": "600" 1858 | }, 1859 | { 1860 | "weight": "700" 1861 | }, 1862 | { 1863 | "weight": "800" 1864 | } 1865 | ], 1866 | "category": "serif", 1867 | "url": "https://fonts.googleapis.com/css?family=Spectral:200,300,400,500,600,700,800", 1868 | "usage": "headings", 1869 | "fallback": "serif" 1870 | }, 1871 | { 1872 | "type": "google", 1873 | "family": "Tillana", 1874 | "weights": [ 1875 | "400", 1876 | "500", 1877 | "600", 1878 | "700", 1879 | "800" 1880 | ], 1881 | "styles": [ 1882 | { 1883 | "weight": "400" 1884 | }, 1885 | { 1886 | "weight": "500" 1887 | }, 1888 | { 1889 | "weight": "600" 1890 | }, 1891 | { 1892 | "weight": "700" 1893 | }, 1894 | { 1895 | "weight": "800" 1896 | } 1897 | ], 1898 | "category": "display", 1899 | "url": "https://fonts.googleapis.com/css?family=Tillana:400,500,600,700,800", 1900 | "usage": "headings", 1901 | "fallback": "sans-serif" 1902 | }, 1903 | { 1904 | "type": "google", 1905 | "family": "Titillium Web", 1906 | "weights": [ 1907 | "200", 1908 | "300", 1909 | "400", 1910 | "600", 1911 | "700", 1912 | "900" 1913 | ], 1914 | "styles": [ 1915 | { 1916 | "weight": "200" 1917 | }, 1918 | { 1919 | "weight": "300" 1920 | }, 1921 | { 1922 | "weight": "400" 1923 | }, 1924 | { 1925 | "weight": "600" 1926 | }, 1927 | { 1928 | "weight": "700" 1929 | }, 1930 | { 1931 | "weight": "900" 1932 | } 1933 | ], 1934 | "category": "sans", 1935 | "url": "https://fonts.googleapis.com/css?family=Titillium+Web:200,300,400,600,700,900", 1936 | "usage": "all", 1937 | "fallback": "sans-serif" 1938 | }, 1939 | { 1940 | "type": "google", 1941 | "family": "Trirong", 1942 | "weights": [ 1943 | "100", 1944 | "200", 1945 | "300", 1946 | "400", 1947 | "500", 1948 | "600", 1949 | "700", 1950 | "800", 1951 | "900" 1952 | ], 1953 | "styles": [ 1954 | { 1955 | "weight": "100" 1956 | }, 1957 | { 1958 | "weight": "200" 1959 | }, 1960 | { 1961 | "weight": "300" 1962 | }, 1963 | { 1964 | "weight": "400" 1965 | }, 1966 | { 1967 | "weight": "500" 1968 | }, 1969 | { 1970 | "weight": "600" 1971 | }, 1972 | { 1973 | "weight": "700" 1974 | }, 1975 | { 1976 | "weight": "800" 1977 | }, 1978 | { 1979 | "weight": "900" 1980 | } 1981 | ], 1982 | "category": "serif", 1983 | "url": "https://fonts.googleapis.com/css?family=Trirong:100,200,300,400,500,600,700,800,900", 1984 | "usage": "h1", 1985 | "fallback": "serif" 1986 | }, 1987 | { 1988 | "type": "google", 1989 | "family": "Ubuntu", 1990 | "weights": [ 1991 | "300", 1992 | "400", 1993 | "500", 1994 | "700" 1995 | ], 1996 | "styles": [ 1997 | { 1998 | "weight": "300" 1999 | }, 2000 | { 2001 | "weight": "400" 2002 | }, 2003 | { 2004 | "weight": "500" 2005 | }, 2006 | { 2007 | "weight": "700" 2008 | } 2009 | ], 2010 | "category": "sans", 2011 | "url": "https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700", 2012 | "usage": "all", 2013 | "fallback": "sans-serif" 2014 | }, 2015 | { 2016 | "type": "google", 2017 | "family": "Work Sans", 2018 | "weights": [ 2019 | "100", 2020 | "200", 2021 | "300", 2022 | "400", 2023 | "500", 2024 | "600", 2025 | "700", 2026 | "800", 2027 | "900" 2028 | ], 2029 | "styles": [ 2030 | { 2031 | "weight": "100" 2032 | }, 2033 | { 2034 | "weight": "200" 2035 | }, 2036 | { 2037 | "weight": "300" 2038 | }, 2039 | { 2040 | "weight": "400" 2041 | }, 2042 | { 2043 | "weight": "500" 2044 | }, 2045 | { 2046 | "weight": "600" 2047 | }, 2048 | { 2049 | "weight": "700" 2050 | }, 2051 | { 2052 | "weight": "800" 2053 | }, 2054 | { 2055 | "weight": "900" 2056 | } 2057 | ], 2058 | "category": "sans", 2059 | "url": "https://fonts.googleapis.com/css?family=Work+Sans:100,200,300,400,500,600,700,800,900", 2060 | "usage": "headings", 2061 | "fallback": "sans-serif" 2062 | }, 2063 | { 2064 | "type": "google", 2065 | "family": "Zilla Slab", 2066 | "weights": [ 2067 | "300", 2068 | "400", 2069 | "500", 2070 | "600", 2071 | "700" 2072 | ], 2073 | "styles": [ 2074 | { 2075 | "weight": "300" 2076 | }, 2077 | { 2078 | "weight": "400" 2079 | }, 2080 | { 2081 | "weight": "500" 2082 | }, 2083 | { 2084 | "weight": "600" 2085 | }, 2086 | { 2087 | "weight": "700" 2088 | } 2089 | ], 2090 | "category": "serif", 2091 | "url": "https://fonts.googleapis.com/css?family=Zilla+Slab:300,400,500,600,700", 2092 | "usage": "all", 2093 | "fallback": "serif" 2094 | } 2095 | ] 2096 | } -------------------------------------------------------------------------------- /src/main/resources/public/assets/identity/identity.js: -------------------------------------------------------------------------------- 1 | const SUPABASE_URL = '@supabase.url@'; 2 | const SUPABASE_ANON_KEY = '@supabase.anon.key@'; 3 | 4 | window.userToken = null; 5 | window.isSignUpPage = false; 6 | const urlParams = new URLSearchParams(window.location.search); 7 | 8 | var supabase = supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); 9 | supabase.auth.onAuthStateChange((event, session) => { 10 | //console.log(event, session); 11 | setToken(session); 12 | if (isSignUpPage) { 13 | if (session.access_token) { 14 | window.location.replace("@supabase.server@"); 15 | } 16 | } 17 | 18 | if (event === 'PASSWORD_RECOVERY') 19 | window.location.replace("@supabase.server@public/update-password"); 20 | }) 21 | 22 | document.addEventListener('DOMContentLoaded', function (event) { 23 | const logoutButton = document.querySelector('#logout-button'); 24 | if (logoutButton) 25 | logoutButton.onclick = logoutSubmitted.bind(logoutButton) 26 | 27 | if (urlParams.get('error_description')) 28 | displayError(urlParams.get('error_description')); 29 | }) 30 | 31 | const logoutSubmitted = (event) => { 32 | event.preventDefault() 33 | supabase.auth.signOut(); 34 | } 35 | 36 | function displayError(errorMessage) { 37 | const toastElement = document.getElementById('toast'); 38 | const toastBody = document.getElementById('toast-body') 39 | if (!toastElement || !toastBody) { 40 | console.log("Can't find toast or toast-body"); 41 | console.log(errorMessage); 42 | alert(errorMessage); 43 | return; 44 | } 45 | 46 | toastBody.innerHTML = errorMessage; 47 | const toast = new bootstrap.Toast(toastElement) 48 | toast.show() 49 | } 50 | 51 | function updateCookies() { 52 | if (!supabase || !supabase.auth || !supabase.auth.session()) { 53 | document.cookie = "access-token=null;max-age=0;path=/"; 54 | return; 55 | } 56 | 57 | if (localStorage.getItem("remember-me") === 'true') 58 | document.cookie = "access-token=" + supabase.auth.session().access_token + ";max-age=2147483640;path=/"; 59 | else 60 | document.cookie = "access-token=" + supabase.auth.session().access_token + ";path=/"; 61 | } 62 | 63 | function setToken(response) { 64 | if (!response || !response.user) { 65 | document.cookie = "access-token=null;max-age=0;path=/"; 66 | return; 67 | } 68 | 69 | if (response.user.confirmation_sent_at && !response.access_token) { 70 | displayError('Confirmation Email Sent') 71 | } else { 72 | updateCookies(response.access_token); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/resources/public/assets/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/resources/public/assets/index.css -------------------------------------------------------------------------------- /src/main/resources/public/assets/js/popper.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @popperjs/core v2.5.4 - MIT License 3 | */ 4 | 5 | "use strict";!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).Popper={})}(this,(function(e){function t(e){return{width:(e=e.getBoundingClientRect()).width,height:e.height,top:e.top,right:e.right,bottom:e.bottom,left:e.left,x:e.left,y:e.top}}function n(e){return"[object Window]"!==e.toString()?(e=e.ownerDocument)&&e.defaultView||window:e}function r(e){return{scrollLeft:(e=n(e)).pageXOffset,scrollTop:e.pageYOffset}}function o(e){return e instanceof n(e).Element||e instanceof Element}function i(e){return e instanceof n(e).HTMLElement||e instanceof HTMLElement}function a(e){return e?(e.nodeName||"").toLowerCase():null}function s(e){return((o(e)?e.ownerDocument:e.document)||window.document).documentElement}function f(e){return t(s(e)).left+r(e).scrollLeft}function c(e){return n(e).getComputedStyle(e)}function p(e){return e=c(e),/auto|scroll|overlay|hidden/.test(e.overflow+e.overflowY+e.overflowX)}function l(e,o,c){void 0===c&&(c=!1);var l=s(o);e=t(e);var u=i(o),d={scrollLeft:0,scrollTop:0},m={x:0,y:0};return(u||!u&&!c)&&(("body"!==a(o)||p(l))&&(d=o!==n(o)&&i(o)?{scrollLeft:o.scrollLeft,scrollTop:o.scrollTop}:r(o)),i(o)?((m=t(o)).x+=o.clientLeft,m.y+=o.clientTop):l&&(m.x=f(l))),{x:e.left+d.scrollLeft-m.x,y:e.top+d.scrollTop-m.y,width:e.width,height:e.height}}function u(e){return{x:e.offsetLeft,y:e.offsetTop,width:e.offsetWidth,height:e.offsetHeight}}function d(e){return"html"===a(e)?e:e.assignedSlot||e.parentNode||e.host||s(e)}function m(e,t){void 0===t&&(t=[]);var r=function e(t){return 0<=["html","body","#document"].indexOf(a(t))?t.ownerDocument.body:i(t)&&p(t)?t:e(d(t))}(e);e="body"===a(r);var o=n(r);return r=e?[o].concat(o.visualViewport||[],p(r)?r:[]):r,t=t.concat(r),e?t:t.concat(m(d(r)))}function h(e){if(!i(e)||"fixed"===c(e).position)return null;if(e=e.offsetParent){var t=s(e);if("body"===a(e)&&"static"===c(e).position&&"static"!==c(t).position)return t}return e}function g(e){for(var t=n(e),r=h(e);r&&0<=["table","td","th"].indexOf(a(r))&&"static"===c(r).position;)r=h(r);if(r&&"body"===a(r)&&"static"===c(r).position)return t;if(!r)e:{for(e=d(e);i(e)&&0>["html","body"].indexOf(a(e));){if("none"!==(r=c(e)).transform||"none"!==r.perspective||r.willChange&&"auto"!==r.willChange){r=e;break e}e=e.parentNode}r=null}return r||t}function v(e){var t=new Map,n=new Set,r=[];return e.forEach((function(e){t.set(e.name,e)})),e.forEach((function(e){n.has(e.name)||function e(o){n.add(o.name),[].concat(o.requires||[],o.requiresIfExists||[]).forEach((function(r){n.has(r)||(r=t.get(r))&&e(r)})),r.push(o)}(e)})),r}function b(e){var t;return function(){return t||(t=new Promise((function(n){Promise.resolve().then((function(){t=void 0,n(e())}))}))),t}}function y(e){return e.split("-")[0]}function O(e,t){var r,o=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if((r=o)&&(r=o instanceof(r=n(o).ShadowRoot)||o instanceof ShadowRoot),r)do{if(t&&e.isSameNode(t))return!0;t=t.parentNode||t.host}while(t);return!1}function w(e){return Object.assign(Object.assign({},e),{},{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function x(e,o){if("viewport"===o){o=n(e);var a=s(e);o=o.visualViewport;var p=a.clientWidth;a=a.clientHeight;var l=0,u=0;o&&(p=o.width,a=o.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(l=o.offsetLeft,u=o.offsetTop)),e=w(e={width:p,height:a,x:l+f(e),y:u})}else i(o)?((e=t(o)).top+=o.clientTop,e.left+=o.clientLeft,e.bottom=e.top+o.clientHeight,e.right=e.left+o.clientWidth,e.width=o.clientWidth,e.height=o.clientHeight,e.x=e.left,e.y=e.top):(u=s(e),e=s(u),l=r(u),o=u.ownerDocument.body,p=Math.max(e.scrollWidth,e.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),a=Math.max(e.scrollHeight,e.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),u=-l.scrollLeft+f(u),l=-l.scrollTop,"rtl"===c(o||e).direction&&(u+=Math.max(e.clientWidth,o?o.clientWidth:0)-p),e=w({width:p,height:a,x:u,y:l}));return e}function j(e,t,n){return t="clippingParents"===t?function(e){var t=m(d(e)),n=0<=["absolute","fixed"].indexOf(c(e).position)&&i(e)?g(e):e;return o(n)?t.filter((function(e){return o(e)&&O(e,n)&&"body"!==a(e)})):[]}(e):[].concat(t),(n=(n=[].concat(t,[n])).reduce((function(t,n){return n=x(e,n),t.top=Math.max(n.top,t.top),t.right=Math.min(n.right,t.right),t.bottom=Math.min(n.bottom,t.bottom),t.left=Math.max(n.left,t.left),t}),x(e,n[0]))).width=n.right-n.left,n.height=n.bottom-n.top,n.x=n.left,n.y=n.top,n}function M(e){return 0<=["top","bottom"].indexOf(e)?"x":"y"}function E(e){var t=e.reference,n=e.element,r=(e=e.placement)?y(e):null;e=e?e.split("-")[1]:null;var o=t.x+t.width/2-n.width/2,i=t.y+t.height/2-n.height/2;switch(r){case"top":o={x:o,y:t.y-n.height};break;case"bottom":o={x:o,y:t.y+t.height};break;case"right":o={x:t.x+t.width,y:i};break;case"left":o={x:t.x-n.width,y:i};break;default:o={x:t.x,y:t.y}}if(null!=(r=r?M(r):null))switch(i="y"===r?"height":"width",e){case"start":o[r]=Math.floor(o[r])-Math.floor(t[i]/2-n[i]/2);break;case"end":o[r]=Math.floor(o[r])+Math.ceil(t[i]/2-n[i]/2)}return o}function D(e){return Object.assign(Object.assign({},{top:0,right:0,bottom:0,left:0}),e)}function P(e,t){return t.reduce((function(t,n){return t[n]=e,t}),{})}function L(e,n){void 0===n&&(n={});var r=n;n=void 0===(n=r.placement)?e.placement:n;var i=r.boundary,a=void 0===i?"clippingParents":i,f=void 0===(i=r.rootBoundary)?"viewport":i;i=void 0===(i=r.elementContext)?"popper":i;var c=r.altBoundary,p=void 0!==c&&c;r=D("number"!=typeof(r=void 0===(r=r.padding)?0:r)?r:P(r,T));var l=e.elements.reference;c=e.rects.popper,a=j(o(p=e.elements[p?"popper"===i?"reference":"popper":i])?p:p.contextElement||s(e.elements.popper),a,f),p=E({reference:f=t(l),element:c,strategy:"absolute",placement:n}),c=w(Object.assign(Object.assign({},c),p)),f="popper"===i?c:f;var u={top:a.top-f.top+r.top,bottom:f.bottom-a.bottom+r.bottom,left:a.left-f.left+r.left,right:f.right-a.right+r.right};if(e=e.modifiersData.offset,"popper"===i&&e){var d=e[n];Object.keys(u).forEach((function(e){var t=0<=["right","bottom"].indexOf(e)?1:-1,n=0<=["top","bottom"].indexOf(e)?"y":"x";u[e]+=d[n]*t}))}return u}function k(){for(var e=arguments.length,t=Array(e),n=0;n(v.devicePixelRatio||1)?"translate("+e+"px, "+l+"px)":"translate3d("+e+"px, "+l+"px, 0)",d)):Object.assign(Object.assign({},r),{},((t={})[h]=a?l+"px":"",t[m]=u?e+"px":"",t.transform="",t))}function A(e){return e.replace(/left|right|bottom|top/g,(function(e){return G[e]}))}function H(e){return e.replace(/start|end/g,(function(e){return J[e]}))}function R(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function S(e){return["top","right","bottom","left"].some((function(t){return 0<=e[t]}))}var T=["top","bottom","right","left"],q=T.reduce((function(e,t){return e.concat([t+"-start",t+"-end"])}),[]),C=[].concat(T,["auto"]).reduce((function(e,t){return e.concat([t,t+"-start",t+"-end"])}),[]),N="beforeRead read afterRead beforeMain main afterMain beforeWrite write afterWrite".split(" "),V={placement:"bottom",modifiers:[],strategy:"absolute"},I={passive:!0},_={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(e){var t=e.state,r=e.instance,o=(e=e.options).scroll,i=void 0===o||o,a=void 0===(e=e.resize)||e,s=n(t.elements.popper),f=[].concat(t.scrollParents.reference,t.scrollParents.popper);return i&&f.forEach((function(e){e.addEventListener("scroll",r.update,I)})),a&&s.addEventListener("resize",r.update,I),function(){i&&f.forEach((function(e){e.removeEventListener("scroll",r.update,I)})),a&&s.removeEventListener("resize",r.update,I)}},data:{}},U={name:"popperOffsets",enabled:!0,phase:"read",fn:function(e){var t=e.state;t.modifiersData[e.name]=E({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})},data:{}},z={top:"auto",right:"auto",bottom:"auto",left:"auto"},F={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(e){var t=e.state,n=e.options;e=void 0===(e=n.gpuAcceleration)||e,n=void 0===(n=n.adaptive)||n,e={placement:y(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:e},null!=t.modifiersData.popperOffsets&&(t.styles.popper=Object.assign(Object.assign({},t.styles.popper),W(Object.assign(Object.assign({},e),{},{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:n})))),null!=t.modifiersData.arrow&&(t.styles.arrow=Object.assign(Object.assign({},t.styles.arrow),W(Object.assign(Object.assign({},e),{},{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1})))),t.attributes.popper=Object.assign(Object.assign({},t.attributes.popper),{},{"data-popper-placement":t.placement})},data:{}},X={name:"applyStyles",enabled:!0,phase:"write",fn:function(e){var t=e.state;Object.keys(t.elements).forEach((function(e){var n=t.styles[e]||{},r=t.attributes[e]||{},o=t.elements[e];i(o)&&a(o)&&(Object.assign(o.style,n),Object.keys(r).forEach((function(e){var t=r[e];!1===t?o.removeAttribute(e):o.setAttribute(e,!0===t?"":t)})))}))},effect:function(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach((function(e){var r=t.elements[e],o=t.attributes[e]||{};e=Object.keys(t.styles.hasOwnProperty(e)?t.styles[e]:n[e]).reduce((function(e,t){return e[t]="",e}),{}),i(r)&&a(r)&&(Object.assign(r.style,e),Object.keys(o).forEach((function(e){r.removeAttribute(e)})))}))}},requires:["computeStyles"]},Y={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(e){var t=e.state,n=e.name,r=void 0===(e=e.options.offset)?[0,0]:e,o=(e=C.reduce((function(e,n){var o=t.rects,i=y(n),a=0<=["left","top"].indexOf(i)?-1:1,s="function"==typeof r?r(Object.assign(Object.assign({},o),{},{placement:n})):r;return o=(o=s[0])||0,s=((s=s[1])||0)*a,i=0<=["left","right"].indexOf(i)?{x:s,y:o}:{x:o,y:s},e[n]=i,e}),{}))[t.placement],i=o.x;o=o.y,null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=i,t.modifiersData.popperOffsets.y+=o),t.modifiersData[n]=e}},G={left:"right",right:"left",bottom:"top",top:"bottom"},J={start:"end",end:"start"},K={name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options;if(e=e.name,!t.modifiersData[e]._skip){var r=n.mainAxis;r=void 0===r||r;var o=n.altAxis;o=void 0===o||o;var i=n.fallbackPlacements,a=n.padding,s=n.boundary,f=n.rootBoundary,c=n.altBoundary,p=n.flipVariations,l=void 0===p||p,u=n.allowedAutoPlacements;p=y(n=t.options.placement),i=i||(p!==n&&l?function(e){if("auto"===y(e))return[];var t=A(e);return[H(e),t,H(t)]}(n):[A(n)]);var d=[n].concat(i).reduce((function(e,n){return e.concat("auto"===y(n)?function(e,t){void 0===t&&(t={});var n=t.boundary,r=t.rootBoundary,o=t.padding,i=t.flipVariations,a=t.allowedAutoPlacements,s=void 0===a?C:a,f=t.placement.split("-")[1];0===(i=(t=f?i?q:q.filter((function(e){return e.split("-")[1]===f})):T).filter((function(e){return 0<=s.indexOf(e)}))).length&&(i=t);var c=i.reduce((function(t,i){return t[i]=L(e,{placement:i,boundary:n,rootBoundary:r,padding:o})[y(i)],t}),{});return Object.keys(c).sort((function(e,t){return c[e]-c[t]}))}(t,{placement:n,boundary:s,rootBoundary:f,padding:a,flipVariations:l,allowedAutoPlacements:u}):n)}),[]);n=t.rects.reference,i=t.rects.popper;var m=new Map;p=!0;for(var h=d[0],g=0;gi[x]&&(O=A(O)),x=A(O),w=[],r&&w.push(0>=j[b]),o&&w.push(0>=j[O],0>=j[x]),w.every((function(e){return e}))){h=v,p=!1;break}m.set(v,w)}if(p)for(r=function(e){var t=d.find((function(t){if(t=m.get(t))return t.slice(0,e).every((function(e){return e}))}));if(t)return h=t,"break"},o=l?3:1;0 3 | 4 | 5 | 7 | 9 | 10 | 11 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/public/assets/svg/logo-email.svg: -------------------------------------------------------------------------------- 1 | 2 | Email 3 | -------------------------------------------------------------------------------- /src/main/resources/public/assets/svg/logo-github.svg: -------------------------------------------------------------------------------- 1 | 2 | Logo Github 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/public/assets/svg/logo-twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | Logo Twitter 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/public/assets/svg/person-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | Person 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/public/blocks.css: -------------------------------------------------------------------------------- 1 | .proportions-box-square { 2 | position: relative; 3 | padding: 100% 0 0 !important; 4 | height: 0; 5 | } 6 | 7 | .proportions-box-content { 8 | width: 100%; 9 | height: 100%; 10 | top: 0; 11 | left: 0; 12 | position: absolute; 13 | } 14 | 15 | .background-cover { 16 | background-size: cover; 17 | background-repeat: no-repeat; 18 | background-position: center center; 19 | } 20 | 21 | .background-left-top { 22 | background-position: left top; 23 | } 24 | 25 | .background-left-center { 26 | background-position: left center; 27 | } 28 | 29 | .background-left-bottom { 30 | background-position: left bottom; 31 | } 32 | 33 | .background-right-top { 34 | background-position: right top; 35 | } 36 | 37 | .background-right-center { 38 | background-position: right center; 39 | } 40 | 41 | .background-right-bottom { 42 | background-position: right bottom; 43 | } 44 | 45 | .background-center-top { 46 | background-position: center top; 47 | } 48 | 49 | .background-center-center { 50 | background-position: center center; 51 | } 52 | 53 | .background-center-bottom { 54 | background-position: center bottom; 55 | } 56 | 57 | .border-width-1 { 58 | border-width: 1px !important; 59 | } 60 | 61 | .border-width-2 { 62 | border-width: 2px !important; 63 | } 64 | 65 | .border-width-3 { 66 | border-width: 3px !important; 67 | } 68 | 69 | .border-width-4 { 70 | border-width: 4px !important; 71 | } 72 | 73 | .border-width-6 { 74 | border-width: 6px !important; 75 | } 76 | 77 | .border-width-8 { 78 | border-width: 8px !important; 79 | } 80 | 81 | .border-width-12 { 82 | border-width: 12px !important; 83 | } 84 | 85 | .border-width-16 { 86 | border-width: 16px !important; 87 | } 88 | 89 | .border-width-24 { 90 | border-width: 24px !important; 91 | } 92 | 93 | .image-fit { 94 | object-fit: contain; 95 | object-position: center center; 96 | } 97 | 98 | .image-fit-left-top { 99 | object-position: left top; 100 | } 101 | 102 | .image-fit-left-center { 103 | object-position: left center; 104 | } 105 | 106 | .image-fit-left-bottom { 107 | object-position: left bottom; 108 | } 109 | 110 | .image-fit-right-top { 111 | object-position: right top; 112 | } 113 | 114 | .image-fit-right-center { 115 | object-position: right center; 116 | } 117 | 118 | .image-fit-right-bottom { 119 | object-position: right bottom; 120 | } 121 | 122 | .image-fit-center-top { 123 | object-position: center top; 124 | } 125 | 126 | .image-fit-center-center { 127 | object-position: center center; 128 | } 129 | 130 | .image-fit-center-bottom { 131 | object-position: center bottom; 132 | } 133 | 134 | .transition { 135 | transition: all 0.25s ease-in; 136 | } 137 | 138 | .transition-slow { 139 | transition: all 0.5s ease-in; 140 | } 141 | 142 | .transition-very-slow { 143 | transition: all 1s ease-in; 144 | } 145 | 146 | .line-height-1 { 147 | line-height: 1 !important; 148 | } 149 | 150 | .opacity-100 { 151 | opacity: 1 !important; 152 | } 153 | 154 | .opacity-85 { 155 | opacity: 0.85 !important; 156 | } 157 | 158 | .opacity-75 { 159 | opacity: 0.75 !important; 160 | } 161 | 162 | .opacity-50 { 163 | opacity: 0.50 !important; 164 | } 165 | 166 | .opacity-25 { 167 | opacity: 0.25 !important; 168 | } 169 | 170 | .opacity-0 { 171 | opacity: 0 !important; 172 | } -------------------------------------------------------------------------------- /src/main/resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/resources/public/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/resources/public/favicon.png -------------------------------------------------------------------------------- /src/main/resources/public/identity/create-account.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | Create Account 10 | 53 | 54 | 55 |
56 |
57 |
58 |

Create Account

59 |
60 |

Already logged in as username

61 |

Sign Out

62 |
63 |
64 |
65 | 68 | 71 |
72 |
73 |
74 |

- or -

75 |
76 |
77 |
78 |
79 |
80 | 82 | 83 |
84 |
85 | 87 | 88 |
89 |
90 | 92 | 93 |
94 |
95 | 96 |
97 |
98 |
99 |
100 |
101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/main/resources/public/identity/forgot-password.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 38 | 39 | 40 |
41 |
42 |
43 |

Forgot Password?

44 |
45 |

Already logged in as username

46 |

Sign Out

47 |
48 |
49 |
50 |
51 |
52 | 54 | 55 |
56 |
57 | 59 |
60 |
61 | Sign Up | 62 | Sign In 63 |
64 |
65 |
66 |
67 |
68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/resources/public/identity/sign-in.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | Sign In 6 | 7 | 8 | 9 | 10 | 99 | 100 | 101 |
102 |
103 |
104 |

Sign In

105 |
106 |

Already logged in as username

107 |

Sign Out

108 |
109 |
110 | 111 |
112 |
113 |
114 |

Redirecting...

115 |
116 |
117 |
118 | 119 |
120 |
121 | 124 | 127 |
128 |
129 |
130 |

- or -

131 |
132 |
133 |
134 |
135 |
136 | 139 | 140 |
141 |
142 | 145 | 146 |
147 |
148 |
149 | 150 | 151 |
152 |
153 |
154 | 157 |
158 | 163 |
164 |
165 |
166 |
167 |
168 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /src/main/resources/public/identity/sign-out.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | Sign Out 10 | 11 | 16 | 17 | 18 |
19 |
20 |
21 |

Bye!

22 |
23 |
24 |

Sign In

25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/public/identity/update-password.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 45 | 46 | 47 |
48 |
49 | 50 |
51 |

Error

52 |
53 | 54 |
55 |

Not Logged In

56 |

Your Token Might Be Expired

57 |
58 | 59 |
61 |

Update Password

62 |
63 |
64 | 66 | 67 |
68 |
69 | 71 | 72 |
73 |
74 | 75 |
76 |
77 |
78 |
79 |
80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/main/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Home 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 | User Name 17 |
18 |
Sign Out 19 |
Profile 20 |
21 | 23 |

Welcome!

24 |
25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/resources/public/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Company 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 27 |
28 | 29 |
30 | Loading Indicator 31 |
32 | 47 |
48 |

Page content goes here

49 |
50 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/resources/public/pinegrow.json: -------------------------------------------------------------------------------- 1 | {"files":{"index-pg.html":{"frameworks":["public","pg.insight.events","pg.svg.lib","pg.css.grid","pg.image.overlay","pg.code-validator","pg.project.items","pg.asset.manager","pg.bs5.lib","bs5","pg.html","pg.components"],"last_page_width":1024},"index.html":{"frameworks":["public","pg.bs5.lib","pg.asset.manager","pg.insight.events","pg.project.items","pg.code-validator","pg.image.overlay","pg.svg.lib","pg.css.grid","bs5","pg.html","pg.components"],"last_page_width":768},"layout.html":{"frameworks":["public","pg.bs5.lib","pg.asset.manager","pg.project.items","pg.insight.events","pg.code-validator","pg.image.overlay","pg.css.grid","pg.svg.lib","bs5","pg.html","pg.components"],"last_page_width":640},"identity/sign-in.html":{"frameworks":["public","pg.bs5.lib","pg.insight.events","pg.asset.manager","pg.project.items","pg.code-validator","pg.image.overlay","pg.css.grid","pg.svg.lib","bs5","pg.html","pg.components"],"last_page_width":853},"identity/sign-out.html":{"frameworks":["public","pg.bs5.lib","pg.insight.events","pg.asset.manager","pg.project.items","pg.code-validator","pg.svg.lib","pg.image.overlay","pg.css.grid","bs5","pg.html","pg.components"],"last_page_width":1024},"identity/sign-up.html":{"frameworks":["public","pg.bs5.lib","pg.insight.events","pg.asset.manager","pg.project.items","pg.code-validator","pg.image.overlay","pg.svg.lib","pg.css.grid","bs5","pg.html","pg.components"],"last_page_width":783},"secure.html":{"frameworks":["public","pg.insight.events","pg.asset.manager","pg.svg.lib","pg.project.items","pg.css.grid","pg.code-validator","pg.image.overlay","pg.html","pg.components"],"last_page_width":768},"identity/forgot-password.html":{"frameworks":["public","pg.bs5.lib","pg.insight.events","pg.asset.manager","pg.project.items","pg.code-validator","pg.image.overlay","pg.css.grid","pg.svg.lib","bs5","pg.html","pg.components"],"last_page_width":1024}},"breakpoints":["576px","768px","992px","1200px","1400px"],"frameworks":["public","pg.bs5.lib","pg.asset.manager","pg.project.items","pg.insight.events","pg.code-validator","pg.image.overlay","pg.css.grid","pg.svg.lib","bs5","pg.html","pg.components"],"template_framework_id":"pg.bs5.lib","urls":{"index-pg.html":{"open-page-views":[{"w":1024,"h":0}]},"index.html":{"open-page-views":[{"w":768,"h":0}]},"layout.html":{"open-page-views":[{"w":640,"h":0}]},"identity/sign-in.html":{"open-page-views":[{"w":853,"h":0}]},"identity/sign-out.html":{"open-page-views":[{"w":1024,"h":0}]},"identity/sign-up.html":{"open-page-views":[{"w":783,"h":0}]},"secure.html":{"open-page-views":[{"w":320,"h":0}]},"blocks.css":{"locked":true,"locked_reason":"This stylesheet is a library resource and will be overwritten on update. Over-ride CSS rules in the your project's stylesheet."},"identity/forgot-password.html":{"open-page-views":[{"w":1024,"h":0}]},"identity/create-account.html":{"open-page-views":[{"w":320,"h":0}]}},"active-design-provider":"bs5"} -------------------------------------------------------------------------------- /src/main/resources/public/secure.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | Profile 10 | 11 | 12 |
13 |
14 |
15 |

User Details

16 |
17 | 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 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
User PropertyValue
Avatar URL
Confirmed At
Created At
Email
Email Confirmed At
Full Name
User ID
Last Sign In At
Name
Phone
Provider
Role
Updated At
Enabled
86 |
87 |
88 |
89 | 90 | 91 | -------------------------------------------------------------------------------- /src/main/resources/public/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/resources/public/style.css -------------------------------------------------------------------------------- /src/main/site/create-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/site/create-account.png -------------------------------------------------------------------------------- /src/main/site/forgot-password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/site/forgot-password.png -------------------------------------------------------------------------------- /src/main/site/home-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/site/home-screen.png -------------------------------------------------------------------------------- /src/main/site/intellij-web-module-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/site/intellij-web-module-setup.png -------------------------------------------------------------------------------- /src/main/site/logged-in-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/site/logged-in-user.png -------------------------------------------------------------------------------- /src/main/site/pinegrow-create-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/site/pinegrow-create-account.png -------------------------------------------------------------------------------- /src/main/site/pinegrow-master-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/site/pinegrow-master-layout.png -------------------------------------------------------------------------------- /src/main/site/pinegrow-mobile-bootstrap-blocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/site/pinegrow-mobile-bootstrap-blocks.png -------------------------------------------------------------------------------- /src/main/site/sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChangeNode/spring-boot-supabase/25ef39207861f378e625f18466ac89fd151ef104/src/main/site/sign-in.png -------------------------------------------------------------------------------- /src/main/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | org.apache.maven.skins 6 | maven-fluido-skin 7 | 1.9 8 | 9 | 10 | 11 | 12 | true 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.apache.maven.skins 5 | maven-fluido-skin 6 | 1.10.0 7 | 8 | 9 | 10 | 11 | true 12 | true 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/test/java/com/changenode/frisson/DataAccessTests.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson; 2 | 3 | import com.changenode.frisson.data.ToDo; 4 | import com.changenode.frisson.query.TodosEntityQuery; 5 | import com.changenode.frisson.query.UserQuery; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | 11 | import java.time.Instant; 12 | import java.util.UUID; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) 17 | @AutoConfigureMockMvc(addFilters = false) 18 | class DataAccessTests { 19 | 20 | @Autowired 21 | TodosEntityQuery todosEntityQuery; 22 | 23 | @Autowired 24 | UserQuery userQuery; 25 | 26 | @Test 27 | public void insertAndDelete() { 28 | ToDo toDo = new ToDo(); 29 | toDo.setTask("Hello"); 30 | toDo.setInsertedAt(Instant.now()); 31 | toDo.setUserId(UUID.fromString("517d99fe-db7d-467e-b3f2-be5ac8c41752")); 32 | todosEntityQuery.save(toDo); 33 | assertThat(toDo.getId()).isNotNull(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/changenode/frisson/JwtDecoderTest.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson; 2 | 3 | import com.changenode.frisson.websecurity.SupabaseUser; 4 | import io.jsonwebtoken.Claims; 5 | import io.jsonwebtoken.JwsHeader; 6 | import io.jsonwebtoken.Jwt; 7 | import io.jsonwebtoken.Jwts; 8 | import org.junit.jupiter.api.Disabled; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.time.Instant; 12 | import java.util.Base64; 13 | import java.util.Date; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | public class JwtDecoderTest { 18 | 19 | String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjMwMDI2NDQ1LCJzdWIiOiI1MTdkOTlmZS1kYjdkLTQ2N2UtYjNmMi1iZTVhYzhjNDE3NTIiLCJlbWFpbCI6IndpdmVyc29uQGdtYWlsLmNvbSIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZ2l0aHViIn0sInVzZXJfbWV0YWRhdGEiOnsiYXZhdGFyX3VybCI6Imh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS84NzM4NTA_dj00IiwiZnVsbF9uYW1lIjoiV2lsbCBJdmVyc29uIiwidXNlcl9uYW1lIjoid2l2ZXJzb24ifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQifQ.5-gknHOAKifeEcCjqWNI8zF7ypQi3MkMDLE6W2qyM_A"; 20 | 21 | String secret = "cb36e3af-0bfb-4994-a735-65d95c0e40aa"; 22 | 23 | @Test 24 | @Disabled("Requires the JWT to be manually updated. :P") 25 | public void jtwDecodeAndVerify() { 26 | String encoded = Base64.getEncoder().encodeToString(secret.getBytes()); 27 | 28 | Jwt parse = Jwts.parserBuilder().setSigningKey(encoded). 29 | requireAudience("authenticated"). 30 | build().parseClaimsJws(jwt); 31 | 32 | JwsHeader header = parse.getHeader(); 33 | for (Object key : header.keySet()) { 34 | System.out.println(key + ":" + header.get(key)); 35 | } 36 | 37 | Claims body = parse.getBody(); 38 | 39 | if (body.getExpiration().before(Date.from(Instant.now()))) 40 | throw new IllegalArgumentException("JWT expired"); 41 | 42 | SupabaseUser user = new SupabaseUser(body, jwt); 43 | 44 | assertThat(user.getId()).isEqualTo(body.get("sub", String.class)); 45 | 46 | System.out.println(body); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/changenode/frisson/SupabaseDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @SpringBootTest 8 | class SupabaseDemoApplicationTests { 9 | 10 | @Test 11 | void contextLoads() { 12 | } 13 | 14 | /* Reduce Spring build noise */ 15 | @Configuration 16 | class Config { 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/changenode/frisson/TestSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.changenode.frisson; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.context.annotation.Profile; 5 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 | 8 | @Profile("test") 9 | @Configuration 10 | public class TestSecurityConfiguration extends WebSecurityConfigurerAdapter { 11 | @Override 12 | public void configure(WebSecurity web) throws Exception { 13 | web.ignoring().antMatchers("/**"); 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/test/resources/test-logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | --------------------------------------------------------------------------------