From 3a82baec9d793edf81ac2b151b0f4d4159641375 Mon Sep 17 00:00:00 2001 From: Freya Murphy Date: Mon, 1 Apr 2024 11:09:25 -0400 Subject: [PATCH] login and register, liking on homepage --- build/php/Dockerfile | 2 +- conf/nginx/site.conf | 1 + db/rest/util/_api_get_user_id.sql | 11 - docker-compose.yml | 11 +- {db => src/db}/ext.sql | 0 {db => src/db}/migrations/0000.sql | 69 +++- {db => src/db}/migrations/0001.sql | 0 {db => src/db}/migrations/0002.sql | 0 {db => src/db}/rest/comment/api_comment.sql | 18 +- .../db}/rest/comment/api_comment_delete.sql | 9 +- .../db}/rest/comment/api_comment_insert.sql | 4 +- .../db}/rest/comment/api_comment_update.sql | 5 +- src/db/rest/like/api_like.sql | 16 + src/db/rest/like/api_like_delete.sql | 32 ++ src/db/rest/like/api_like_insert.sql | 51 +++ src/db/rest/like/api_like_update.sql | 44 +++ {db => src/db}/rest/login/_api_sign_jwt.sql | 0 .../db}/rest/login/_api_validate_role.sql | 0 {db => src/db}/rest/login/_api_verify_jwt.sql | 11 +- {db => src/db}/rest/login/api_login.sql | 0 {db => src/db}/rest/post/api_post.sql | 18 +- {db => src/db}/rest/post/api_post_delete.sql | 9 +- {db => src/db}/rest/post/api_post_insert.sql | 4 +- {db => src/db}/rest/post/api_post_update.sql | 5 +- {db => src/db}/rest/rest.sql | 6 + {db => src/db}/rest/user/api_avatar.sql | 0 {db => src/db}/rest/user/api_user.sql | 12 +- {db => src/db}/rest/user/api_user_delete.sql | 8 +- {db => src/db}/rest/user/api_user_insert.sql | 4 +- {db => src/db}/rest/user/api_user_update.sql | 3 +- src/db/rest/util/_api_get_user_id.sql | 22 ++ {db => src/db}/rest/util/_api_raise.sql | 0 {db => src/db}/rest/util/_api_raise_deny.sql | 0 {db => src/db}/rest/util/_api_raise_null.sql | 0 .../db}/rest/util/_api_raise_unique.sql | 0 {db => src/db}/rest/util/_api_serve_media.sql | 0 {db => src/db}/rest/util/_api_trim.sql | 0 .../db}/rest/util/_api_validate_text.sql | 0 {db => src/db}/rev.sql | 0 src/public/css/auth.css | 45 +++ {web => src}/public/css/common.css | 343 ++++++++++++------ {web => src}/public/css/error.css | 10 +- {web => src}/public/css/home.css | 4 +- {web => src}/public/css/post.css | 13 +- {web => src}/public/favicon.ico | Bin {web => src}/public/font/facebook.otf | Bin src/public/font/helvetica-neue.otf | Bin 0 -> 17556 bytes {web => src}/public/font/material-icons.ttf | Bin {web => src}/public/font/sfpro.otf | Bin {web => src}/public/font/sfprobold.otf | Bin {web => src}/public/js/lib.js | 31 +- {web => src}/public/js/modal.js | 0 {web => src}/public/js/post.js | 53 ++- {web => src}/public/js/routes/home.js | 0 .../public/js/thirdparty/jquery.min.js | 0 {web => src/web}/_controller/_index.php | 2 +- {web => src/web}/_controller/_util/post.php | 97 ++++- src/web/_controller/apps/auth.php | 56 +++ {web => src/web}/_controller/apps/error.php | 3 +- {web => src/web}/_controller/apps/home.php | 1 + {web => src/web}/_controller/modal.php | 8 + {web => src/web}/_controller/template.php | 0 src/web/_model/apps/auth.php | 13 + {web => src/web}/_model/apps/error.php | 16 +- {web => src/web}/_model/apps/home.php | 0 {web => src/web}/_model/cache.php | 0 {web => src/web}/_model/format.php | 0 {web => src/web}/_model/main.php | 2 +- {web => src/web}/_model/request.php | 0 src/web/_views/apps/auth/login.php | 86 +++++ {web => src/web}/_views/apps/error/main.php | 2 +- {web => src/web}/_views/apps/home/main.php | 2 +- {web => src/web}/_views/footer.php | 4 + {web => src/web}/_views/header.php | 32 +- src/web/_views/header_empty.php | 23 ++ {web => src/web}/_views/modal/new_post.php | 22 +- src/web/_views/modal/register.php | 173 +++++++++ {web => src/web}/_views/template/comment.php | 2 +- {web => src/web}/_views/template/error.php | 0 {web => src/web}/_views/template/modal.php | 0 {web => src/web}/_views/template/pfp.php | 0 {web => src/web}/_views/template/post.php | 31 +- {web => src/web}/_views/template/posts.php | 2 +- {web => src/web}/_views/template/toast.php | 11 +- {web => src/web}/config/aesthetic.php | 5 + {web => src/web}/config/routes.php | 1 + {web => src/web}/core/_controller.php | 15 + {web => src/web}/core/_model.php | 0 {web => src/web}/core/database.php | 5 + {web => src/web}/core/loader.php | 0 {web => src/web}/core/router.php | 0 {web => src/web}/helper/error.php | 0 {web => src/web}/helper/lang.php | 2 +- {web => src/web}/index.php | 1 + {web => src/web}/lang/en_US/api_lang.php | 6 + src/web/lang/en_US/apps/auth.php | 34 ++ {web => src/web}/lang/en_US/apps/home.php | 0 {web => src/web}/lang/en_US/common_lang.php | 0 {web => src/web}/lang/en_US/error_lang.php | 0 99 files changed, 1250 insertions(+), 281 deletions(-) delete mode 100644 db/rest/util/_api_get_user_id.sql rename {db => src/db}/ext.sql (100%) rename {db => src/db}/migrations/0000.sql (64%) rename {db => src/db}/migrations/0001.sql (100%) rename {db => src/db}/migrations/0002.sql (100%) rename {db => src/db}/rest/comment/api_comment.sql (51%) rename {db => src/db}/rest/comment/api_comment_delete.sql (79%) rename {db => src/db}/rest/comment/api_comment_insert.sql (96%) rename {db => src/db}/rest/comment/api_comment_update.sql (91%) create mode 100644 src/db/rest/like/api_like.sql create mode 100644 src/db/rest/like/api_like_delete.sql create mode 100644 src/db/rest/like/api_like_insert.sql create mode 100644 src/db/rest/like/api_like_update.sql rename {db => src/db}/rest/login/_api_sign_jwt.sql (100%) rename {db => src/db}/rest/login/_api_validate_role.sql (100%) rename {db => src/db}/rest/login/_api_verify_jwt.sql (74%) rename {db => src/db}/rest/login/api_login.sql (100%) rename {db => src/db}/rest/post/api_post.sql (66%) rename {db => src/db}/rest/post/api_post_delete.sql (79%) rename {db => src/db}/rest/post/api_post_insert.sql (96%) rename {db => src/db}/rest/post/api_post_update.sql (91%) rename {db => src/db}/rest/rest.sql (89%) rename {db => src/db}/rest/user/api_avatar.sql (100%) rename {db => src/db}/rest/user/api_user.sql (75%) rename {db => src/db}/rest/user/api_user_delete.sql (79%) rename {db => src/db}/rest/user/api_user_insert.sql (98%) rename {db => src/db}/rest/user/api_user_update.sql (98%) create mode 100644 src/db/rest/util/_api_get_user_id.sql rename {db => src/db}/rest/util/_api_raise.sql (100%) rename {db => src/db}/rest/util/_api_raise_deny.sql (100%) rename {db => src/db}/rest/util/_api_raise_null.sql (100%) rename {db => src/db}/rest/util/_api_raise_unique.sql (100%) rename {db => src/db}/rest/util/_api_serve_media.sql (100%) rename {db => src/db}/rest/util/_api_trim.sql (100%) rename {db => src/db}/rest/util/_api_validate_text.sql (100%) rename {db => src/db}/rev.sql (100%) create mode 100644 src/public/css/auth.css rename {web => src}/public/css/common.css (57%) rename {web => src}/public/css/error.css (57%) rename {web => src}/public/css/home.css (86%) rename {web => src}/public/css/post.css (51%) rename {web => src}/public/favicon.ico (100%) rename {web => src}/public/font/facebook.otf (100%) create mode 100644 src/public/font/helvetica-neue.otf rename {web => src}/public/font/material-icons.ttf (100%) rename {web => src}/public/font/sfpro.otf (100%) rename {web => src}/public/font/sfprobold.otf (100%) rename {web => src}/public/js/lib.js (79%) rename {web => src}/public/js/modal.js (100%) rename {web => src}/public/js/post.js (64%) rename {web => src}/public/js/routes/home.js (100%) rename {web => src}/public/js/thirdparty/jquery.min.js (100%) rename {web => src/web}/_controller/_index.php (90%) rename {web => src/web}/_controller/_util/post.php (59%) create mode 100644 src/web/_controller/apps/auth.php rename {web => src/web}/_controller/apps/error.php (85%) rename {web => src/web}/_controller/apps/home.php (94%) rename {web => src/web}/_controller/modal.php (78%) rename {web => src/web}/_controller/template.php (100%) create mode 100644 src/web/_model/apps/auth.php rename {web => src/web}/_model/apps/error.php (68%) rename {web => src/web}/_model/apps/home.php (100%) rename {web => src/web}/_model/cache.php (100%) rename {web => src/web}/_model/format.php (100%) rename {web => src/web}/_model/main.php (97%) rename {web => src/web}/_model/request.php (100%) create mode 100644 src/web/_views/apps/auth/login.php rename {web => src/web}/_views/apps/error/main.php (84%) rename {web => src/web}/_views/apps/home/main.php (94%) rename {web => src/web}/_views/footer.php (51%) rename {web => src/web}/_views/header.php (65%) create mode 100644 src/web/_views/header_empty.php rename {web => src/web}/_views/modal/new_post.php (73%) create mode 100644 src/web/_views/modal/register.php rename {web => src/web}/_views/template/comment.php (83%) rename {web => src/web}/_views/template/error.php (100%) rename {web => src/web}/_views/template/modal.php (100%) rename {web => src/web}/_views/template/pfp.php (100%) rename {web => src/web}/_views/template/post.php (67%) rename {web => src/web}/_views/template/posts.php (91%) rename {web => src/web}/_views/template/toast.php (68%) rename {web => src/web}/config/aesthetic.php (94%) rename {web => src/web}/config/routes.php (83%) rename {web => src/web}/core/_controller.php (67%) rename {web => src/web}/core/_model.php (100%) rename {web => src/web}/core/database.php (96%) rename {web => src/web}/core/loader.php (100%) rename {web => src/web}/core/router.php (100%) rename {web => src/web}/helper/error.php (100%) rename {web => src/web}/helper/lang.php (98%) rename {web => src/web}/index.php (94%) rename {web => src/web}/lang/en_US/api_lang.php (79%) create mode 100644 src/web/lang/en_US/apps/auth.php rename {web => src/web}/lang/en_US/apps/home.php (100%) rename {web => src/web}/lang/en_US/common_lang.php (100%) rename {web => src/web}/lang/en_US/error_lang.php (100%) diff --git a/build/php/Dockerfile b/build/php/Dockerfile index 280ca35..d05e60b 100644 --- a/build/php/Dockerfile +++ b/build/php/Dockerfile @@ -1,4 +1,4 @@ FROM php:fpm-alpine -RUN apk add --no-cache postgresql-dev +RUN apk add --no-cache postgresql-dev runuser RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql RUN docker-php-ext-install pdo pdo_pgsql diff --git a/conf/nginx/site.conf b/conf/nginx/site.conf index fd4cbe6..ed9bff0 100644 --- a/conf/nginx/site.conf +++ b/conf/nginx/site.conf @@ -87,6 +87,7 @@ server { } location / { + root /opt/xssbook/web; include fastcgi_params; fastcgi_pass php:9000; fastcgi_param SCRIPT_FILENAME $document_root/index.php; diff --git a/db/rest/util/_api_get_user_id.sql b/db/rest/util/_api_get_user_id.sql deleted file mode 100644 index 23eb160..0000000 --- a/db/rest/util/_api_get_user_id.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE FUNCTION _api.get_user_id() -RETURNS INTEGER -LANGUAGE plpgsql VOLATILE -AS $BODY$ -BEGIN - RETURN CURRENT_SETTING( - 'request.jwt.claims', - TRUE - )::JSON->>'user_id'; -END -$BODY$; diff --git a/docker-compose.yml b/docker-compose.yml index af31ac6..bcc96be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: ports: - '80:80' volumes: - - ./web:/opt/xssbook + - ./src:/opt/xssbook:ro - ./conf/nginx:/etc/nginx/conf.d:ro depends_on: - rest @@ -18,8 +18,9 @@ services: env_file: - ./conf/postgres/database.env volumes: - - ./web:/opt/xssbook - - ./data/status:/status + - ./src:/opt/xssbook:ro + - ./data/status:/status:ro + - ./data/session:/var/lib/php/session depends_on: - db @@ -33,7 +34,7 @@ services: - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C volumes: - './data/schemas:/var/lib/postgresql/data' - - ./db:/db:ro + - ./src/db:/db:ro rest: build: ./build/postgrest @@ -47,7 +48,7 @@ services: env_file: - ./conf/postgres/database.env volumes: - - ./db:/db:ro + - ./src/db:/db:ro - ./data/status:/status depends_on: - db diff --git a/db/ext.sql b/src/db/ext.sql similarity index 100% rename from db/ext.sql rename to src/db/ext.sql diff --git a/db/migrations/0000.sql b/src/db/migrations/0000.sql similarity index 64% rename from db/migrations/0000.sql rename to src/db/migrations/0000.sql index f3577d4..b60c55b 100644 --- a/db/migrations/0000.sql +++ b/src/db/migrations/0000.sql @@ -50,11 +50,12 @@ CREATE TABLE admin.user ( middle_name TEXT DEFAULT ''::text NOT NULL, email TEXT DEFAULT ''::text NOT NULL, gender TEXT DEFAULT ''::text NOT NULL, - join_date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, birth_date TIMESTAMP WITH TIME ZONE NOT NULL, - profile_avatar BYTEA, - profile_banner BYTEA, - profile_bio TEXT DEFAULT ''::text NOT NULL + profile_bio TEXT DEFAULT ''::text NOT NULL, + created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, + modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, + seen TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, + deleted BOOLEAN DEFAULT FALSE NOT NULL ); ALTER TABLE admin.user OWNER TO xssbook; @@ -75,10 +76,12 @@ CREATE SEQUENCE IF NOT EXISTS sys.post_id_seq ALTER TABLE sys.post_id_seq OWNER TO xssbook; CREATE TABLE admin.post ( - id INTEGER DEFAULT nextval('sys.post_id_seq'::regclass) NOT NULL, - user_id INTEGER NOT NULL, - content TEXT DEFAULT ''::text NOT NULL, - date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL + id INTEGER DEFAULT nextval('sys.post_id_seq'::regclass) NOT NULL, + user_id INTEGER NOT NULL, + content TEXT DEFAULT ''::text NOT NULL, + created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, + modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, + deleted BOOLEAN DEFAULT FALSE NOT NULL ); ALTER TABLE admin.post OWNER TO xssbook; @@ -97,11 +100,13 @@ CREATE SEQUENCE IF NOT EXISTS sys.comment_id_seq CACHE 1; CREATE TABLE admin.comment ( - id INTEGER DEFAULT nextval('sys.comment_id_seq'::regclass) NOT NULL, - user_id INTEGER NOT NULL, - post_id INTEGER NOT NULL, - content TEXT DEFAULT ''::text NOT NULL, - date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL + id INTEGER DEFAULT nextval('sys.comment_id_seq'::regclass) NOT NULL, + user_id INTEGER NOT NULL, + post_id INTEGER NOT NULL, + content TEXT DEFAULT ''::text NOT NULL, + created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, + modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, + deleted BOOLEAN DEFAULT FALSE NOT NULL ); ALTER TABLE admin.comment OWNER TO xssbook; @@ -115,15 +120,28 @@ ALTER TABLE ONLY admin.comment ALTER TABLE ONLY admin.comment ADD CONSTRAINT comment_post_id_fkey FOREIGN KEY (post_id) REFERENCES admin.post (id) ON DELETE CASCADE; +CREATE SEQUENCE IF NOT EXISTS sys.like_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + CREATE TABLE admin.like ( + id INTEGER DEFAULT nextval('sys.like_id_seq'::regclass) NOT NULL, user_id INTEGER NOT NULL, post_id INTEGER, comment_id INTEGER, - date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL + value BOOLEAN NOT NULL DEFAULT TRUE, + created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, + modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL ); ALTER TABLE admin.like OWNER TO xssbook; +ALTER TABLE ONLY admin.like + ADD CONSTRAINT like_pkey PRIMARY KEY (id); + ALTER TABLE ONLY admin.like ADD CONSTRAINT like_user_id_fkey FOREIGN KEY (user_id) REFERENCES admin.user (id) ON DELETE CASCADE; @@ -133,16 +151,32 @@ ALTER TABLE ONLY admin.like ALTER TABLE ONLY admin.like ADD CONSTRAINT like_comment_id_fkey FOREIGN KEY (comment_id) REFERENCES admin.comment (id) ON DELETE CASCADE; +ALTER TABLE ONLY admin.like + ADD CONSTRAINT like_post_id_unique UNIQUE (user_id, post_id); + +ALTER TABLE ONLY admin.like + ADD CONSTRAINT like_comment_id_unique UNIQUE (user_id, comment_id); + +CREATE SEQUENCE IF NOT EXISTS sys.follow_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + CREATE TABLE admin.follow ( + id INTEGER DEFAULT nextval('sys.follow_id_seq'::regclass) NOT NULL, follower_id INTEGER NOT NULL, followee_id INTEGER NOT NULL, - date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL + value BOOLEAN NOT NULL DEFAULT TRUE, + created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, + modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL ); ALTER TABLE admin.follow OWNER TO xssbook; ALTER TABLE ONLY admin.follow - ADD CONSTRAINT follow_pkey PRIMARY KEY (follower_id, followee_id); + ADD CONSTRAINT follow_pkey PRIMARY KEY (id); ALTER TABLE ONLY admin.follow ADD CONSTRAINT follow_follower_id FOREIGN KEY (follower_id) REFERENCES admin.user (id) ON DELETE CASCADE; @@ -150,6 +184,9 @@ ALTER TABLE ONLY admin.follow ALTER TABLE ONLY admin.follow ADD CONSTRAINT follow_followee_id FOREIGN KEY (followee_id) REFERENCES admin.user (id) ON DELETE CASCADE; +ALTER TABLE ONLY admin.follow + ADD CONSTRAINT follow_follower_unique UNIQUE (follower_id, followee_id); + CREATE TABLE admin.media ( name TEXT NOT NULL, content BYTEA NOT NULL, diff --git a/db/migrations/0001.sql b/src/db/migrations/0001.sql similarity index 100% rename from db/migrations/0001.sql rename to src/db/migrations/0001.sql diff --git a/db/migrations/0002.sql b/src/db/migrations/0002.sql similarity index 100% rename from db/migrations/0002.sql rename to src/db/migrations/0002.sql diff --git a/db/rest/comment/api_comment.sql b/src/db/rest/comment/api_comment.sql similarity index 51% rename from db/rest/comment/api_comment.sql rename to src/db/rest/comment/api_comment.sql index e50ca2f..c8a0e19 100644 --- a/db/rest/comment/api_comment.sql +++ b/src/db/rest/comment/api_comment.sql @@ -4,10 +4,24 @@ CREATE VIEW api.comment AS c.user_id, c.post_id, c.content, - c.date + c.created, + c.modified FROM admin.comment c - ORDER BY id ASC; + LEFT JOIN + admin.post p + ON + p.id = c.post_id + LEFT JOIN + admin.user u + ON + u.id = c.user_id + WHERE + c.deleted <> TRUE AND + p.deleted <> TRUE AND + u.deleted <> TRUE + ORDER BY + id ASC; GRANT SELECT ON TABLE api.comment TO rest_anon, rest_user; diff --git a/db/rest/comment/api_comment_delete.sql b/src/db/rest/comment/api_comment_delete.sql similarity index 79% rename from db/rest/comment/api_comment_delete.sql rename to src/db/rest/comment/api_comment_delete.sql index d7db8a4..262b2ed 100644 --- a/db/rest/comment/api_comment_delete.sql +++ b/src/db/rest/comment/api_comment_delete.sql @@ -11,9 +11,10 @@ BEGIN PERFORM _api.raise_deny(); END IF; - DELETE FROM admin.comment - WHERE user_id = _user_id - AND id = OLD.id; + UPDATE admin.comment SET + deleted = TRUE, + modified = clock_timestamp() + WHERE id = OLD.id; END $BODY$; @@ -21,7 +22,7 @@ GRANT EXECUTE ON FUNCTION _api.comment_delete() TO rest_user; GRANT DELETE ON TABLE api.comment TO rest_user; -GRANT DELETE ON TABLE admin.comment +GRANT UPDATE ON TABLE admin.comment TO rest_user; CREATE TRIGGER api_comment_delete_trgr diff --git a/db/rest/comment/api_comment_insert.sql b/src/db/rest/comment/api_comment_insert.sql similarity index 96% rename from db/rest/comment/api_comment_insert.sql rename to src/db/rest/comment/api_comment_insert.sql index 878e194..990beef 100644 --- a/db/rest/comment/api_comment_insert.sql +++ b/src/db/rest/comment/api_comment_insert.sql @@ -34,7 +34,9 @@ BEGIN _user_id, NEW.post_id, NEW.content - ); + ) + RETURNING id + INTO NEW.id; RETURN NEW; END diff --git a/db/rest/comment/api_comment_update.sql b/src/db/rest/comment/api_comment_update.sql similarity index 91% rename from db/rest/comment/api_comment_update.sql rename to src/db/rest/comment/api_comment_update.sql index d6b4aca..b8fc16d 100644 --- a/db/rest/comment/api_comment_update.sql +++ b/src/db/rest/comment/api_comment_update.sql @@ -27,8 +27,9 @@ BEGIN END IF; IF _changed THEN - UPDATE admin.comment - SET content = NEW.content + UPDATE admin.comment SET + content = NEW.content, + modified = clock_timestamp() WHERE id = OLD.id; END IF; diff --git a/src/db/rest/like/api_like.sql b/src/db/rest/like/api_like.sql new file mode 100644 index 0000000..6588b43 --- /dev/null +++ b/src/db/rest/like/api_like.sql @@ -0,0 +1,16 @@ +CREATE VIEW api.like AS + SELECT + l.id, + l.user_id, + l.post_id, + l.comment_id, + l.value, + l.created, + l.modified + FROM + admin.like l; + +GRANT SELECT ON TABLE api.like + TO rest_anon, rest_user; +GRANT SELECT ON TABLE admin.like + TO rest_anon, rest_user; diff --git a/src/db/rest/like/api_like_delete.sql b/src/db/rest/like/api_like_delete.sql new file mode 100644 index 0000000..7209a40 --- /dev/null +++ b/src/db/rest/like/api_like_delete.sql @@ -0,0 +1,32 @@ +CREATE FUNCTION _api.like_delete() +RETURNS TRIGGER +LANGUAGE plpgsql VOLATILE +AS $BODY$ +DECLARE + _user_id INTEGER; +BEGIN + _user_id = _api.get_user_id(); + + IF OLD.user_id <> _user_id THEN + PERFORM _api.raise_deny(); + END IF; + + UPDATE admin.like SET + value = FALSE, + modified = clock_timestamp() + WHERE id = OLD.id; +END +$BODY$; + +GRANT EXECUTE ON FUNCTION _api.like_delete() + TO rest_user; +GRANT DELETE ON TABLE api.like + TO rest_user; +GRANT UPDATE ON TABLE admin.like + TO rest_user; + +CREATE TRIGGER api_like_delete_trgr + INSTEAD OF DELETE + ON api.like + FOR EACH ROW + EXECUTE PROCEDURE _api.like_delete(); diff --git a/src/db/rest/like/api_like_insert.sql b/src/db/rest/like/api_like_insert.sql new file mode 100644 index 0000000..a02ad4e --- /dev/null +++ b/src/db/rest/like/api_like_insert.sql @@ -0,0 +1,51 @@ +CREATE FUNCTION _api.like_insert() +RETURNS TRIGGER +LANGUAGE plpgsql VOLATILE +AS $BODY$ +DECLARE + _user_id INTEGER; +BEGIN + _user_id = _api.get_user_id(); + + IF + NEW.post_id IS NULL AND + NEW.comment_id IS NULL + THEN + -- for now + PERFORM _api.raise_deny(); + END IF; + + NEW.value := COALESCE(NEW.value, TRUE); + + INSERT INTO admin.like ( + user_id, + post_id, + comment_id, + value + ) VALUES ( + _user_id, + NEW.post_id, + NEW.comment_id, + NEW.value + ) + RETURNING id + INTO NEW.id; + + RETURN NEW; +END +$BODY$; + +GRANT EXECUTE ON FUNCTION _api.like_insert() + TO rest_user; +GRANT INSERT ON TABLE api.like + TO rest_user; +GRANT INSERT ON TABLE admin.like + TO rest_user; +GRANT UPDATE ON TABLE sys.like_id_seq + TO rest_user; + +CREATE TRIGGER api_like_insert_trgr + INSTEAD OF INSERT + ON api.like + FOR EACH ROW + EXECUTE PROCEDURE _api.like_insert(); diff --git a/src/db/rest/like/api_like_update.sql b/src/db/rest/like/api_like_update.sql new file mode 100644 index 0000000..76db73a --- /dev/null +++ b/src/db/rest/like/api_like_update.sql @@ -0,0 +1,44 @@ +CREATE FUNCTION _api.like_update() +RETURNS TRIGGER +LANGUAGE plpgsql VOLATILE +AS $BODY$ +DECLARE + _user_id INTEGER; + _changed BOOLEAN; +BEGIN + _user_id = _api.get_user_id(); + _changed = FALSE; + + IF OLD.user_id <> _user_id THEN + PERFORM _api.raise_deny(); + END IF; + + NEW.value = COALESCE(NEW.value, OLD.value); + + IF NEW.value IS DISTINCT FROM OLD.value THEN + _changed = TRUE; + END IF; + + IF _changed THEN + UPDATE admin.like SET + value = NEW.value, + modified = clock_timestamp() + WHERE id = OLD.id; + END IF; + + RETURN NEW; +END +$BODY$; + +GRANT EXECUTE ON FUNCTION _api.like_update() + TO rest_user; +GRANT UPDATE ON TABLE api.like + TO rest_user; +GRANT UPDATE ON TABLE admin.like + TO rest_user; + +CREATE TRIGGER api_like_update_trgr + INSTEAD OF UPDATE + ON api.like + FOR EACH ROW + EXECUTE PROCEDURE _api.like_update(); diff --git a/db/rest/login/_api_sign_jwt.sql b/src/db/rest/login/_api_sign_jwt.sql similarity index 100% rename from db/rest/login/_api_sign_jwt.sql rename to src/db/rest/login/_api_sign_jwt.sql diff --git a/db/rest/login/_api_validate_role.sql b/src/db/rest/login/_api_validate_role.sql similarity index 100% rename from db/rest/login/_api_validate_role.sql rename to src/db/rest/login/_api_validate_role.sql diff --git a/db/rest/login/_api_verify_jwt.sql b/src/db/rest/login/_api_verify_jwt.sql similarity index 74% rename from db/rest/login/_api_verify_jwt.sql rename to src/db/rest/login/_api_verify_jwt.sql index f5a6daf..9e63cc9 100644 --- a/db/rest/login/_api_verify_jwt.sql +++ b/src/db/rest/login/_api_verify_jwt.sql @@ -8,6 +8,7 @@ DECLARE _payload JSON; _valid BOOLEAN; _jwt_secret TEXT; + _user_id INTEGER; BEGIN SELECT jwt_secret INTO _jwt_secret FROM sys.database_info @@ -28,7 +29,13 @@ BEGIN RETURN NULL; END IF; - RETURN _payload->>'user_id'; + _user_id = _payload->>'user_id'; + + UPDATE admin.user + SET seen = clock_timestamp() + WHERE id = _user_id; + + RETURN _user_id; END $BODY$; @@ -36,3 +43,5 @@ GRANT EXECUTE ON FUNCTION _api.verify_jwt(TEXT) TO rest_anon, rest_user; GRANT SELECT ON TABLE sys.database_info TO rest_anon, rest_user; +GRANT UPDATE ON TABLE admin.user + TO rest_anon, rest_user; diff --git a/db/rest/login/api_login.sql b/src/db/rest/login/api_login.sql similarity index 100% rename from db/rest/login/api_login.sql rename to src/db/rest/login/api_login.sql diff --git a/db/rest/post/api_post.sql b/src/db/rest/post/api_post.sql similarity index 66% rename from db/rest/post/api_post.sql rename to src/db/rest/post/api_post.sql index 375f292..0d60473 100644 --- a/db/rest/post/api_post.sql +++ b/src/db/rest/post/api_post.sql @@ -3,7 +3,8 @@ CREATE VIEW api.post AS p.id, p.user_id, p.content, - p.date, + p.created, + p.modified, COALESCE(c.cc, 0) AS comment_count FROM @@ -16,8 +17,19 @@ CREATE VIEW api.post AS admin.comment c GROUP BY c.post_id - ) c ON p.id = c.post_id - ORDER BY p.id DESC; + ) c + ON + p.id = c.post_id + LEFT JOIN + admin.user u + ON + u.id = p.user_id + WHERE + p.deleted <> TRUE + AND + u.deleted <> TRUE + ORDER BY + p.id DESC; GRANT SELECT ON TABLE api.post TO rest_anon, rest_user; diff --git a/db/rest/post/api_post_delete.sql b/src/db/rest/post/api_post_delete.sql similarity index 79% rename from db/rest/post/api_post_delete.sql rename to src/db/rest/post/api_post_delete.sql index e3dec55..8f26b40 100644 --- a/db/rest/post/api_post_delete.sql +++ b/src/db/rest/post/api_post_delete.sql @@ -11,9 +11,10 @@ BEGIN PERFORM _api.raise_deny(); END IF; - DELETE FROM admin.post - WHERE user_id = _user_id - AND id = OLD.id; + UPDATE admin.post SET + deleted = TRUE, + modified = clock_timestamp() + WHERE id = OLD.id; END $BODY$; @@ -21,7 +22,7 @@ GRANT EXECUTE ON FUNCTION _api.post_delete() TO rest_user; GRANT DELETE ON TABLE api.post TO rest_user; -GRANT DELETE ON TABLE admin.post +GRANT UPDATE ON TABLE admin.post TO rest_user; CREATE TRIGGER api_post_delete_trgr diff --git a/db/rest/post/api_post_insert.sql b/src/db/rest/post/api_post_insert.sql similarity index 96% rename from db/rest/post/api_post_insert.sql rename to src/db/rest/post/api_post_insert.sql index 8b2eb48..e0594dc 100644 --- a/db/rest/post/api_post_insert.sql +++ b/src/db/rest/post/api_post_insert.sql @@ -22,7 +22,9 @@ BEGIN ) VALUES ( _user_id, NEW.content - ); + ) + RETURNING id + INTO NEW.id; RETURN NEW; END diff --git a/db/rest/post/api_post_update.sql b/src/db/rest/post/api_post_update.sql similarity index 91% rename from db/rest/post/api_post_update.sql rename to src/db/rest/post/api_post_update.sql index 70230d0..7b4360d 100644 --- a/db/rest/post/api_post_update.sql +++ b/src/db/rest/post/api_post_update.sql @@ -27,8 +27,9 @@ BEGIN END IF; IF _changed THEN - UPDATE admin.post - SET content = NEW.content + UPDATE admin.post SET + content = NEW.content, + modified = clock_timestamp() WHERE id = OLD.id; END IF; diff --git a/db/rest/rest.sql b/src/db/rest/rest.sql similarity index 89% rename from db/rest/rest.sql rename to src/db/rest/rest.sql index 54f5118..3e6737c 100644 --- a/db/rest/rest.sql +++ b/src/db/rest/rest.sql @@ -41,6 +41,12 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user; \i /db/rest/comment/api_comment_update.sql; \i /db/rest/comment/api_comment_delete.sql; +-- like +\i /db/rest/like/api_like.sql; +\i /db/rest/like/api_like_insert.sql; +\i /db/rest/like/api_like_update.sql; +\i /db/rest/like/api_like_delete.sql; + -- login \i /db/rest/login/_api_sign_jwt.sql; \i /db/rest/login/_api_verify_jwt.sql; diff --git a/db/rest/user/api_avatar.sql b/src/db/rest/user/api_avatar.sql similarity index 100% rename from db/rest/user/api_avatar.sql rename to src/db/rest/user/api_avatar.sql diff --git a/db/rest/user/api_user.sql b/src/db/rest/user/api_user.sql similarity index 75% rename from db/rest/user/api_user.sql rename to src/db/rest/user/api_user.sql index e45768a..6735775 100644 --- a/db/rest/user/api_user.sql +++ b/src/db/rest/user/api_user.sql @@ -9,13 +9,15 @@ CREATE VIEW api.user AS u.middle_name, u.email, u.gender, - u.join_date, u.birth_date, - u.profile_avatar, - u.profile_banner, - u.profile_bio + u.profile_bio, + u.created, + u.modified, + u.seen FROM - admin.user u; + admin.user u + WHERE + u.deleted <> TRUE; GRANT SELECT ON TABLE api.user TO rest_anon, rest_user; diff --git a/db/rest/user/api_user_delete.sql b/src/db/rest/user/api_user_delete.sql similarity index 79% rename from db/rest/user/api_user_delete.sql rename to src/db/rest/user/api_user_delete.sql index 8d7d52f..4389fa0 100644 --- a/db/rest/user/api_user_delete.sql +++ b/src/db/rest/user/api_user_delete.sql @@ -11,8 +11,10 @@ BEGIN PERFORM _api.raise_deny(); END IF; - DELETE FROM admin.user - WHERE id = _user_id; + UPDATE admin.user SET + deleted = TRUE, + modified = clock_timestamp() + WHERE id = _user_id; END $BODY$; @@ -20,7 +22,7 @@ GRANT EXECUTE ON FUNCTION _api.user_delete() TO rest_user; GRANT DELETE ON TABLE api.user TO rest_user; -GRANT DELETE ON TABLE admin.user +GRANT UPDATE ON TABLE admin.user TO rest_user; CREATE TRIGGER api_user_delete_trgr diff --git a/db/rest/user/api_user_insert.sql b/src/db/rest/user/api_user_insert.sql similarity index 98% rename from db/rest/user/api_user_insert.sql rename to src/db/rest/user/api_user_insert.sql index 2297ecd..1a6ef7c 100644 --- a/db/rest/user/api_user_insert.sql +++ b/src/db/rest/user/api_user_insert.sql @@ -104,7 +104,9 @@ BEGIN NEW.gender, NEW.birth_date, NEW.profile_bio - ); + ) + RETURNING id + INTO NEW.id; NEW.password := NULL; diff --git a/db/rest/user/api_user_update.sql b/src/db/rest/user/api_user_update.sql similarity index 98% rename from db/rest/user/api_user_update.sql rename to src/db/rest/user/api_user_update.sql index 28e4368..2e7cd50 100644 --- a/db/rest/user/api_user_update.sql +++ b/src/db/rest/user/api_user_update.sql @@ -145,7 +145,8 @@ BEGIN email = NEW.email, gender = NEW.gender, birth_date = NEW.birth_date, - profile_bio = NEW.profile_bio + profile_bio = NEW.profile_bio, + modified = clock_timestamp() WHERE id = OLD.id; END IF; diff --git a/src/db/rest/util/_api_get_user_id.sql b/src/db/rest/util/_api_get_user_id.sql new file mode 100644 index 0000000..e86afc3 --- /dev/null +++ b/src/db/rest/util/_api_get_user_id.sql @@ -0,0 +1,22 @@ +CREATE FUNCTION _api.get_user_id() +RETURNS INTEGER +LANGUAGE plpgsql VOLATILE +AS $BODY$ +DECLARE + _user_id INTEGER; +BEGIN + _user_id = CURRENT_SETTING( + 'request.jwt.claims', + TRUE + )::JSON->>'user_id'; + + UPDATE admin.user + SET seen = clock_timestamp() + WHERE id = _user_id; + + RETURN _user_id; +END +$BODY$; + +GRANT UPDATE ON TABLE admin.user + TO rest_anon, rest_user; diff --git a/db/rest/util/_api_raise.sql b/src/db/rest/util/_api_raise.sql similarity index 100% rename from db/rest/util/_api_raise.sql rename to src/db/rest/util/_api_raise.sql diff --git a/db/rest/util/_api_raise_deny.sql b/src/db/rest/util/_api_raise_deny.sql similarity index 100% rename from db/rest/util/_api_raise_deny.sql rename to src/db/rest/util/_api_raise_deny.sql diff --git a/db/rest/util/_api_raise_null.sql b/src/db/rest/util/_api_raise_null.sql similarity index 100% rename from db/rest/util/_api_raise_null.sql rename to src/db/rest/util/_api_raise_null.sql diff --git a/db/rest/util/_api_raise_unique.sql b/src/db/rest/util/_api_raise_unique.sql similarity index 100% rename from db/rest/util/_api_raise_unique.sql rename to src/db/rest/util/_api_raise_unique.sql diff --git a/db/rest/util/_api_serve_media.sql b/src/db/rest/util/_api_serve_media.sql similarity index 100% rename from db/rest/util/_api_serve_media.sql rename to src/db/rest/util/_api_serve_media.sql diff --git a/db/rest/util/_api_trim.sql b/src/db/rest/util/_api_trim.sql similarity index 100% rename from db/rest/util/_api_trim.sql rename to src/db/rest/util/_api_trim.sql diff --git a/db/rest/util/_api_validate_text.sql b/src/db/rest/util/_api_validate_text.sql similarity index 100% rename from db/rest/util/_api_validate_text.sql rename to src/db/rest/util/_api_validate_text.sql diff --git a/db/rev.sql b/src/db/rev.sql similarity index 100% rename from db/rev.sql rename to src/db/rev.sql diff --git a/src/public/css/auth.css b/src/public/css/auth.css new file mode 100644 index 0000000..b08e27b --- /dev/null +++ b/src/public/css/auth.css @@ -0,0 +1,45 @@ +#main-content { + padding-top: 20rem; + padding-bottom: 5rem; + display: flex; + flex-direction: row; + justify-content: center; +} + +.branding { + max-width: 30rem; + margin-right: 5rem; +} + +.branding h1 { + color: var(--blue); + font-family: facebook; + font-size: 3.5rem; +} + +.branding span { + font-size: 1.5rem; +} + +.form { + display: flex; + flex-direction: column; + width: 30rem; +} + +@media(max-width: 1200px) { + #main-content { + flex-direction: column; + align-items: center; + width: 100%; + padding: 10rem 0; + } + + .branding { + margin: 0; + } + + .form { + margin-top: 4rem; + } +} diff --git a/web/public/css/common.css b/src/public/css/common.css similarity index 57% rename from web/public/css/common.css rename to src/public/css/common.css index 8b55268..8535564 100644 --- a/web/public/css/common.css +++ b/src/public/css/common.css @@ -1,17 +1,44 @@ :root { - --primary: #242424 !important; - --secondary: #181818 !important; - --hover: #1b1b1b !important; - --light: #3e4042 !important; - --mild: #1b1b1b !important; - --medium: #e2ded6 !important; - --extreme: #e2ded6 !important; - --logo: #1778f2 !important; - --error: #f02849 !important; - --success: #30ab5a !important; - --text: #ffffff !important; - --banner: #6b6b6b !important; - --popup: #242424cc !important; + --white: #E4E6EB; + --blue: #1778f2; + --red: #f02849; + --green: #30ab5a; + + --blue-alt: #1D85FC; + --green-alt: #39B463; + + --font: Helvetica; +} + +:root { + --base :#18191A; + --surface0: #242526; + --surface1: #3A3B3C; + --surface2: #4E4F50; + + --text: #E4E6EB; + --subtext: #B0B3B8; + --btntext: #E4E6EB; +} + +/** +:root { + --base: #f0f2f5; + --surface0: #ffffff; + --surface1: #f0f2f5; + --surface2: #dadde1; + + --text: #000000; + --subtext: #1d2129; + --btntext: #606770; +} +*/ + +@font-face { + font-family: 'Helvetica Neue'; + font-style: normal; + src: url("/public/font/helvetica-neue.otf") format("opentype"); + font-display: swap; } @font-face { @@ -33,14 +60,8 @@ font-display: swap; } -@font-face { - font-family: sfprobold; - src: url("/public/font/sfprobold.otf") format("opentype"); - font-display: swap; -} - body { - background-color: var(--secondary); + background-color: var(--surface0); width: 100%; height: 100%; margin: 0; @@ -48,93 +69,132 @@ body { display: flex; flex-direction: column; color: var(--text); - font-family: sfpro; + font-family: var(--font); +} + +#main-content { + background-color: var(--base); + padding-top: 1rem; } header { top: 0; position: sticky; height: 3.5rem; - background-color: var(--primary); + background-color: var(--surface0); display: flex; flex-direction: row; align-items: center; padding: 0 1rem; + border-bottom: 1px solid var(--surface1); } header .logo { font-family: facebook; - color: var(--logo); + color: var(--blue); font-size: 2.25rem; height: 100%; line-height: 2rem; margin-top: .75rem; } +footer { + text-align: center; + padding: 1rem; + color: var(--subtext); + font-size: .75rem; +} + +hr { + color: var(--surface2); + background-color: var(--surface2); + width: 100%; + height: 1px; + border: none; +} + +a, button, input, div { + box-sizing: border-box; +} + a, button, input { background: none; border: none; - display: flex; - flex-direction: row; - align-items: center; - font-family: sfprobold; color: inherit; - text-decoration: none; - font-size: 1rem; } a, button { cursor: pointer; } -form button { - padding: .5rem; - border-radius: .5rem; +.btn { + color: var(--btntext); + display: flex; + align-items: center; + align-content: center; + flex-direction: row; + font-weight: bold; + font-size: 1rem; + text-decoration: none; + + padding: .4rem .6rem; + border-radius: .25rem; + background-color: transparent; + width: fit-content; } -input:focus { +.btn:hover { + background-color: var(--surface1); +} + +.btn-alt { + background-color: var(--surface1); +} + +.btn-alt:hover { + background-color: var(--surface2); +} + +.btn-wide { + width: auto; + flex-grow: 1; + justify-content: center; +} + +.btn-line:hover { + background-color: inherit; + text-decoration: underline; +} + +.btn-blue { + color: var(--blue-alt); +} + +input.btn:focus { border: none; outline: none; } -.header-entry { - display: flex; - flex-direction: row; - text-decoration: none; - align-items: center; - color: var(--text); +.btn-submit { + color: var(--white); + background-color: var(--blue); + flex-grow: 1; + padding: .5rem; } -.nav .header-entry { - height: 100%; +.btn-submit:hover { + background-color: var(--blue-alt); } -.nav-center .header-entry:hover { - background-color: var(--hover); +.btn-success { + color: var(--white); + background-color: var(--green); + flex-grow: 1; + padding: .5rem; } -.btn-action { - justify-content: center; - align-items: center; - padding: .35rem; - margin: .25rem; - border-radius: .25rem; -} - -.btn-action:hover { - background-color: var(--hover); -} - -.btn-blue:hover { - color: var(--logo); -} - -.header .btn-blue { - border-bottom: 1px solid var(--logo); -} - -.btn-line:hover { - text-decoration: underline; +.btn-success:hover { + background-color: var(--green-alt); } .nav, @@ -167,12 +227,13 @@ input:focus { } @media (min-width: 800px) { - .header-entry > span { + .nav-center .btn > span { display: none; } - .nav-center .header-entry { + .nav-center .btn { padding: 0 3rem; + height: 100%; } #action-hamburger { @@ -187,7 +248,7 @@ input:focus { flex-direction: column; top: 100%; height: fit-content; - background-color: var(--primary); + background-color: var(--surface0); width: 100%; left: 0; transform: translateX(0%); @@ -198,18 +259,18 @@ input:focus { display: inherit !important; } - .nav-center .header-entry { + .nav-center .btn { width: calc(100% - 3rem); padding: .75rem 0rem !important; padding-left: 3rem !important; justify-content: flex-start; } - .nav-center .header-entry > span { + .nav-center .btn > span { margin-left: 1rem; } - .nav-center .header-entry.active { + .nav-center .btn.active { border-bottom: none; } } @@ -218,11 +279,6 @@ input:focus { display: block; } -.nav-right .header-entry { - padding: 0; - padding-left: 1.5rem; -} - @keyframes shimmer { to { background-position-x: 0%; @@ -242,40 +298,32 @@ input:focus { } .image-loading { - background: linear-gradient(-45deg, var(--secondary) 0%, var(--primary) 25%, var(--secondary) 50%); + background: linear-gradient(-45deg, var(--surface0) 0%, var(--base) 25%, var(--surface0) 50%); background-size: 500%; background-position-x: 150%; animation: shimmer 1s linear infinite; } .card { - background-color: var(--primary); + background-color: var(--surface0); border-radius: .5rem; padding: 1rem; } +.card p { + margin-bottom: 0; +} + .card form { flex-grow: 1; } .card .sub-card { - background-color: var(--secondary); + background-color: var(--surface1); border-radius: .5rem; padding: .75rem; } -.input { - padding: 10px; - border-radius: 10px; - width: calc(100% - 20px); - background-color: var(--secondary); - font-family: sfpro; -} - -.input:hover { - background-color: var(--hover); -} - .row { display: flex; flex-direction: row; @@ -314,8 +362,12 @@ input:focus { margin-bottom: .75rem; } +.pb { + padding-bottom: 1rem; +} + .dim { - color: var(--medium); + color: var(--subtext); } .modal-container { @@ -329,7 +381,7 @@ input:focus { } .modal { - background-color: var(--primary); + background-color: var(--surface0); position: absolute; top: 50%; left: 50%; @@ -372,18 +424,14 @@ input:focus { } .modal-header { - font-family: sfprobold; + font-weight: bold; position: relative; - border-bottom: 1px solid var(--light); - text-align: center; - margin: 0 1rem; - border-radius: .5rem .5rem 0 0; + border-bottom: 1px solid var(--surface1); display: flex; justify-content: center; align-items: center; - padding-left: 1rem; cursor: grab; - padding: 1rem; + padding: 1rem 0; } .modal-content { @@ -402,9 +450,9 @@ input:focus { .float-right { position: absolute; - transform: translate(0%, -50%); - top: 45%; - right: 0; + top: 50%; + left: 100%; + transform: translate(-125%, -50%); } .mi { @@ -421,17 +469,6 @@ input:focus { font-size: 2rem; } -button[type="submit"] { - text-align: center; - background-color: var(--logo); - flex-grow: 1; - padding: .5rem; -} - -button[type="submit"]:hover { - background-color: var(--logo); -} - #toast-container { position: fixed; top: 4rem; @@ -442,20 +479,92 @@ button[type="submit"]:hover { } .toast { + color: var(--white); padding: .75rem; margin: .5rem; border-radius: .5rem; min-width: 15rem; - font-family: sfpro; animation: fadeIn .1s, slideIn .25s linear; display: flex; justify-content: space-between; } .toast.error { - background-color: var(--error); + background-color: var(--red); } .toast.success { - background-color: var(--success); + background-color: var(--green); +} + +form input:not(.btn) { + display: block; + font-size: 1.1rem; + outline: 2px solid var(--surface2); + border-radius: .25rem; + padding: .75rem; +} + +form input:not(.btn):focus { + outline-color: var(--blue); +} + +form .rel label:not(.static) { + position: absolute; + top: 50%; + transform: translate(.5rem, -40%); + color: var(--subtext); + transition: all 0.2s ease-out; + pointer-events: none; + width: fit-content; + font-size: 1.1rem; +} + +input:focus + label:not(.static), +input:not(:placeholder-shown) + label:not(.static) { + color: var(--text); + top: 0; + padding: .5rem; + padding-top: 0; + font-size: .75rem; + transform: translate(.5rem, -25%); + background-color: var(--surface0); +} + +.rel { + position: relative; +} + +.rel input { + width: 100%; + flex-grow: 1; +} + +input[type=radio] { + padding: 3rem !important; +} + +.radio { + display: flex; + flex-direction: row; + width: auto; + flex-grow: 1; +} + +.radio label { + border: 1px solid var(--surface2); + height: fit-content; + width: 100%; + padding: .75rem; + border-radius: .25rem; + cursor: pointer; +} + +.radio input { + position: absolute; + top: 50%; + left: 100%; + transform: translate(-250%, -70%); + width: fit-content; + outline: none !important; } diff --git a/web/public/css/error.css b/src/public/css/error.css similarity index 57% rename from web/public/css/error.css rename to src/public/css/error.css index aea11d9..5567cd5 100644 --- a/web/public/css/error.css +++ b/src/public/css/error.css @@ -1,16 +1,16 @@ -#error { +#main-content { display: flex; flex-direction: column; align-items: center; - margin-top: 10rem; + padding: 10rem 0; } -#error h1 { - color: var(--logo); +#main-content h1 { + color: var(--blue); font-family: Facebook; font-size: 5rem; } -#error span { +#main-content span { font-size: 2rem; } diff --git a/web/public/css/home.css b/src/public/css/home.css similarity index 86% rename from web/public/css/home.css rename to src/public/css/home.css index e70223e..3c2a3a1 100644 --- a/web/public/css/home.css +++ b/src/public/css/home.css @@ -1,9 +1,7 @@ #main-content { - width: 100%; display: flex; flex-direction: column; align-items: center; - margin-top: 1rem; } .card { @@ -15,7 +13,6 @@ border: none; resize: none; outline: none; - font-family: sfpro; font-size: 1.5rem; margin: 1rem 0; width: 100%; @@ -23,4 +20,5 @@ flex-grow: 1; background-color: transparent; color: var(--text); + font-family: var(--font); } diff --git a/web/public/css/post.css b/src/public/css/post.css similarity index 51% rename from web/public/css/post.css rename to src/public/css/post.css index 4030da3..6fd7ca0 100644 --- a/web/public/css/post.css +++ b/src/public/css/post.css @@ -1,16 +1,13 @@ -.post hr { - color: var(--light); - margin: 0; -} - -.post hr:nth-of-type(1) { - margin-top: .5rem; -} .action-load-comments { margin-left: 4rem; } #action-load-posts { + width: 100%; justify-content: center; } + +.post { + padding-bottom: 0; +} diff --git a/web/public/favicon.ico b/src/public/favicon.ico similarity index 100% rename from web/public/favicon.ico rename to src/public/favicon.ico diff --git a/web/public/font/facebook.otf b/src/public/font/facebook.otf similarity index 100% rename from web/public/font/facebook.otf rename to src/public/font/facebook.otf diff --git a/src/public/font/helvetica-neue.otf b/src/public/font/helvetica-neue.otf new file mode 100644 index 0000000000000000000000000000000000000000..7a08ea080365c03f9d3ea7346fdc4c2f9b927046 GIT binary patch literal 17556 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 R+Qs=5e>jOGQOH^H{{aqLcbNbH literal 0 HcmV?d00001 diff --git a/web/public/font/material-icons.ttf b/src/public/font/material-icons.ttf similarity index 100% rename from web/public/font/material-icons.ttf rename to src/public/font/material-icons.ttf diff --git a/web/public/font/sfpro.otf b/src/public/font/sfpro.otf similarity index 100% rename from web/public/font/sfpro.otf rename to src/public/font/sfpro.otf diff --git a/web/public/font/sfprobold.otf b/src/public/font/sfprobold.otf similarity index 100% rename from web/public/font/sfprobold.otf rename to src/public/font/sfprobold.otf diff --git a/web/public/js/lib.js b/src/public/js/lib.js similarity index 79% rename from web/public/js/lib.js rename to src/public/js/lib.js index 55b8161..edb7258 100644 --- a/web/public/js/lib.js +++ b/src/public/js/lib.js @@ -58,11 +58,19 @@ var $$ = (selector) => { /// ajax error handle /// -var errorToast = (xhr) => { +var errorToastAjax = (xhr) => { let data = xhr.responseJSON; - let msg = data.message; - let detail = data.details; - let hint = data.hint; + + let msg, detail, hint; + + if (data) { + msg = data.message; + detail = data.details; + hint = data.hint; + } else { + msg = 'api_unknown'; + } + let query = '?msg=' + msg; if (detail) { @@ -77,6 +85,13 @@ var errorToast = (xhr) => { }) } +var errorToast = (msg) => { + let url = '/template/toast?msg=' + msg; + $.get(url, function (data) { + $('#toast-container').prepend(data); + }) +} + $$('.action-close-toast').on('click', function() { $(this).parent().remove(); }); @@ -94,12 +109,14 @@ $$('.action-close-toast').each(function() { $.ajaxSetup({ headers: (() => { - let ajaxHeaders = {}; - ajaxHeaders['Content-Type'] = 'application/json'; + let ajaxHeaders = { + 'Content-Type': 'application/json', + 'Prefer': 'return=representation' + }; if (jwtStr) { ajaxHeaders['Authorization'] = 'Bearer ' + jwtStr } return ajaxHeaders; })(), - error: errorToast + error: errorToastAjax }) diff --git a/web/public/js/modal.js b/src/public/js/modal.js similarity index 100% rename from web/public/js/modal.js rename to src/public/js/modal.js diff --git a/web/public/js/post.js b/src/public/js/post.js similarity index 64% rename from web/public/js/post.js rename to src/public/js/post.js index 7e524bb..38bbb78 100644 --- a/web/public/js/post.js +++ b/src/public/js/post.js @@ -70,12 +70,59 @@ $$('.action-new-comment-form').on('submit', function(e) { let input = me.find('.action-new-comment'); let content = input.val(); let post_id = input.attr('postId'); + + const getComment = function(data) { + if (data) { + let container = me.closest('.post').find('.comments'); + container.prepend(data); + } + input.val(''); + } + + const onComment = function(data) { + let id = data[0].id; + $.get({ + url: '/_util/post/comment?id=' + id, + success: getComment + }); + } + $.ajax({ url: '/api/comment', method: 'POST', data: JSON.stringify({ post_id, content }), - success: function(_data) { - window.location.reload(); - }, + success: onComment }); }); + +$$('.action-like').on('click', function() { + let me = $(this); + let liked = me.hasClass('btn-blue'); + let like_id = me.attr('likeId'); + let post_id = me.attr('postId'); + + const onPatch = () => { + me.toggleClass('btn-blue'); + } + + const onPost = (data) => { + me.attr('likeId', data[0].id + ''); + me.toggleClass('btn-blue'); + } + + if (like_id) { + $.ajax({ + url: '/api/like?id=eq.' + like_id, + method: 'PATCH', + data: JSON.stringify({ value: !liked }), + success: onPatch + }); + } else { + $.ajax({ + url: '/api/like', + method: 'POST', + data: JSON.stringify({ post_id, value: true }), + success: onPost, + }); + } +}); diff --git a/web/public/js/routes/home.js b/src/public/js/routes/home.js similarity index 100% rename from web/public/js/routes/home.js rename to src/public/js/routes/home.js diff --git a/web/public/js/thirdparty/jquery.min.js b/src/public/js/thirdparty/jquery.min.js similarity index 100% rename from web/public/js/thirdparty/jquery.min.js rename to src/public/js/thirdparty/jquery.min.js diff --git a/web/_controller/_index.php b/src/web/_controller/_index.php similarity index 90% rename from web/_controller/_index.php rename to src/web/_controller/_index.php index fdf9440..2fd7db2 100644 --- a/web/_controller/_index.php +++ b/src/web/_controller/_index.php @@ -14,7 +14,7 @@ class _index_controller extends Controller { if ($this->main->session) { $this->redirect('/home'); } else { - $this->redirect('/login'); + $this->redirect('/auth/login'); } } diff --git a/web/_controller/_util/post.php b/src/web/_controller/_util/post.php similarity index 59% rename from web/_controller/_util/post.php rename to src/web/_controller/_util/post.php index b128d67..b48816d 100644 --- a/web/_controller/_util/post.php +++ b/src/web/_controller/_util/post.php @@ -21,6 +21,39 @@ class Post_controller extends Controller { $this->view('template/posts'); } + public function post(): void { + $pid = $this->request_model->get_int('id', 0); + + $post = $this->db + ->select('p.*, l.id as like_id') + ->from('api.post p') + ->join('api.like l', 'p.id = l.post_id AND l.user_id') + ->eq($pid) + ->where('p.id') + ->eq($pid) + ->row(); + + if (!$post) { + return; + } + + $users = $this->cache_model->get_users([$post]); + $uid = $post['user_id']; + + if (!array_key_exists($uid, $users)) { + return; + } + + $user = $users[$uid]; + + $data = array( + 'user' => $user, + 'page_size' => $this->page_size, + 'post' => $post + ); + $this->view('template/post', $data); + } + /** * @return array */ @@ -30,28 +63,23 @@ class Post_controller extends Controller { $offset = $page * $this->page_size; $user = $this->main->user(); + $uid = isset($user) ? $user['id'] : NULL; $query = $this->db; - if ($user) { - $query = $query->select('p.*, l.post_id IS NOT NULL as liked'); - } else { - $query = $query->select('p.*, FALSE as liked'); - } - - $query = $query->from('api.post p'); - - if ($user) { - $query = $query->join('admin.like l', 'p.id = l.post_id AND l.user_id') - ->eq($user['id']); - } + $query = $this->db + ->select('p.*, l.id as like_id') + ->from('api.post p') + ->join('api.like l', 'p.id = l.post_id AND l.user_id') + ->eq($uid); if ($max) { $query = $query - ->where('id')->le($max); + ->where('p.id')->le($max); } $posts = $query + ->order_by('p.id', 'DESC') ->limit($this->page_size) ->offset($offset) ->rows(); @@ -73,7 +101,6 @@ class Post_controller extends Controller { ->from('api.post p') ->row()['pc']; - return array( 'loaded' => count($posts), 'total' => $pc, @@ -82,6 +109,36 @@ class Post_controller extends Controller { ); } + public function comment(): void { + $cid = $this->request_model->get_int('id', 0); + + $comment = $this->db + ->select('*') + ->from('api.comment') + ->where('id') + ->eq($cid) + ->row(); + + if (!$comment) { + return; + } + + $users = $this->cache_model->get_users([$comment]); + $uid = $comment['user_id']; + + if (!array_key_exists($uid, $users)) { + return; + } + + $user = $users[$uid]; + + $data = array( + 'user' => $user, + 'comment' => $comment + ); + $this->view('template/comment', $data); + } + /** * @return array */ @@ -105,6 +162,7 @@ class Post_controller extends Controller { } $comments = $query + ->order_by('id', 'ASC') ->limit($this->page_size) ->offset($offset) ->rows(); @@ -112,6 +170,17 @@ class Post_controller extends Controller { $users = $this->cache_model->get_users($comments); $max = 0; + // only add this hr when not logged in + // otherwise its added automatically by + // the like and comment buttons + if ( + count($comments) && + $page == 0 && + $this->main->session === NULL + ) { + echo '


'; + } + foreach ($comments as $comment) { $max = max($max, $comment['id']); $data = array(); diff --git a/src/web/_controller/apps/auth.php b/src/web/_controller/apps/auth.php new file mode 100644 index 0000000..6b30cc9 --- /dev/null +++ b/src/web/_controller/apps/auth.php @@ -0,0 +1,56 @@ +auth_model = $this->load->model('apps/auth'); + } + + public function index(): void { + if ($this->main->session) { + $this->redirect('/home'); + } else { + $this->redirect('/auth/login'); + } + } + + public function login(): void { + if ($this->main->session) { + $this->redirect('/home'); + } + + parent::index(); + $data = $this->auth_model->get_data(); + $this->view('header_empty', $data); + $this->view('apps/auth/login', $data); + $this->view('footer', $data); + } + + public function logout(): void { + if ($this->main->session) { + $_SESSION['jwt'] = NULL; + } + $this->redirect('/auth/login'); + } + + public function update(): void { + if (!$this->is_ajax()) { + $this->error(400); + } + if (!isset($_POST['key']) || !isset($_POST['value'])) { + $this->error(400); + } + $key = $_POST['key']; + $value = $_POST['value']; + $_SESSION[$key] = $value; + } + +} + +?> diff --git a/web/_controller/apps/error.php b/src/web/_controller/apps/error.php similarity index 85% rename from web/_controller/apps/error.php rename to src/web/_controller/apps/error.php index 5ce9ec4..03bbd8d 100644 --- a/web/_controller/apps/error.php +++ b/src/web/_controller/apps/error.php @@ -8,11 +8,12 @@ class Error_controller extends Controller { $this->error_model = $this->load->model('apps/error'); } - public function index() { + public function index(): void { parent::index(); $data = $this->error_model->get_data(); $this->view('header', $data); $this->view('apps/error/main', $data); + $this->view('footer', $data); } } diff --git a/web/_controller/apps/home.php b/src/web/_controller/apps/home.php similarity index 94% rename from web/_controller/apps/home.php rename to src/web/_controller/apps/home.php index edf7e2b..c9a116d 100644 --- a/web/_controller/apps/home.php +++ b/src/web/_controller/apps/home.php @@ -18,6 +18,7 @@ class Home_controller extends Controller { $data = $this->home_model->get_data(); $this->view('header', $data); $this->view('apps/home/main', $data); + $this->view('footer', $data); } } diff --git a/web/_controller/modal.php b/src/web/_controller/modal.php similarity index 78% rename from web/_controller/modal.php rename to src/web/_controller/modal.php index 9ae4ca8..03074d4 100644 --- a/web/_controller/modal.php +++ b/src/web/_controller/modal.php @@ -20,6 +20,14 @@ class Modal_controller extends Controller { public function new_post(): void { $this->modal('new_post'); } + + public function register(): void { + $this->load->app_lang( + $this->main->info['lang'], + 'auth' + ); + $this->modal('register'); + } } ?> diff --git a/web/_controller/template.php b/src/web/_controller/template.php similarity index 100% rename from web/_controller/template.php rename to src/web/_controller/template.php diff --git a/src/web/_model/apps/auth.php b/src/web/_model/apps/auth.php new file mode 100644 index 0000000..a1802de --- /dev/null +++ b/src/web/_model/apps/auth.php @@ -0,0 +1,13 @@ + + +
+
+

xssbook

+ +
+
+
+
+ + +
+
+ + +
+ 'submit') + )?> + +
+
+ 'submit') + )?> +
+ +
diff --git a/web/_views/apps/error/main.php b/src/web/_views/apps/error/main.php similarity index 84% rename from web/_views/apps/error/main.php rename to src/web/_views/apps/error/main.php index 81051bd..dde39cf 100644 --- a/web/_views/apps/error/main.php +++ b/src/web/_views/apps/error/main.php @@ -1,6 +1,6 @@ -
+

diff --git a/web/_views/apps/home/main.php b/src/web/_views/apps/home/main.php similarity index 94% rename from web/_views/apps/home/main.php rename to src/web/_views/apps/home/main.php index 5cfdf8c..29bf7c3 100644 --- a/web/_views/apps/home/main.php +++ b/src/web/_views/apps/home/main.php @@ -7,7 +7,7 @@ view('template/pfp', array('user' => $self))?> diff --git a/web/_views/footer.php b/src/web/_views/footer.php similarity index 51% rename from web/_views/footer.php rename to src/web/_views/footer.php index 1266b9a..9040c3a 100644 --- a/web/_views/footer.php +++ b/src/web/_views/footer.php @@ -1,4 +1,8 @@ + + diff --git a/web/_views/header.php b/src/web/_views/header.php similarity index 65% rename from web/_views/header.php rename to src/web/_views/header.php index 891e27e..7c60197 100644 --- a/web/_views/header.php +++ b/src/web/_views/header.php @@ -2,28 +2,8 @@ main->user(); + $this->view('header_empty', $data); ?> - - - - - main->link_js($js); - } - foreach ($css_files as $css) { - echo $this->main->link_css($css); - } - ?> - <?=$title?> - - -
-
diff --git a/src/web/_views/header_empty.php b/src/web/_views/header_empty.php new file mode 100644 index 0000000..75f6f17 --- /dev/null +++ b/src/web/_views/header_empty.php @@ -0,0 +1,23 @@ + + + + + main->link_js($js); + } + foreach ($css_files as $css) { + echo $this->main->link_css($css); + } + ?> + <?=$title?> + + +
+
diff --git a/web/_views/modal/new_post.php b/src/web/_views/modal/new_post.php similarity index 73% rename from web/_views/modal/new_post.php rename to src/web/_views/modal/new_post.php index 71028ad..50b9b84 100644 --- a/web/_views/modal/new_post.php +++ b/src/web/_views/modal/new_post.php @@ -22,7 +22,7 @@

@@ -13,20 +13,28 @@

main->user(); + $liked = $post['like_id'] ? 'btn-blue' : ''; + $post_attrs = array( + 'postId' => $post['id'] + ); + if ($post['like_id'] !== NULL) { + $post_attrs['likeId'] = $post['like_id']; + } ?>
- - +

- -
-
+
$post['id']); $cdata = $this->comments(); @@ -52,16 +60,17 @@ ?>
-
+
view('template/pfp', array('user' => $user))?> -
+
+ + diff --git a/web/_views/template/posts.php b/src/web/_views/template/posts.php similarity index 91% rename from web/_views/template/posts.php rename to src/web/_views/template/posts.php index f57a25f..5e9156c 100644 --- a/web/_views/template/posts.php +++ b/src/web/_views/template/posts.php @@ -10,7 +10,7 @@ if ($loaded >= $page_size && $page_size < $total) { ilang('action_load_posts', id: 'action-load-posts', - class: 'btn btn-line mb', + class: 'btn btn-line btn-wide mb', attrs: array( 'loaded' => $loaded, 'pageSize' => $page_size, diff --git a/web/_views/template/toast.php b/src/web/_views/template/toast.php similarity index 68% rename from web/_views/template/toast.php rename to src/web/_views/template/toast.php index 1f74602..ae2e7d8 100644 --- a/web/_views/template/toast.php +++ b/src/web/_views/template/toast.php @@ -11,9 +11,16 @@ array_push($params, $hint); } - $msg = lang($msg, sub: $params); + $lang_msg = lang($msg, FALSE, sub: $params); + + if(!$lang_msg) { + $lang_msg = $msg; + } else { + $lang_msg = ucfirst($lang_msg); + } + ?>
- +
diff --git a/web/config/aesthetic.php b/src/web/config/aesthetic.php similarity index 94% rename from web/config/aesthetic.php rename to src/web/config/aesthetic.php index a2e4194..304baec 100644 --- a/web/config/aesthetic.php +++ b/src/web/config/aesthetic.php @@ -30,6 +30,11 @@ class Aesthetic { 'css/post.css' ], ), + 'auth' => array( + 'css' => [ + 'css/auth.css' + ], + ), ); } /** diff --git a/web/config/routes.php b/src/web/config/routes.php similarity index 83% rename from web/config/routes.php rename to src/web/config/routes.php index 78df332..33c871b 100644 --- a/web/config/routes.php +++ b/src/web/config/routes.php @@ -3,5 +3,6 @@ $routes = array(); $routes['home'] = 'apps/home'; $routes['error'] = 'apps/error'; +$routes['auth'] = 'apps/auth'; $routes[''] = '_index'; diff --git a/web/core/_controller.php b/src/web/core/_controller.php similarity index 67% rename from web/core/_controller.php rename to src/web/core/_controller.php index a357ccc..4a788d3 100644 --- a/web/core/_controller.php +++ b/src/web/core/_controller.php @@ -45,5 +45,20 @@ abstract class Controller { } } + protected function is_ajax(): bool { + $_POST = json_decode( + file_get_contents("php://input"), true + ); + return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest'; + } + + protected function error($code): void { + $_GET['code'] = $code; + $this->main->info['app'] = 'error'; + $error_controller = $this->load->controller('apps/error'); + $error_controller->index(); + die(); + } + } ?> diff --git a/web/core/_model.php b/src/web/core/_model.php similarity index 100% rename from web/core/_model.php rename to src/web/core/_model.php diff --git a/web/core/database.php b/src/web/core/database.php similarity index 96% rename from web/core/database.php rename to src/web/core/database.php index 079b0de..81352a9 100644 --- a/web/core/database.php +++ b/src/web/core/database.php @@ -122,6 +122,11 @@ class DatabaseQuery { return $this; } + public function order_by($column, $order = 'ASC') { + $this->query .= "ORDER BY " . $column . ' ' . $order . ' '; + return $this; + } + public function rows() { $stmt = $this->conn->prepare($this->query); try { diff --git a/web/core/loader.php b/src/web/core/loader.php similarity index 100% rename from web/core/loader.php rename to src/web/core/loader.php diff --git a/web/core/router.php b/src/web/core/router.php similarity index 100% rename from web/core/router.php rename to src/web/core/router.php diff --git a/web/helper/error.php b/src/web/helper/error.php similarity index 100% rename from web/helper/error.php rename to src/web/helper/error.php diff --git a/web/helper/lang.php b/src/web/helper/lang.php similarity index 98% rename from web/helper/lang.php rename to src/web/helper/lang.php index 96944da..48acba9 100644 --- a/web/helper/lang.php +++ b/src/web/helper/lang.php @@ -69,7 +69,7 @@ function ilang($key, } echo '>' . $text . ''; } - if ($click) { + if ($click || $button) { echo ''; } else { echo '
'; diff --git a/web/index.php b/src/web/index.php similarity index 94% rename from web/index.php rename to src/web/index.php index 9c2d239..688383f 100644 --- a/web/index.php +++ b/src/web/index.php @@ -1,5 +1,6 @@ diff --git a/src/web/lang/en_US/apps/auth.php b/src/web/lang/en_US/apps/auth.php new file mode 100644 index 0000000..fb9d758 --- /dev/null +++ b/src/web/lang/en_US/apps/auth.php @@ -0,0 +1,34 @@ + diff --git a/web/lang/en_US/apps/home.php b/src/web/lang/en_US/apps/home.php similarity index 100% rename from web/lang/en_US/apps/home.php rename to src/web/lang/en_US/apps/home.php diff --git a/web/lang/en_US/common_lang.php b/src/web/lang/en_US/common_lang.php similarity index 100% rename from web/lang/en_US/common_lang.php rename to src/web/lang/en_US/common_lang.php diff --git a/web/lang/en_US/error_lang.php b/src/web/lang/en_US/error_lang.php similarity index 100% rename from web/lang/en_US/error_lang.php rename to src/web/lang/en_US/error_lang.php