diff --git a/.gitignore b/.gitignore index 0397954..54ed519 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,5 @@ backend/config.json /bin/ /.shards/ *.dwarf +Makefile +.vscode/ diff --git a/backend/config.json.example b/backend/config.json.example new file mode 100644 index 0000000..4999981 --- /dev/null +++ b/backend/config.json.example @@ -0,0 +1,9 @@ +{ + "secret": "TEST_SECRET", + "port": 8080, + "db_url": "postgres://postgres:@127.0.0.1/todo", + "mail_host": "smtp.migadu.com", + "mail_port": 465, + "mail_username": "", + "mail_password": "" +} \ No newline at end of file diff --git a/backend/config/config.cr b/backend/config/config.cr new file mode 100644 index 0000000..c7dcc81 --- /dev/null +++ b/backend/config/config.cr @@ -0,0 +1,2 @@ +require "./initializers/**" +require "../src/models/*" diff --git a/backend/config/database.yml b/backend/config/database.yml new file mode 100644 index 0000000..c81bab7 --- /dev/null +++ b/backend/config/database.yml @@ -0,0 +1,16 @@ +default: &default + host: localhost + user: postgres + adapter: postgres + +development: + <<: *default + db: todo_dev + +test: + <<: *default + db: todo_dev + +production: + <<: *default + db: todo \ No newline at end of file diff --git a/backend/config/initializers/database.cr b/backend/config/initializers/database.cr new file mode 100644 index 0000000..519a926 --- /dev/null +++ b/backend/config/initializers/database.cr @@ -0,0 +1,12 @@ +require "jennifer" +require "jennifer/adapter/postgres" + +APP_ENV = ENV["APP_ENV"]? || "development" + +Jennifer::Config.configure do |conf| + conf.read("config/database.yml", APP_ENV) + conf.from_uri(ENV["DATABASE_URI"]) if ENV.has_key?("DATABASE_URI") + conf.logger.level = APP_ENV == "development" ? Log::Severity::Debug : Log::Severity::Info +end + +Log.setup "db", :debug, Log::IOBackend.new(formatter: Jennifer::Adapter::DBFormatter) diff --git a/backend/config/initializers/zzz_i18n.cr b/backend/config/initializers/zzz_i18n.cr new file mode 100644 index 0000000..26e3268 --- /dev/null +++ b/backend/config/initializers/zzz_i18n.cr @@ -0,0 +1,3 @@ +I18n.load_path += ["./config/locales"] + +I18n.init diff --git a/backend/config/locales/en.yml b/backend/config/locales/en.yml new file mode 100644 index 0000000..e69de29 diff --git a/backend/db/migrations/20210723170518920_create_users.cr b/backend/db/migrations/20210723170518920_create_users.cr new file mode 100644 index 0000000..44238f0 --- /dev/null +++ b/backend/db/migrations/20210723170518920_create_users.cr @@ -0,0 +1,19 @@ +require "jennifer" + +class CreateUsers < Jennifer::Migration::Base + def up + create_table :users do |t| + t.string :id, {:primary => true} + t.string :email + t.bool :discord_only_account + t.string :discord_id, {:null => true} + t.string :password_hash, {:null => true} + + t.timestamps + end + end + + def down + drop_table :users if table_exists? :users + end +end diff --git a/backend/db/migrations/20210723171718613_create_unverifiedusers.cr b/backend/db/migrations/20210723171718613_create_unverifiedusers.cr new file mode 100644 index 0000000..d9628f3 --- /dev/null +++ b/backend/db/migrations/20210723171718613_create_unverifiedusers.cr @@ -0,0 +1,20 @@ +require "jennifer" + +class CreateUnverifiedusers < Jennifer::Migration::Base + def up + create_table :unverifiedusers do |t| + t.string :id, {:primary => true} + t.string :email + t.bool :discord_only_account + t.string :discord_id, {:null => true} + t.string :password_hash, {:null => true} + t.string :verification_token + + t.timestamps + end + end + + def down + drop_table :unverifiedusers if table_exists? :unverifiedusers + end +end diff --git a/backend/db/migrations/20210723171731912_create_todos.cr b/backend/db/migrations/20210723171731912_create_todos.cr new file mode 100644 index 0000000..f020265 --- /dev/null +++ b/backend/db/migrations/20210723171731912_create_todos.cr @@ -0,0 +1,20 @@ +require "jennifer" + +class CreateTodos < Jennifer::Migration::Base + def up + create_table :todos do |t| + t.string :id, {:primary => true} + t.string :userid # remember: trying to insert a column named the same as + # another model will make the database error :) + t.text :content + t.string :tags, {:array => true, :null => true} + t.bool :complete, {:null => true} + t.date_time :deadline, {:null => true} + t.timestamps + end + end + + def down + drop_table :todos if table_exists? :todos + end +end diff --git a/backend/db/structure.sql b/backend/db/structure.sql new file mode 100644 index 0000000..84a3679 --- /dev/null +++ b/backend/db/structure.sql @@ -0,0 +1,152 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 13.3 +-- Dumped by pg_dump version 13.3 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: migration_versions; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.migration_versions ( + id integer NOT NULL, + version character varying(17) NOT NULL +); + + +ALTER TABLE public.migration_versions OWNER TO postgres; + +-- +-- Name: migration_versions_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE public.migration_versions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.migration_versions_id_seq OWNER TO postgres; + +-- +-- Name: migration_versions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres +-- + +ALTER SEQUENCE public.migration_versions_id_seq OWNED BY public.migration_versions.id; + + +-- +-- Name: todos; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.todos ( + id character varying(254) NOT NULL, + userid character varying(254), + content text, + tags character varying(254)[], + complete boolean, + deadline timestamp without time zone, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +ALTER TABLE public.todos OWNER TO postgres; + +-- +-- Name: unverifiedusers; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.unverifiedusers ( + id character varying(254) NOT NULL, + email character varying(254), + discord_only_account boolean, + discord_id character varying(254), + password_hash character varying(254), + verification_token character varying(254), + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +ALTER TABLE public.unverifiedusers OWNER TO postgres; + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.users ( + id character varying(254) NOT NULL, + email character varying(254), + discord_only_account boolean, + discord_id character varying(254), + password_hash character varying(254), + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +ALTER TABLE public.users OWNER TO postgres; + +-- +-- Name: migration_versions id; Type: DEFAULT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.migration_versions ALTER COLUMN id SET DEFAULT nextval('public.migration_versions_id_seq'::regclass); + + +-- +-- Name: migration_versions migration_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.migration_versions + ADD CONSTRAINT migration_versions_pkey PRIMARY KEY (id); + + +-- +-- Name: todos todos_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.todos + ADD CONSTRAINT todos_pkey PRIMARY KEY (id); + + +-- +-- Name: unverifiedusers unverifiedusers_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.unverifiedusers + ADD CONSTRAINT unverifiedusers_pkey PRIMARY KEY (id); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/backend/sam.cr b/backend/sam.cr new file mode 100644 index 0000000..92a3974 --- /dev/null +++ b/backend/sam.cr @@ -0,0 +1,13 @@ +require "./config/*" +require "sam" +require "./db/migrations/*" + +load_dependencies "jennifer" + +# Here you can define your tasks +# desc "with description to be used by help command" +# task "test" do +# puts "ping" +# end + +Sam.help diff --git a/backend/shard.lock b/backend/shard.lock new file mode 100644 index 0000000..2413bfa --- /dev/null +++ b/backend/shard.lock @@ -0,0 +1,78 @@ +version: 2.0 +shards: + ameba: + git: https://github.com/crystal-ameba/ameba.git + version: 0.14.3 + + backtracer: + git: https://github.com/sija/backtracer.cr.git + version: 1.2.1 + + base62: + git: https://github.com/janeptrv/base62.cr.git + version: 0.1.3 + + db: + git: https://github.com/crystal-lang/crystal-db.git + version: 0.10.1 + + email: + git: https://github.com/arcage/crystal-email.git + version: 0.6.3 + + exception_page: + git: https://github.com/crystal-loot/exception_page.git + version: 0.2.0 + + i18n: + git: https://github.com/techmagister/i18n.cr.git + version: 0.3.1+git.commit.b323291b772c97bc1661888eb9e82dadb722acaa + + ifrit: + git: https://github.com/imdrasil/ifrit.git + version: 0.1.3 + + inflector: + git: https://github.com/phoffer/inflector.cr.git + version: 1.0.0 + + jennifer: + git: https://github.com/imdrasil/jennifer.cr.git + version: 0.11.0 + + kemal: + git: https://gitdab.com/luna/kemal.git + version: 1.0.0+git.commit.bba5bef50506f7572db9fcdeb107c65709bf1244 + + kilt: + git: https://github.com/jeromegn/kilt.git + version: 0.6.1 + + ksuid: + git: https://github.com/janeptrv/ksuid.cr.git + version: 0.5.2 + + pg: + git: https://github.com/will/crystal-pg.git + version: 0.23.2 + + pool: + git: https://github.com/ysbaddaden/pool.git + version: 0.2.4 + + radix: + git: https://github.com/luislavena/radix.git + version: 0.4.1 + + redis: + git: https://github.com/stefanwille/crystal-redis.git + version: 2.8.0 + + sam: + git: https://github.com/imdrasil/sam.cr.git + version: 0.4.1 + + spec-kemal: + git: https://gitdab.com/luna/spec-kemal.git + version: 1.0.0+git.commit.e4765ff11d66d705438b9c79e77665d85e4ef8f4 + diff --git a/backend/shard.yml b/backend/shard.yml index 5af70bb..0a43d9a 100644 --- a/backend/shard.yml +++ b/backend/shard.yml @@ -11,3 +11,27 @@ targets: crystal: 1.0.0 license: MIT + +dependencies: + spec-kemal: + git: https://gitdab.com/luna/spec-kemal.git + kemal: + git: https://gitdab.com/luna/kemal.git + ksuid: + github: janeptrv/ksuid.cr + email: + github: arcage/crystal-email + pg: + github: will/crystal-pg + redis: + github: stefanwille/crystal-redis + jennifer: + github: imdrasil/jennifer.cr + version: "~> 0.11.0" + sam: + github: imdrasil/sam.cr + +development_dependencies: + ameba: + github: crystal-ameba/ameba + version: ~> 0.14.0 \ No newline at end of file diff --git a/backend/spec/backend_spec.cr b/backend/spec/backend_spec.cr deleted file mode 100644 index 197b354..0000000 --- a/backend/spec/backend_spec.cr +++ /dev/null @@ -1,9 +0,0 @@ -require "./spec_helper" - -describe Backend do - # TODO: Write tests - - it "works" do - false.should eq(true) - end -end diff --git a/backend/spec/spec_helper.cr b/backend/spec/spec_helper.cr index 4e1e289..e8b9e93 100644 --- a/backend/spec/spec_helper.cr +++ b/backend/spec/spec_helper.cr @@ -1,2 +1,6 @@ require "spec" -require "../src/backend" + +# note: we cannot spec backend.cr +# because kemal will start automatically. +# that's fine, because there's nothing of note there +# anyways. diff --git a/backend/spec/utils/config_spec.cr b/backend/spec/utils/config_spec.cr new file mode 100644 index 0000000..7b2cd0c --- /dev/null +++ b/backend/spec/utils/config_spec.cr @@ -0,0 +1,61 @@ +require "../spec_helper" +require "../../src/utils/config" + +describe Config do + before_each { + Config.load_config + } + it "secret is present" do + secret = Config.get_config_value("secret") + secret.is_a?(JSON::Any).should be_true + if secret.is_a?(JSON::Any) + secret.as_s.empty?.should be_false + end + end + + it "port is present" do + port = Config.get_config_value("port") + port.is_a?(JSON::Any).should be_true + if port.is_a?(JSON::Any) + port.as_i.is_a?(Int).should be_true + port.as_i.should_not eq(0) + end + end + + it "database url is present" do + url = Config.get_config_value("db_url") + url.is_a?(JSON::Any).should be_true + if url.is_a?(JSON::Any) + url.as_s.empty?.should be_false + end + end + + it "mail host and port are present" do + host = Config.get_config_value("mail_host") + host.is_a?(JSON::Any).should be_true + if host.is_a?(JSON::Any) + host.as_s.empty?.should be_false + end + + port = Config.get_config_value("mail_port") + port.is_a?(JSON::Any).should be_true + if port.is_a?(JSON::Any) + port.as_i.is_a?(Int).should be_true + port.as_i.should_not eq(0) + end + end + + it "mail username and password are present" do + uname = Config.get_config_value("mail_username") + uname.is_a?(JSON::Any).should be_true + if uname.is_a?(JSON::Any) + uname.as_s.empty?.should be_false + end + + pword = Config.get_config_value("mail_password") + pword.is_a?(JSON::Any).should be_true + if pword.is_a?(JSON::Any) + pword.as_s.empty?.should be_false + end + end +end diff --git a/backend/src/backend.cr b/backend/src/backend.cr index 4b62aae..5ad8087 100644 --- a/backend/src/backend.cr +++ b/backend/src/backend.cr @@ -1,6 +1,32 @@ -# TODO: Write documentation for `Backend` -module Backend - VERSION = "0.1.0" +require "kemal" +require "log" +require "./utils/config" +require "./endpoints/user" - # TODO: Put your code here +if !Config.is_loaded + puts "loading config" + Config.load_config +end + +serve_static false + +# replacement for the expressjs/sequelize backend of todo. +# because javascript sucks. +module Backend + VERSION = "0.0.1" +end + +get "/api/hello" do + "Hello" +end + +# this is a slightly less hilarious way to get the integer value of something +# because now i am using JSON::Any. but i am going to keep this comment +# because i want to. +port = Config.get_config_value("port") +port_to_use = port.is_a?(JSON::Any) ? port.as_i : 8000 + +Kemal.run do |config| + server = config.server.not_nil! + server.bind_tcp "0.0.0.0", port_to_use end diff --git a/backend/src/endpoints/auth.cr b/backend/src/endpoints/auth.cr new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/endpoints/register.cr b/backend/src/endpoints/register.cr new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/endpoints/todo.cr b/backend/src/endpoints/todo.cr new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/endpoints/user.cr b/backend/src/endpoints/user.cr new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/models/todo.cr b/backend/src/models/todo.cr new file mode 100644 index 0000000..81275a7 --- /dev/null +++ b/backend/src/models/todo.cr @@ -0,0 +1,16 @@ +require "jennifer" + +class Todo < Jennifer::Model::Base + with_timestamps + + mapping( + id: {type: String, primary: true}, + userid: String, + content: String, + tags: Array(String)?, + complete: Bool?, + deadline: Time?, + created_at: Time?, + updated_at: Time?, + ) +end diff --git a/backend/src/models/unverified_user.cr b/backend/src/models/unverified_user.cr new file mode 100644 index 0000000..c4d2f76 --- /dev/null +++ b/backend/src/models/unverified_user.cr @@ -0,0 +1,16 @@ +require "jennifer" + +class UnverifiedUser < Jennifer::Model::Base + with_timestamps + + mapping( + id: {type: String, primary: true}, + email: String, + discord_only_account: {type: Bool, default: false}, + discord_id: String?, + password_hash: String?, + verification_token: String, + created_at: Time?, + updated_at: Time?, + ) +end diff --git a/backend/src/models/user.cr b/backend/src/models/user.cr new file mode 100644 index 0000000..7f9f07b --- /dev/null +++ b/backend/src/models/user.cr @@ -0,0 +1,15 @@ +require "jennifer" + +class User < Jennifer::Model::Base + with_timestamps + + mapping( + id: {type: String, primary: true}, + email: String, + discord_only_account: {type: Bool, default: false}, + discord_id: String?, + password_hash: String?, + created_at: Time?, + updated_at: Time?, + ) +end diff --git a/backend/src/utils/config.cr b/backend/src/utils/config.cr new file mode 100644 index 0000000..8371b19 --- /dev/null +++ b/backend/src/utils/config.cr @@ -0,0 +1,53 @@ +require "file" +require "json" +require "log" + +class ConfigInstance + @@config = {} of String => JSON::Any + @@loaded = false + + def config + @@config + end + + def config=(new_value) + @@config = new_value + end + + def loaded + @@loaded + end + + def loaded=(new_value) + @@loaded = new_value + end +end + +Instance = ConfigInstance.new + +module Config + extend self + + def get_config_value(key) : JSON::Any | Nil + Log.debug { "looking for #{key}" } + if Instance.config.has_key? key + Instance.config[key] + else + nil + end + end + + def is_loaded : Bool + Instance.loaded + end + + def load_config : Nil + loaded_config = File.open("config.json") do |file| + Hash(String, JSON::Any).from_json(file) + end + Log.debug { "loaded config is #{loaded_config}" } + Instance.config = loaded_config + Instance.loaded = true + Log.debug { "instance config is #{Instance.config}" } + end +end diff --git a/backend/src/utils/database_caching.cr b/backend/src/utils/database_caching.cr new file mode 100644 index 0000000..60a61ee --- /dev/null +++ b/backend/src/utils/database_caching.cr @@ -0,0 +1,5 @@ +require "jennifer" +require "redis" + +module DatabaseCaching +end diff --git a/frontend/package.json b/frontend/package.json index e06b6a8..4fdffca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,10 +24,10 @@ "web-vitals": "^1.1.2" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" + "start": "BROWSER=none react-scripts start", + "build": "BROWSER=none react-scripts build", + "test": "BROWSER=none react-scripts test", + "eject": "BROWSER=none react-scripts eject" }, "eslintConfig": { "extends": [