summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/php/Dockerfile2
-rw-r--r--conf/nginx/site.conf1
-rw-r--r--db/rest/util/_api_get_user_id.sql11
-rw-r--r--docker-compose.yml11
-rw-r--r--src/db/ext.sql (renamed from db/ext.sql)0
-rw-r--r--src/db/migrations/0000.sql (renamed from db/migrations/0000.sql)69
-rw-r--r--src/db/migrations/0001.sql (renamed from db/migrations/0001.sql)0
-rw-r--r--src/db/migrations/0002.sql (renamed from db/migrations/0002.sql)0
-rw-r--r--src/db/rest/comment/api_comment.sql (renamed from db/rest/comment/api_comment.sql)18
-rw-r--r--src/db/rest/comment/api_comment_delete.sql (renamed from db/rest/comment/api_comment_delete.sql)9
-rw-r--r--src/db/rest/comment/api_comment_insert.sql (renamed from db/rest/comment/api_comment_insert.sql)4
-rw-r--r--src/db/rest/comment/api_comment_update.sql (renamed from db/rest/comment/api_comment_update.sql)5
-rw-r--r--src/db/rest/like/api_like.sql16
-rw-r--r--src/db/rest/like/api_like_delete.sql32
-rw-r--r--src/db/rest/like/api_like_insert.sql51
-rw-r--r--src/db/rest/like/api_like_update.sql44
-rw-r--r--src/db/rest/login/_api_sign_jwt.sql (renamed from db/rest/login/_api_sign_jwt.sql)0
-rw-r--r--src/db/rest/login/_api_validate_role.sql (renamed from db/rest/login/_api_validate_role.sql)0
-rw-r--r--src/db/rest/login/_api_verify_jwt.sql (renamed from db/rest/login/_api_verify_jwt.sql)11
-rw-r--r--src/db/rest/login/api_login.sql (renamed from db/rest/login/api_login.sql)0
-rw-r--r--src/db/rest/post/api_post.sql (renamed from db/rest/post/api_post.sql)18
-rw-r--r--src/db/rest/post/api_post_delete.sql (renamed from db/rest/post/api_post_delete.sql)9
-rw-r--r--src/db/rest/post/api_post_insert.sql (renamed from db/rest/post/api_post_insert.sql)4
-rw-r--r--src/db/rest/post/api_post_update.sql (renamed from db/rest/post/api_post_update.sql)5
-rw-r--r--src/db/rest/rest.sql (renamed from db/rest/rest.sql)6
-rw-r--r--src/db/rest/user/api_avatar.sql (renamed from db/rest/user/api_avatar.sql)0
-rw-r--r--src/db/rest/user/api_user.sql (renamed from db/rest/user/api_user.sql)12
-rw-r--r--src/db/rest/user/api_user_delete.sql (renamed from db/rest/user/api_user_delete.sql)8
-rw-r--r--src/db/rest/user/api_user_insert.sql (renamed from db/rest/user/api_user_insert.sql)4
-rw-r--r--src/db/rest/user/api_user_update.sql (renamed from db/rest/user/api_user_update.sql)3
-rw-r--r--src/db/rest/util/_api_get_user_id.sql22
-rw-r--r--src/db/rest/util/_api_raise.sql (renamed from db/rest/util/_api_raise.sql)0
-rw-r--r--src/db/rest/util/_api_raise_deny.sql (renamed from db/rest/util/_api_raise_deny.sql)0
-rw-r--r--src/db/rest/util/_api_raise_null.sql (renamed from db/rest/util/_api_raise_null.sql)0
-rw-r--r--src/db/rest/util/_api_raise_unique.sql (renamed from db/rest/util/_api_raise_unique.sql)0
-rw-r--r--src/db/rest/util/_api_serve_media.sql (renamed from db/rest/util/_api_serve_media.sql)0
-rw-r--r--src/db/rest/util/_api_trim.sql (renamed from db/rest/util/_api_trim.sql)0
-rw-r--r--src/db/rest/util/_api_validate_text.sql (renamed from db/rest/util/_api_validate_text.sql)0
-rw-r--r--src/db/rev.sql (renamed from db/rev.sql)0
-rw-r--r--src/public/css/auth.css45
-rw-r--r--src/public/css/common.css (renamed from web/public/css/common.css)331
-rw-r--r--src/public/css/error.css (renamed from web/public/css/error.css)10
-rw-r--r--src/public/css/home.css (renamed from web/public/css/home.css)4
-rw-r--r--src/public/css/post.css (renamed from web/public/css/post.css)13
-rw-r--r--src/public/favicon.ico (renamed from web/public/favicon.ico)bin38078 -> 38078 bytes
-rw-r--r--src/public/font/facebook.otf (renamed from web/public/font/facebook.otf)bin25740 -> 25740 bytes
-rw-r--r--src/public/font/helvetica-neue.otfbin0 -> 17556 bytes
-rw-r--r--src/public/font/material-icons.ttf (renamed from web/public/font/material-icons.ttf)bin356840 -> 356840 bytes
-rw-r--r--src/public/font/sfpro.otf (renamed from web/public/font/sfpro.otf)bin2230364 -> 2230364 bytes
-rw-r--r--src/public/font/sfprobold.otf (renamed from web/public/font/sfprobold.otf)bin2298456 -> 2298456 bytes
-rw-r--r--src/public/js/lib.js (renamed from web/public/js/lib.js)31
-rw-r--r--src/public/js/modal.js (renamed from web/public/js/modal.js)0
-rw-r--r--src/public/js/post.js (renamed from web/public/js/post.js)53
-rw-r--r--src/public/js/routes/home.js (renamed from web/public/js/routes/home.js)0
-rw-r--r--src/public/js/thirdparty/jquery.min.js (renamed from web/public/js/thirdparty/jquery.min.js)0
-rw-r--r--src/web/_controller/_index.php (renamed from web/_controller/_index.php)2
-rw-r--r--src/web/_controller/_util/post.php (renamed from web/_controller/_util/post.php)97
-rw-r--r--src/web/_controller/apps/auth.php56
-rw-r--r--src/web/_controller/apps/error.php (renamed from web/_controller/apps/error.php)3
-rw-r--r--src/web/_controller/apps/home.php (renamed from web/_controller/apps/home.php)1
-rw-r--r--src/web/_controller/modal.php (renamed from web/_controller/modal.php)8
-rw-r--r--src/web/_controller/template.php (renamed from web/_controller/template.php)0
-rw-r--r--src/web/_model/apps/auth.php13
-rw-r--r--src/web/_model/apps/error.php (renamed from web/_model/apps/error.php)16
-rw-r--r--src/web/_model/apps/home.php (renamed from web/_model/apps/home.php)0
-rw-r--r--src/web/_model/cache.php (renamed from web/_model/cache.php)0
-rw-r--r--src/web/_model/format.php (renamed from web/_model/format.php)0
-rw-r--r--src/web/_model/main.php (renamed from web/_model/main.php)2
-rw-r--r--src/web/_model/request.php (renamed from web/_model/request.php)0
-rw-r--r--src/web/_views/apps/auth/login.php86
-rw-r--r--src/web/_views/apps/error/main.php (renamed from web/_views/apps/error/main.php)2
-rw-r--r--src/web/_views/apps/home/main.php (renamed from web/_views/apps/home/main.php)2
-rw-r--r--src/web/_views/footer.php (renamed from web/_views/footer.php)4
-rw-r--r--src/web/_views/header.php (renamed from web/_views/header.php)32
-rw-r--r--src/web/_views/header_empty.php23
-rw-r--r--src/web/_views/modal/new_post.php (renamed from web/_views/modal/new_post.php)22
-rw-r--r--src/web/_views/modal/register.php173
-rw-r--r--src/web/_views/template/comment.php (renamed from web/_views/template/comment.php)2
-rw-r--r--src/web/_views/template/error.php (renamed from web/_views/template/error.php)0
-rw-r--r--src/web/_views/template/modal.php (renamed from web/_views/template/modal.php)0
-rw-r--r--src/web/_views/template/pfp.php (renamed from web/_views/template/pfp.php)0
-rw-r--r--src/web/_views/template/post.php (renamed from web/_views/template/post.php)31
-rw-r--r--src/web/_views/template/posts.php (renamed from web/_views/template/posts.php)2
-rw-r--r--src/web/_views/template/toast.php (renamed from web/_views/template/toast.php)11
-rw-r--r--src/web/config/aesthetic.php (renamed from web/config/aesthetic.php)5
-rw-r--r--src/web/config/routes.php (renamed from web/config/routes.php)1
-rw-r--r--src/web/core/_controller.php (renamed from web/core/_controller.php)15
-rw-r--r--src/web/core/_model.php (renamed from web/core/_model.php)0
-rw-r--r--src/web/core/database.php (renamed from web/core/database.php)5
-rw-r--r--src/web/core/loader.php (renamed from web/core/loader.php)0
-rw-r--r--src/web/core/router.php (renamed from web/core/router.php)0
-rw-r--r--src/web/helper/error.php (renamed from web/helper/error.php)0
-rw-r--r--src/web/helper/lang.php (renamed from web/helper/lang.php)2
-rw-r--r--src/web/index.php (renamed from web/index.php)1
-rw-r--r--src/web/lang/en_US/api_lang.php (renamed from web/lang/en_US/api_lang.php)6
-rw-r--r--src/web/lang/en_US/apps/auth.php34
-rw-r--r--src/web/lang/en_US/apps/home.php (renamed from web/lang/en_US/apps/home.php)0
-rw-r--r--src/web/lang/en_US/common_lang.php (renamed from web/lang/en_US/common_lang.php)0
-rw-r--r--src/web/lang/en_US/error_lang.php (renamed from web/lang/en_US/error_lang.php)0
99 files changed, 1244 insertions, 275 deletions
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
index 2f3376f..2f3376f 100644
--- a/db/ext.sql
+++ b/src/db/ext.sql
diff --git a/db/migrations/0000.sql b/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,16 +120,29 @@ 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;
ALTER TABLE ONLY admin.like
@@ -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
index d20d895..d20d895 100644
--- a/db/migrations/0001.sql
+++ b/src/db/migrations/0001.sql
diff --git a/db/migrations/0002.sql b/src/db/migrations/0002.sql
index 47c2e24..47c2e24 100644
--- a/db/migrations/0002.sql
+++ b/src/db/migrations/0002.sql
diff --git a/db/rest/comment/api_comment.sql b/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
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
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
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
index dc8e920..dc8e920 100644
--- a/db/rest/login/_api_sign_jwt.sql
+++ b/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
index 9f1e54f..9f1e54f 100644
--- a/db/rest/login/_api_validate_role.sql
+++ b/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
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
index 0cf0535..0cf0535 100644
--- a/db/rest/login/api_login.sql
+++ b/src/db/rest/login/api_login.sql
diff --git a/db/rest/post/api_post.sql b/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
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
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
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
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
index 981409f..981409f 100644
--- a/db/rest/user/api_avatar.sql
+++ b/src/db/rest/user/api_avatar.sql
diff --git a/db/rest/user/api_user.sql b/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
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
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
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
index 5c740c6..5c740c6 100644
--- a/db/rest/util/_api_raise.sql
+++ b/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
index 17406b7..17406b7 100644
--- a/db/rest/util/_api_raise_deny.sql
+++ b/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
index be6ee29..be6ee29 100644
--- a/db/rest/util/_api_raise_null.sql
+++ b/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
index a18d960..a18d960 100644
--- a/db/rest/util/_api_raise_unique.sql
+++ b/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
index 8b0f0b8..8b0f0b8 100644
--- a/db/rest/util/_api_serve_media.sql
+++ b/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
index c972282..c972282 100644
--- a/db/rest/util/_api_trim.sql
+++ b/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
index ff3a227..ff3a227 100644
--- a/db/rest/util/_api_validate_text.sql
+++ b/src/db/rest/util/_api_validate_text.sql
diff --git a/db/rev.sql b/src/db/rev.sql
index dff1cfe..dff1cfe 100644
--- a/db/rev.sql
+++ b/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
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;
-}
-
-input:focus {
- border: none;
- outline: none;
-}
-
-.header-entry {
+.btn {
+ color: var(--btntext);
display: flex;
+ align-items: center;
+ align-content: center;
flex-direction: row;
+ font-weight: bold;
+ font-size: 1rem;
text-decoration: none;
- align-items: center;
- color: var(--text);
+
+ padding: .4rem .6rem;
+ border-radius: .25rem;
+ background-color: transparent;
+ width: fit-content;
}
-.nav .header-entry {
- height: 100%;
+.btn:hover {
+ background-color: var(--surface1);
}
-.nav-center .header-entry:hover {
- background-color: var(--hover);
+.btn-alt {
+ background-color: var(--surface1);
}
-.btn-action {
+.btn-alt:hover {
+ background-color: var(--surface2);
+}
+
+.btn-wide {
+ width: auto;
+ flex-grow: 1;
justify-content: center;
- align-items: center;
- padding: .35rem;
- margin: .25rem;
- border-radius: .25rem;
}
-.btn-action:hover {
- background-color: var(--hover);
+.btn-line:hover {
+ background-color: inherit;
+ text-decoration: underline;
}
-.btn-blue:hover {
- color: var(--logo);
+.btn-blue {
+ color: var(--blue-alt);
}
-.header .btn-blue {
- border-bottom: 1px solid var(--logo);
+input.btn:focus {
+ border: none;
+ outline: none;
}
-.btn-line:hover {
- text-decoration: underline;
+.btn-submit {
+ color: var(--white);
+ background-color: var(--blue);
+ flex-grow: 1;
+ padding: .5rem;
+}
+
+.btn-submit:hover {
+ background-color: var(--blue-alt);
+}
+
+.btn-success {
+ color: var(--white);
+ background-color: var(--green);
+ flex-grow: 1;
+ padding: .5rem;
+}
+
+.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
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
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
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
index e023946..e023946 100644
--- a/web/public/favicon.ico
+++ b/src/public/favicon.ico
Binary files differ
diff --git a/web/public/font/facebook.otf b/src/public/font/facebook.otf
index 97d5c6f..97d5c6f 100644
--- a/web/public/font/facebook.otf
+++ b/src/public/font/facebook.otf
Binary files differ
diff --git a/src/public/font/helvetica-neue.otf b/src/public/font/helvetica-neue.otf
new file mode 100644
index 0000000..7a08ea0
--- /dev/null
+++ b/src/public/font/helvetica-neue.otf
Binary files differ
diff --git a/web/public/font/material-icons.ttf b/src/public/font/material-icons.ttf
index 9d09b0f..9d09b0f 100644
--- a/web/public/font/material-icons.ttf
+++ b/src/public/font/material-icons.ttf
Binary files differ
diff --git a/web/public/font/sfpro.otf b/src/public/font/sfpro.otf
index 7042365..7042365 100644
--- a/web/public/font/sfpro.otf
+++ b/src/public/font/sfpro.otf
Binary files differ
diff --git a/web/public/font/sfprobold.otf b/src/public/font/sfprobold.otf
index 28fa5a4..28fa5a4 100644
--- a/web/public/font/sfprobold.otf
+++ b/src/public/font/sfprobold.otf
Binary files differ
diff --git a/web/public/js/lib.js b/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
index 2a704f3..2a704f3 100644
--- a/web/public/js/modal.js
+++ b/src/public/js/modal.js
diff --git a/web/public/js/post.js b/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
index e69de29..e69de29 100644
--- a/web/public/js/routes/home.js
+++ b/src/public/js/routes/home.js
diff --git a/web/public/js/thirdparty/jquery.min.js b/src/public/js/thirdparty/jquery.min.js
index 7f37b5d..7f37b5d 100644
--- a/web/public/js/thirdparty/jquery.min.js
+++ b/src/public/js/thirdparty/jquery.min.js
diff --git a/web/_controller/_index.php b/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
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<string,mixed>
*/
@@ -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<string,mixed>
*/
@@ -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 '<hr>';
+ }
+
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 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Auth_controller extends Controller {
+
+ // the home model
+ private $auth_model;
+
+ // the post controller
+ protected $post_controller;
+
+ function __construct($load) {
+ parent::__construct($load);
+ $this->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
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
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
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
index 7a8cdf8..7a8cdf8 100644
--- a/web/_controller/template.php
+++ b/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 @@
+<?php /* Copyright (c) 2024 Freya Murphy */
+class Auth_model extends Model {
+
+ function __construct($load) {
+ parent::__construct($load);
+ }
+
+ public function get_data(): array {
+ $data = parent::get_data();
+ $data['title'] = lang('login');
+ return $data;
+ }
+}
diff --git a/web/_model/apps/error.php b/src/web/_model/apps/error.php
index ad72b28..58e3346 100644
--- a/web/_model/apps/error.php
+++ b/src/web/_model/apps/error.php
@@ -7,22 +7,18 @@ class Error_model extends Model {
private function get_msg(&$data) {
if (!array_key_exists('code', $_GET)) {
+ http_response_code(500);
$data['msg'] = lang('error');
$data['title'] = '500';
} else {
$code = $_GET['code'];
+ http_response_code($code);
$data['title'] = $code;
- switch ($code) {
- case '404':
- $data['msg'] = lang('error_404');
- break;
- case '500':
- $data['msg'] = lang('error_500');
- break;
- default:
- $data['msg'] = lang('error');
- break;
+ $msg = lang('error_' . $code, FALSE);
+ if (!$msg) {
+ $msg = lang('error');
}
+ $data['msg'] = $msg;
}
}
diff --git a/web/_model/apps/home.php b/src/web/_model/apps/home.php
index 82fbf26..82fbf26 100644
--- a/web/_model/apps/home.php
+++ b/src/web/_model/apps/home.php
diff --git a/web/_model/cache.php b/src/web/_model/cache.php
index 6cf9924..6cf9924 100644
--- a/web/_model/cache.php
+++ b/src/web/_model/cache.php
diff --git a/web/_model/format.php b/src/web/_model/format.php
index 52b51be..52b51be 100644
--- a/web/_model/format.php
+++ b/src/web/_model/format.php
diff --git a/web/_model/main.php b/src/web/_model/main.php
index ab964fd..6d8b708 100644
--- a/web/_model/main.php
+++ b/src/web/_model/main.php
@@ -53,7 +53,7 @@ class Main_model {
*/
private function asset_stamp($path): int {
$root = $GLOBALS['webroot'];
- $path = $root . '/public/' . $path;
+ $path = $root . '/../public/' . $path;
return filemtime($path);
}
diff --git a/web/_model/request.php b/src/web/_model/request.php
index 4cce07a..4cce07a 100644
--- a/web/_model/request.php
+++ b/src/web/_model/request.php
diff --git a/src/web/_views/apps/auth/login.php b/src/web/_views/apps/auth/login.php
new file mode 100644
index 0000000..d7f326b
--- /dev/null
+++ b/src/web/_views/apps/auth/login.php
@@ -0,0 +1,86 @@
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<div id="main-content">
+ <div class="branding col">
+ <h1>xssbook</h1>
+ <span><?=lang('login_branding')?></span>
+ </div>
+ <div class="form card col">
+ <form id="action-login" class="col" action="">
+ <div class="rel mb">
+ <input
+ type="text"
+ name="username"
+ id="login-username"
+ placeholder=" "
+ >
+ <label for="username">
+ <?=lang('ph_username')?>
+ </label>
+ </div>
+ <div class="rel mb">
+ <input
+ type="password"
+ name="password"
+ id="login-password"
+ placeholder=" "
+ >
+ <label for="password">
+ <?=lang('ph_password')?>
+ </label>
+ </div>
+ <?=ilang('action_login',
+ class: 'btn btn-submit btn-wide',
+ button: TRUE,
+ attrs: array('type' => 'submit')
+ )?>
+ <?=ilang('action_forgot_passwd',
+ class: 'btn btn-line btn-blue btn-wide mt'
+ )?>
+ </form>
+ <hr>
+ <?=ilang('action_create_account',
+ id: 'action-register',
+ class: 'btn btn-success btn-wide',
+ button: TRUE,
+ attrs: array('type' => 'submit')
+ )?>
+ </div>
+ <script>
+
+ var onLogin = function(data) {
+ let jwt = data.token;
+
+ $.ajax({
+ url: '/auth/update',
+ method: 'POST',
+ data: JSON.stringify({
+ key: 'jwt',
+ value: jwt
+ }),
+ success: function (_) {
+ window.location = '/home';
+ }
+ })
+ };
+
+ $('#action-login').on('submit', function(e) {
+ e.preventDefault();
+ let username = $('#login-username').val();
+ let password = $('#login-password').val();
+
+ $.ajax({
+ url: '/api/rpc/login',
+ method: 'POST',
+ data: JSON.stringify({ username, password }),
+ success: onLogin
+ });
+ });
+
+ $('#action-register').on('click', function() {
+ $.get( "/modal/register", function (data) {
+ $(document.body).append(data);
+ });
+ })
+ </script>
+</div>
diff --git a/web/_views/apps/error/main.php b/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 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
-<div id="error">
+<div id="main-content">
<h1><?=$title?></h1>
<span><?=$msg?></span>
</div>
diff --git a/web/_views/apps/home/main.php b/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 @@
<?php $this->view('template/pfp', array('user' => $self))?>
<a
id="action-new-post"
- class="input btn-fake ml"
+ class="btn btn-alt btn-wide ml"
autocomplete="off"
aria-label="<?=lang('action_new_post_tip')?>"
>
diff --git a/web/_views/footer.php b/src/web/_views/footer.php
index 1266b9a..9040c3a 100644
--- a/web/_views/footer.php
+++ b/src/web/_views/footer.php
@@ -1,4 +1,8 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
+ <footer>
+ Freya Murphy © 2023 | <a href="https://freya.cat">freya.cat</a>
+ </footer>
<body>
+
</html>
diff --git a/web/_views/header.php b/src/web/_views/header.php
index 891e27e..7c60197 100644
--- a/web/_views/header.php
+++ b/src/web/_views/header.php
@@ -2,28 +2,8 @@
<?php /* vi: syntax=php */ ?>
<?php
$self = $this->main->user();
+ $this->view('header_empty', $data);
?>
-<!DOCTYPE html>
-<html>
- <head>
- <script>
- <?php if ($this->main->session): ?>
- var jwtStr = <?=json_encode($this->main->session['jwt'])?>;
- <?php else: ?>
- var jwtStr = null;
- <?php endif; ?>
- </script>
- <?php
- foreach ($js_files as $js) {
- echo $this->main->link_js($js);
- }
- foreach ($css_files as $css) {
- echo $this->main->link_css($css);
- }
- ?>
- <title><?=$title?></title>
- </head>
- <body>
<header class="nav">
<div class="nav-left">
<span class="logo">xssbook</span>
@@ -31,7 +11,7 @@
<div class="nav-center" :class="{hidden: !visible}">
<a
id="action-home"
- class="header-entry btn btn-hover btn-action btn-blue"
+ class="btn"
href="/home"
title="<?=lang('action_home_tip')?>"
>
@@ -40,7 +20,7 @@
</a>
<a
id="action-people"
- class="header-entry btn btn-hover btn-action btn-blue"
+ class="btn"
href="/people"
title="<?=lang('action_people_tip')?>"
>
@@ -49,7 +29,7 @@
</a>
<a
id="action-chat"
- class="header-entry btn btn-hover btn-action btn-blue"
+ class="btn"
href="/chat"
title="<?=lang('action_chat_tip')?>"
>
@@ -70,7 +50,7 @@
'class' => 'pfp-sm ml',
)); ?>
<?php else: ?>
- <?=ilang('action_login', class: 'btn btn-action', href: '/auth/login')?>
+ <?=ilang('action_login', class: 'btn', href: '/auth/login')?>
<?php endif; ?>
</div>
<script>
@@ -80,5 +60,3 @@
});
</script>
</header>
- <div id="toast-container">
- </div>
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 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script>
+ <?php if ($this->main->session): ?>
+ var jwtStr = <?=json_encode($this->main->session['jwt'])?>;
+ <?php else: ?>
+ var jwtStr = null;
+ <?php endif; ?>
+ </script>
+ <?php
+ foreach ($js_files as $js) {
+ echo $this->main->link_js($js);
+ }
+ foreach ($css_files as $css) {
+ echo $this->main->link_css($css);
+ }
+ ?>
+ <title><?=$title?></title>
+ </head>
+ <body>
+ <div id="toast-container">
+ </div>
diff --git a/web/_views/modal/new_post.php b/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 @@
<div class="modal-footer">
<?=ilang('action_submit',
id: 'new-post-submit',
- class: 'btn-action',
+ class: 'btn btn-wide btn-submit',
attrs: array('type' => 'submit'),
button: TRUE
)?>
@@ -32,14 +32,28 @@
$('#new-post-form').submit(function(e) {
e.preventDefault();
let content = $('#new-post-content').val();
+ let me = $(this);
+
+ const getPost = function(data) {
+ if (data) {
+ $('#post-container').prepend(data);
+ }
+ me.closest('.modal-container').remove();
+ }
+
+ const onPost = function(data) {
+ let id = data[0].id;
+ $.get({
+ url: '/_util/post/post?id=' + id,
+ success: getPost
+ });
+ }
$.ajax({
url: '/api/post',
method: 'POST',
data: JSON.stringify({ content }),
- success: function(data) {
- window.location.reload();
- },
+ success: onPost
});
});
</script>
diff --git a/src/web/_views/modal/register.php b/src/web/_views/modal/register.php
new file mode 100644
index 0000000..f4d364a
--- /dev/null
+++ b/src/web/_views/modal/register.php
@@ -0,0 +1,173 @@
+
+<?php /* Copyright (c) 2024 Freya Murphy */ ?>
+<?php /* vi: syntax=php */ ?>
+<form id="register-form">
+<div class="modal-content register-modal col">
+ <label class="static">
+ <?=lang('ph_basic_info')?>
+ </label>
+ <div class="row mt">
+ <div class="rel btn-wide">
+ <input
+ type="text"
+ name="first_name"
+ id="register-first-name"
+ placeholder=" "
+ >
+ <label for="first_name">
+ <?=lang('ph_first_name')?>
+ </label>
+ </div>
+ <div class="rel ml btn-wide">
+ <input
+ type="text"
+ name="last_name"
+ id="register-last-name"
+ placeholder=" "
+ >
+ <label for="last_name">
+ <?=lang('ph_last_name')?>
+ </label>
+ </div>
+ </div>
+ <div class="rel mt">
+ <input
+ type="text"
+ name="username"
+ id="register-username"
+ placeholder=" "
+ >
+ <label for="username">
+ <?=lang('ph_username')?>
+ </label>
+ </div>
+ <div class="rel mt">
+ <input
+ type="password"
+ name="password"
+ id="register-password"
+ placeholder=" "
+ >
+ <label for="password">
+ <?=lang('ph_password')?>
+ </label>
+ </div>
+ <div class="rel mt">
+ <input
+ type="text"
+ name="email"
+ id="register-email"
+ placeholder=" "
+ >
+ <label for="email">
+ <?=lang('ph_email')?>
+ </label>
+ </div>
+ <label for="birth_date" class="mt static">
+ <?=lang('ph_birth_date')?>
+ </label>
+ <input
+ class="mt"
+ type="date"
+ name="birth_date"
+ id="register-birth-date"
+ >
+ <label for="gender" class="mt static">
+ <?=lang('ph_gender')?>
+ </label>
+ <div class="row mt" data-type="radio" data-name="gender-wrapper">
+ <div class="rel radio mr">
+ <input
+ type="radio"
+ id="register-gender-male"
+ name="gender"
+ value="male"
+ >
+ <label
+ for="register-gender-male"
+ class="static"
+ >
+ <?=lang('ph_gender_male')?>
+ </label>
+ </div>
+ <div class="rel radio mr">
+ <input
+ type="radio"
+ id="register-gender-female"
+ name="gender"
+ value="female"
+ >
+ <label
+ for="register-gender-female"
+ class="static"
+ >
+ <?=lang('ph_gender_female')?>
+ </label>
+ </div>
+ <div class="rel radio">
+ <input
+ type="radio"
+ id="register-gender-lettuce"
+ name="gender"
+ value="lettuce"
+ >
+ <label
+ for="register-gender-lettuce"
+ class="static"
+ >
+ <?=lang('ph_gender_lettuce')?>
+ </label>
+ </div>
+ </div>
+</div>
+<div class="modal-footer">
+ <?=ilang('action_register',
+ id: 'register-submit',
+ class: 'btn btn-wide btn-success',
+ attrs: array('type' => 'submit'),
+ button: TRUE
+ )?>
+</div>
+</form>
+<script>
+ $('#register-form').submit(function(e) {
+ e.preventDefault();
+
+ const form = event.target;
+ const formFields = form.elements;
+
+ let first_name = formFields.first_name.value.trim();
+ let last_name = formFields.last_name.value.trim();
+ let username = formFields.username.value.trim();
+ let password = formFields.password.value.trim();
+ let email = formFields.email.value.trim();
+ let birth_date = formFields.birth_date.value.trim();
+ let gender = formFields.gender.value.trim();
+
+ if(birth_date === '') {
+ errorToast('toast_date_empty');
+ return;
+ }
+
+ const onSuccess = () => {
+ $.ajax({
+ url: '/api/rpc/login',
+ method: 'POST',
+ data: JSON.stringify({
+ username, password
+ }),
+ success: onLogin
+ });
+ };
+
+ $.ajax({
+ url: '/api/user',
+ method: 'POST',
+ data: JSON.stringify({
+ first_name, last_name, username, password,
+ email, birth_date, gender
+ }),
+ success: onSuccess
+ });
+ });
+</script>
diff --git a/web/_views/template/comment.php b/src/web/_views/template/comment.php
index 20032b2..3ff473b 100644
--- a/web/_views/template/comment.php
+++ b/src/web/_views/template/comment.php
@@ -8,7 +8,7 @@
<div class="ml col sub-card">
<div class="row">
<strong><?=$format_model->name($user)?></strong>
- <span class="dim ml"><?=$format_model->date($comment['date'])?></span>
+ <span class="dim ml"><?=$format_model->date($comment['created'])?></span>
</div>
<?=$comment['content']?>
</div>
diff --git a/web/_views/template/error.php b/src/web/_views/template/error.php
index 2e02cb1..2e02cb1 100644
--- a/web/_views/template/error.php
+++ b/src/web/_views/template/error.php
diff --git a/web/_views/template/modal.php b/src/web/_views/template/modal.php
index e3ce6fe..e3ce6fe 100644
--- a/web/_views/template/modal.php
+++ b/src/web/_views/template/modal.php
diff --git a/web/_views/template/pfp.php b/src/web/_views/template/pfp.php
index aec7318..aec7318 100644
--- a/web/_views/template/pfp.php
+++ b/src/web/_views/template/pfp.php
diff --git a/web/_views/template/post.php b/src/web/_views/template/post.php
index 0541026..83a72bf 100644
--- a/web/_views/template/post.php
+++ b/src/web/_views/template/post.php
@@ -5,7 +5,7 @@
<?php $this->view('template/pfp', array('user' => $user))?>
<div class="col ml">
<strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong>
- <span class="dim"><?=$post['date']?></span>
+ <span class="dim"><?=$post['created']?></span>
</div>
</div>
<p>
@@ -13,20 +13,28 @@
</p>
<?php
$self = $this->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'];
+ }
?>
<?php if ($self): ?>
<hr>
<div class="row">
- <?=ilang('action_like', class: 'grow btn btn-hover btn-action')?>
- <?=ilang('action_comment', class: 'grow btn btn-hover btn-action action-comment',
- click: '$(\'#new-comment-' . $post['id'] . '\').focus()'
+ <?=ilang('action_like',
+ class: 'btn btn-wide action-like ' . $liked,
+ attrs: $post_attrs
+ )?>
+ <?=ilang('action_comment', class: 'btn btn-wide action-comment',
+ click: '$(\'#action-new-comment-' . $post['id'] . '\').focus()'
)?>
</div>
<hr>
-<?php else: ?>
- <hr>
<?php endif; ?>
- <div class="col comments">
+ <div class="col comments pb">
<?php
$_GET = array('id' => $post['id']);
$cdata = $this->comments();
@@ -52,16 +60,17 @@
?>
</div>
<?php if ($self): ?>
- <div class="row grow mt">
+ <div class="row pb">
<?php $this->view('template/pfp', array('user' => $user))?>
- <form class="ml action-new-comment-form">
+ <form class="ml action-new-comment-form row">
<input
type="hidden"
name="id"
value="<?=$post['id']?>"
>
<input
- class="action-new-comment input"
+ id="action-new-comment-<?=$post['id']?>"
+ class="action-new-comment btn btn-wide btn-alt"
postId="<?=$post['id']?>"
autocomplete="off"
type="text"
@@ -73,3 +82,5 @@
</div>
<?php endif; ?>
</div>
+
+
diff --git a/web/_views/template/posts.php b/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
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);
+ }
+
?>
<div class="toast error">
- <?=ucfirst($msg)?>
+ <?=$lang_msg?>
<?=ilang('action_close', class: 'action-close-toast')?>
</div>
diff --git a/web/config/aesthetic.php b/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
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
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
index 936fab4..936fab4 100644
--- a/web/core/_model.php
+++ b/src/web/core/_model.php
diff --git a/web/core/database.php b/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
index 2091533..2091533 100644
--- a/web/core/loader.php
+++ b/src/web/core/loader.php
diff --git a/web/core/router.php b/src/web/core/router.php
index 72c7674..72c7674 100644
--- a/web/core/router.php
+++ b/src/web/core/router.php
diff --git a/web/helper/error.php b/src/web/helper/error.php
index 6fcaddd..6fcaddd 100644
--- a/web/helper/error.php
+++ b/src/web/helper/error.php
diff --git a/web/helper/lang.php b/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 . '</span>';
}
- if ($click) {
+ if ($click || $button) {
echo '</button>';
} else {
echo '</a>';
diff --git a/web/index.php b/src/web/index.php
index 9c2d239..688383f 100644
--- a/web/index.php
+++ b/src/web/index.php
@@ -1,5 +1,6 @@
<?php /* Copyright (c) 2024 Freya Murphy */
+session_save_path('/var/lib/php/session');
session_start();
$webroot = dirname(__FILE__);
diff --git a/web/lang/en_US/api_lang.php b/src/web/lang/en_US/api_lang.php
index 129147c..3afc4f6 100644
--- a/web/lang/en_US/api_lang.php
+++ b/src/web/lang/en_US/api_lang.php
@@ -6,6 +6,7 @@ $lang['api_column_first_name'] = 'first name';
$lang['api_column_last_name'] = 'last name';
$lang['api_column_middle_name'] = 'middle name';
$lang['api_column_email'] = 'email';
+$lang['api_column_password'] = 'password';
$lang['api_column_gender'] = 'gender';
$lang['api_column_join_date'] = 'join date';
$lang['api_column_birth_date'] = 'birth date';
@@ -22,5 +23,10 @@ $lang['api_null_value'] = '%s cannot be empty';
$lang['api_unique_value'] = '%s is not available (not unique)';
$lang['api_min_value'] = '%s length cannot be less than %s';
$lang['api_max_value'] = '%s length cannot exceed %s';
+$lang['api_invalid_login'] = 'Invalid username or password';
+$lang['api_unknown'] = 'An unknown error as occurred';
+
+// toast messages
+$lang['toast_date_empty'] = 'Birthday cannot be empty';
?>
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 @@
+<?php
+
+$lang['login'] = 'Login';
+$lang['login_branding'] = 'Connect with javascript and the world around you on XSSBook.';
+
+$lang['ph_username'] = 'Username';
+$lang['ph_password'] = 'Password';
+$lang['ph_first_name'] = 'First Name';
+$lang['ph_last_name'] = 'Last Name';
+$lang['ph_middle_name'] = 'Middle Name';
+$lang['ph_username'] = 'Username';
+$lang['ph_email'] = 'Email';
+$lang['ph_password'] = 'Password';
+$lang['ph_birth_date'] = 'Birthday';
+$lang['ph_gender'] = 'Gender';
+$lang['ph_gender_male'] = 'Male';
+$lang['ph_gender_female'] = 'Female';
+$lang['ph_gender_lettuce'] = 'Lettuce';
+$lang['ph_basic_info'] = 'General Information';
+
+$lang['action_login_tip'] = 'Login';
+$lang['action_login_text'] = 'Login';
+$lang['action_register_tip'] = 'Register';
+$lang['action_register_text'] = 'Register';
+$lang['action_create_account_tip'] = 'Create a new account';
+$lang['action_create_account_text'] = 'Create new account';
+$lang['action_forgot_passwd_tip'] = 'Reset your password';
+$lang['action_forgot_passwd_text'] = 'Forgot password?';
+
+$lang['register_modal_title'] = 'Create New Account';
+$lang['action_register_text'] = 'Register';
+$lang['action_register_tip'] = 'Register';
+
+?>
diff --git a/web/lang/en_US/apps/home.php b/src/web/lang/en_US/apps/home.php
index a30eb88..a30eb88 100644
--- a/web/lang/en_US/apps/home.php
+++ b/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
index 7e214b5..7e214b5 100644
--- a/web/lang/en_US/common_lang.php
+++ b/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
index afecaa1..afecaa1 100644
--- a/web/lang/en_US/error_lang.php
+++ b/src/web/lang/en_US/error_lang.php