Compare commits

..

No commits in common. "7e2553646c27cae8baaca1cc5c13d980661b5d90" and "ef7b0e26fadb882e026f1b447b6d18259057c040" have entirely different histories.

109 changed files with 333 additions and 1712 deletions

View file

@ -1,4 +1,4 @@
FROM php:fpm-alpine FROM php:fpm-alpine
RUN apk add --no-cache postgresql-dev runuser RUN apk add --no-cache postgresql-dev
RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql
RUN docker-php-ext-install pdo pdo_pgsql RUN docker-php-ext-install pdo pdo_pgsql

View file

@ -87,7 +87,6 @@ server {
} }
location / { location / {
root /opt/xssbook/web;
include fastcgi_params; include fastcgi_params;
fastcgi_pass php:9000; fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME $document_root/index.php; fastcgi_param SCRIPT_FILENAME $document_root/index.php;

View file

@ -50,12 +50,11 @@ CREATE TABLE admin.user (
middle_name TEXT DEFAULT ''::text NOT NULL, middle_name TEXT DEFAULT ''::text NOT NULL,
email TEXT DEFAULT ''::text NOT NULL, email TEXT DEFAULT ''::text NOT NULL,
gender 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, birth_date TIMESTAMP WITH TIME ZONE NOT NULL,
profile_bio TEXT DEFAULT ''::text NOT NULL, profile_avatar BYTEA,
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, profile_banner BYTEA,
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, profile_bio TEXT DEFAULT ''::text 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; ALTER TABLE admin.user OWNER TO xssbook;
@ -76,12 +75,10 @@ CREATE SEQUENCE IF NOT EXISTS sys.post_id_seq
ALTER TABLE sys.post_id_seq OWNER TO xssbook; ALTER TABLE sys.post_id_seq OWNER TO xssbook;
CREATE TABLE admin.post ( CREATE TABLE admin.post (
id INTEGER DEFAULT nextval('sys.post_id_seq'::regclass) NOT NULL, id INTEGER DEFAULT nextval('sys.post_id_seq'::regclass) NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
content TEXT DEFAULT ''::text NOT NULL, content TEXT DEFAULT ''::text NOT NULL,
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, date 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; ALTER TABLE admin.post OWNER TO xssbook;
@ -100,13 +97,11 @@ CREATE SEQUENCE IF NOT EXISTS sys.comment_id_seq
CACHE 1; CACHE 1;
CREATE TABLE admin.comment ( CREATE TABLE admin.comment (
id INTEGER DEFAULT nextval('sys.comment_id_seq'::regclass) NOT NULL, id INTEGER DEFAULT nextval('sys.comment_id_seq'::regclass) NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
post_id INTEGER NOT NULL, post_id INTEGER NOT NULL,
content TEXT DEFAULT ''::text NOT NULL, content TEXT DEFAULT ''::text NOT NULL,
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, date 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; ALTER TABLE admin.comment OWNER TO xssbook;
@ -120,28 +115,15 @@ ALTER TABLE ONLY admin.comment
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; 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 ( CREATE TABLE admin.like (
id INTEGER DEFAULT nextval('sys.like_id_seq'::regclass) NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
post_id INTEGER, post_id INTEGER,
comment_id INTEGER, comment_id INTEGER,
value BOOLEAN NOT NULL DEFAULT TRUE, date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
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 admin.like OWNER TO xssbook;
ALTER TABLE ONLY admin.like
ADD CONSTRAINT like_pkey PRIMARY KEY (id);
ALTER TABLE ONLY admin.like ALTER TABLE ONLY admin.like
ADD CONSTRAINT like_user_id_fkey FOREIGN KEY (user_id) REFERENCES admin.user (id) ON DELETE CASCADE; ADD CONSTRAINT like_user_id_fkey FOREIGN KEY (user_id) REFERENCES admin.user (id) ON DELETE CASCADE;
@ -151,32 +133,16 @@ ALTER TABLE ONLY admin.like
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; 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 ( CREATE TABLE admin.follow (
id INTEGER DEFAULT nextval('sys.follow_id_seq'::regclass) NOT NULL,
follower_id INTEGER NOT NULL, follower_id INTEGER NOT NULL,
followee_id INTEGER NOT NULL, followee_id INTEGER NOT NULL,
value BOOLEAN NOT NULL DEFAULT TRUE, date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
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 admin.follow OWNER TO xssbook;
ALTER TABLE ONLY admin.follow ALTER TABLE ONLY admin.follow
ADD CONSTRAINT follow_pkey PRIMARY KEY (id); ADD CONSTRAINT follow_pkey PRIMARY KEY (follower_id, followee_id);
ALTER TABLE ONLY admin.follow ALTER TABLE ONLY admin.follow
ADD CONSTRAINT follow_follower_id FOREIGN KEY (follower_id) REFERENCES admin.user (id) ON DELETE CASCADE; ADD CONSTRAINT follow_follower_id FOREIGN KEY (follower_id) REFERENCES admin.user (id) ON DELETE CASCADE;
@ -184,64 +150,16 @@ ALTER TABLE ONLY admin.follow
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; 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 SEQUENCE IF NOT EXISTS sys.media_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
CREATE TABLE admin.media ( CREATE TABLE admin.media (
id INTEGER DEFAULT nextval('sys.media_id_seq'::regclass) NOT NULL, name TEXT NOT NULL,
name TEXT NOT NULL, content BYTEA NOT NULL,
content BYTEA NOT NULL, type TEXT NOT NULL
type TEXT NOT NULL,
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
); );
ALTER TABLE admin.media OWNER TO xssbook; ALTER TABLE admin.media OWNER TO xssbook;
ALTER TABLE ONLY admin.media ALTER TABLE ONLY admin.media
ADD CONSTRAINT media_pkey PRIMARY KEY (id); ADD CONSTRAINT media_pkey PRIMARY KEY (name);
ALTER TABLE ONLY admin.media
ADD CONSTRAINT media_name_unique UNIQUE (name);
CREATE SEQUENCE IF NOT EXISTS sys.user_media_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
CREATE TYPE admin.user_media_type AS ENUM (
'avatar', 'banner'
);
CREATE TABLE admin.user_media (
id INTEGER DEFAULT nextval('sys.user_media_id_seq'::regclass) NOT NULL,
media_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
type admin.user_media_type NOT NULL
);
ALTER TABLE admin.user_media OWNER TO xssbook;
ALTER TABLE ONLY admin.user_media
ADD CONSTRAINT user_media_pkey PRIMARY KEY (id);
ALTER TABLE ONLY admin.user_media
ADD CONSTRAINT user_media_media_id_fkey FOREIGN KEY (media_id) REFERENCES admin.media (id) ON DELETE CASCADE;
ALTER TABLE ONLY admin.user_media
ADD CONSTRAINT user_media_user_id_fkey FOREIGN KEY (user_id) REFERENCES admin.user (id) ON DELETE CASCADE;
ALTER TABLE ONLY admin.user_media
ADD CONSTRAINT user_media_type_unique UNIQUE (user_id, type);
ALTER DATABASE xssbook SET search_path = admin,public; ALTER DATABASE xssbook SET search_path = admin,public;
ALTER DATABASE xssbook SET bytea_output = 'hex'; ALTER DATABASE xssbook SET bytea_output = 'hex';

View file

@ -4,24 +4,10 @@ CREATE VIEW api.comment AS
c.user_id, c.user_id,
c.post_id, c.post_id,
c.content, c.content,
c.created, c.date
c.modified
FROM FROM
admin.comment c admin.comment c
LEFT JOIN ORDER BY id ASC;
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 GRANT SELECT ON TABLE api.comment
TO rest_anon, rest_user; TO rest_anon, rest_user;

View file

@ -11,10 +11,9 @@ BEGIN
PERFORM _api.raise_deny(); PERFORM _api.raise_deny();
END IF; END IF;
UPDATE admin.comment SET DELETE FROM admin.comment
deleted = TRUE, WHERE user_id = _user_id
modified = clock_timestamp() AND id = OLD.id;
WHERE id = OLD.id;
END END
$BODY$; $BODY$;
@ -22,7 +21,7 @@ GRANT EXECUTE ON FUNCTION _api.comment_delete()
TO rest_user; TO rest_user;
GRANT DELETE ON TABLE api.comment GRANT DELETE ON TABLE api.comment
TO rest_user; TO rest_user;
GRANT UPDATE ON TABLE admin.comment GRANT DELETE ON TABLE admin.comment
TO rest_user; TO rest_user;
CREATE TRIGGER api_comment_delete_trgr CREATE TRIGGER api_comment_delete_trgr

View file

@ -34,9 +34,7 @@ BEGIN
_user_id, _user_id,
NEW.post_id, NEW.post_id,
NEW.content NEW.content
) );
RETURNING id
INTO NEW.id;
RETURN NEW; RETURN NEW;
END END

View file

@ -27,9 +27,8 @@ BEGIN
END IF; END IF;
IF _changed THEN IF _changed THEN
UPDATE admin.comment SET UPDATE admin.comment
content = NEW.content, SET content = NEW.content
modified = clock_timestamp()
WHERE id = OLD.id; WHERE id = OLD.id;
END IF; END IF;

View file

@ -8,7 +8,6 @@ DECLARE
_payload JSON; _payload JSON;
_valid BOOLEAN; _valid BOOLEAN;
_jwt_secret TEXT; _jwt_secret TEXT;
_user_id INTEGER;
BEGIN BEGIN
SELECT jwt_secret INTO _jwt_secret SELECT jwt_secret INTO _jwt_secret
FROM sys.database_info FROM sys.database_info
@ -29,13 +28,7 @@ BEGIN
RETURN NULL; RETURN NULL;
END IF; END IF;
_user_id = _payload->>'user_id'; RETURN _payload->>'user_id';
UPDATE admin.user
SET seen = clock_timestamp()
WHERE id = _user_id;
RETURN _user_id;
END END
$BODY$; $BODY$;
@ -43,5 +36,3 @@ GRANT EXECUTE ON FUNCTION _api.verify_jwt(TEXT)
TO rest_anon, rest_user; TO rest_anon, rest_user;
GRANT SELECT ON TABLE sys.database_info GRANT SELECT ON TABLE sys.database_info
TO rest_anon, rest_user; TO rest_anon, rest_user;
GRANT UPDATE ON TABLE admin.user
TO rest_anon, rest_user;

View file

@ -3,8 +3,7 @@ CREATE VIEW api.post AS
p.id, p.id,
p.user_id, p.user_id,
p.content, p.content,
p.created, p.date,
p.modified,
COALESCE(c.cc, 0) COALESCE(c.cc, 0)
AS comment_count AS comment_count
FROM FROM
@ -17,19 +16,8 @@ CREATE VIEW api.post AS
admin.comment c admin.comment c
GROUP BY GROUP BY
c.post_id c.post_id
) c ) c ON p.id = c.post_id
ON ORDER BY p.id DESC;
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 GRANT SELECT ON TABLE api.post
TO rest_anon, rest_user; TO rest_anon, rest_user;

View file

@ -11,10 +11,9 @@ BEGIN
PERFORM _api.raise_deny(); PERFORM _api.raise_deny();
END IF; END IF;
UPDATE admin.post SET DELETE FROM admin.post
deleted = TRUE, WHERE user_id = _user_id
modified = clock_timestamp() AND id = OLD.id;
WHERE id = OLD.id;
END END
$BODY$; $BODY$;
@ -22,7 +21,7 @@ GRANT EXECUTE ON FUNCTION _api.post_delete()
TO rest_user; TO rest_user;
GRANT DELETE ON TABLE api.post GRANT DELETE ON TABLE api.post
TO rest_user; TO rest_user;
GRANT UPDATE ON TABLE admin.post GRANT DELETE ON TABLE admin.post
TO rest_user; TO rest_user;
CREATE TRIGGER api_post_delete_trgr CREATE TRIGGER api_post_delete_trgr

View file

@ -22,9 +22,7 @@ BEGIN
) VALUES ( ) VALUES (
_user_id, _user_id,
NEW.content NEW.content
) );
RETURNING id
INTO NEW.id;
RETURN NEW; RETURN NEW;
END END

View file

@ -27,9 +27,8 @@ BEGIN
END IF; END IF;
IF _changed THEN IF _changed THEN
UPDATE admin.post SET UPDATE admin.post
content = NEW.content, SET content = NEW.content
modified = clock_timestamp()
WHERE id = OLD.id; WHERE id = OLD.id;
END IF; END IF;

View file

@ -27,6 +27,7 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user;
\i /db/rest/user/api_user_insert.sql; \i /db/rest/user/api_user_insert.sql;
\i /db/rest/user/api_user_update.sql; \i /db/rest/user/api_user_update.sql;
\i /db/rest/user/api_user_delete.sql; \i /db/rest/user/api_user_delete.sql;
\i /db/rest/user/api_avatar.sql;
-- post -- post
\i /db/rest/post/api_post.sql; \i /db/rest/post/api_post.sql;
@ -40,16 +41,6 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user;
\i /db/rest/comment/api_comment_update.sql; \i /db/rest/comment/api_comment_update.sql;
\i /db/rest/comment/api_comment_delete.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;
-- media
\i /db/rest/media/api_profile_avatar.sql;
\i /db/rest/media/api_profile_banner.sql;
-- login -- login
\i /db/rest/login/_api_sign_jwt.sql; \i /db/rest/login/_api_sign_jwt.sql;
\i /db/rest/login/_api_verify_jwt.sql; \i /db/rest/login/_api_verify_jwt.sql;

View file

@ -0,0 +1,22 @@
CREATE FUNCTION api.avatar(
user_id INTEGER DEFAULT 0
)
RETURNS sys."*/*"
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_mod INTEGER;
_name TEXT;
BEGIN
_mod = MOD(user_id, 24);
_name = 'default_avatar_' || _mod || '.png';
RETURN _api.serve_media(_name);
END
$BODY$;
GRANT EXECUTE ON FUNCTION api.avatar(INTEGER)
TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.user
TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.media
TO rest_anon, rest_user;

View file

@ -9,15 +9,13 @@ CREATE VIEW api.user AS
u.middle_name, u.middle_name,
u.email, u.email,
u.gender, u.gender,
u.join_date,
u.birth_date, u.birth_date,
u.profile_bio, u.profile_avatar,
u.created, u.profile_banner,
u.modified, u.profile_bio
u.seen
FROM FROM
admin.user u admin.user u;
WHERE
u.deleted <> TRUE;
GRANT SELECT ON TABLE api.user GRANT SELECT ON TABLE api.user
TO rest_anon, rest_user; TO rest_anon, rest_user;

View file

@ -11,10 +11,8 @@ BEGIN
PERFORM _api.raise_deny(); PERFORM _api.raise_deny();
END IF; END IF;
UPDATE admin.user SET DELETE FROM admin.user
deleted = TRUE, WHERE id = _user_id;
modified = clock_timestamp()
WHERE id = _user_id;
END END
$BODY$; $BODY$;
@ -22,7 +20,7 @@ GRANT EXECUTE ON FUNCTION _api.user_delete()
TO rest_user; TO rest_user;
GRANT DELETE ON TABLE api.user GRANT DELETE ON TABLE api.user
TO rest_user; TO rest_user;
GRANT UPDATE ON TABLE admin.user GRANT DELETE ON TABLE admin.user
TO rest_user; TO rest_user;
CREATE TRIGGER api_user_delete_trgr CREATE TRIGGER api_user_delete_trgr

View file

@ -104,9 +104,7 @@ BEGIN
NEW.gender, NEW.gender,
NEW.birth_date, NEW.birth_date,
NEW.profile_bio NEW.profile_bio
) );
RETURNING id
INTO NEW.id;
NEW.password := NULL; NEW.password := NULL;

View file

@ -145,8 +145,7 @@ BEGIN
email = NEW.email, email = NEW.email,
gender = NEW.gender, gender = NEW.gender,
birth_date = NEW.birth_date, birth_date = NEW.birth_date,
profile_bio = NEW.profile_bio, profile_bio = NEW.profile_bio
modified = clock_timestamp()
WHERE id = OLD.id; WHERE id = OLD.id;
END IF; END IF;

View file

@ -0,0 +1,11 @@
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$;

View file

@ -1,5 +1,5 @@
CREATE FUNCTION _api.serve_media( CREATE FUNCTION _api.serve_media(
_media_id INTEGER _name TEXT
) )
RETURNS sys."*/*" RETURNS sys."*/*"
LANGUAGE plpgsql VOLATILE LANGUAGE plpgsql VOLATILE
@ -15,13 +15,13 @@ BEGIN
'{"Cache-Control": "max-age=259200"}]' '{"Cache-Control": "max-age=259200"}]'
, m.type, m.name) , m.type, m.name)
FROM admin.media m FROM admin.media m
WHERE m.id = _media_id INTO _headers; WHERE m.name = _name INTO _headers;
PERFORM SET_CONFIG('response.headers', _headers, true); PERFORM SET_CONFIG('response.headers', _headers, true);
SELECT m.content SELECT m.content
FROM admin.media m FROM admin.media m
WHERE m.id = _media_id WHERE m.name = _name
INTO _data; INTO _data;
IF FOUND THEN IF FOUND THEN
@ -35,7 +35,7 @@ BEGIN
END END
$BODY$; $BODY$;
GRANT EXECUTE ON FUNCTION _api.serve_media(INTEGER) GRANT EXECUTE ON FUNCTION _api.serve_media(TEXT)
TO rest_anon, rest_user; TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.media GRANT SELECT ON TABLE admin.media
TO rest_anon, rest_user; TO rest_anon, rest_user;

View file

@ -5,7 +5,7 @@ services:
ports: ports:
- '80:80' - '80:80'
volumes: volumes:
- ./src:/opt/xssbook:ro - ./web:/opt/xssbook
- ./conf/nginx:/etc/nginx/conf.d:ro - ./conf/nginx:/etc/nginx/conf.d:ro
depends_on: depends_on:
- rest - rest
@ -18,9 +18,8 @@ services:
env_file: env_file:
- ./conf/postgres/database.env - ./conf/postgres/database.env
volumes: volumes:
- ./src:/opt/xssbook:ro - ./web:/opt/xssbook
- ./data/status:/status:ro - ./data/status:/status
- ./data/session:/var/lib/php/session
depends_on: depends_on:
- db - db
@ -34,7 +33,7 @@ services:
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
volumes: volumes:
- './data/schemas:/var/lib/postgresql/data' - './data/schemas:/var/lib/postgresql/data'
- ./src/db:/db:ro - ./db:/db:ro
rest: rest:
build: ./build/postgrest build: ./build/postgrest
@ -48,7 +47,7 @@ services:
env_file: env_file:
- ./conf/postgres/database.env - ./conf/postgres/database.env
volumes: volumes:
- ./src/db:/db:ro - ./db:/db:ro
- ./data/status:/status - ./data/status:/status
depends_on: depends_on:
- db - db

View file

@ -1,16 +0,0 @@
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;

View file

@ -1,32 +0,0 @@
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();

View file

@ -1,51 +0,0 @@
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();

View file

@ -1,44 +0,0 @@
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();

View file

@ -1,36 +0,0 @@
CREATE FUNCTION api.profile_avatar(
user_id INTEGER DEFAULT 0
)
RETURNS sys."*/*"
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_id INTEGER;
_mod INTEGER;
_name TEXT;
BEGIN
SELECT media_id INTO _id
FROM admin.user_media m
WHERE m.user_id = profile_avatar.user_id
AND type = 'avatar'::admin.user_media_type;
-- get default if not exists
IF NOT FOUND THEN
_mod = MOD(user_id, 24);
_name = 'default_avatar_' || _mod || '.png';
SELECT id INTO _id
FROM admin.media
WHERE name = _name;
END IF;
RETURN _api.serve_media(_id);
END
$BODY$;
GRANT EXECUTE ON FUNCTION api.profile_avatar(INTEGER)
TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.user_media
TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.media
TO rest_anon, rest_user;

View file

@ -1,13 +0,0 @@
CREATE FUNCTION api.profile_banner(
user_id INTEGER DEFAULT 0
)
RETURNS sys."*/*"
LANGUAGE plpgsql VOLATILE
AS $BODY$
BEGIN
PERFORM _api.raise_deny();
END
$BODY$;
GRANT EXECUTE ON FUNCTION api.profile_banner(INTEGER)
TO rest_anon, rest_user;

View file

@ -1,22 +0,0 @@
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;

View file

@ -1,45 +0,0 @@
#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;
}
}

View file

@ -1,69 +0,0 @@
.title {
margin-top: 2rem;
margin-left: 3rem;
font-size: 3rem;
margin-bottom: 0;
}
.desc {
margin-left: 3rem;
}
#people-container {
margin-left: auto;
margin-right: auto;
padding: 1rem 2rem;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-row-gap: 2rem;
grid-auto-rows: 1fr;
flex-direction: column;
align-items: center;
max-width: 90rem;
}
.profile {
margin: 1rem;
text-decoration: none;
height: 100%;
}
.profile:hover {
outline: 1px solid var(--blue);
}
.profile strong {
font-size: 2rem;
}
.profile .pfp, .profile .pfp img {
padding: none;
margin: none;
height: 6rem;
border-radius: .3rem;
}
@media(max-width: 1400px) {
#people-container {
max-width: 90rem;
grid-template-columns: 1fr 1fr;
}
}
@media(max-width: 1000px) {
#people-container {
max-width: 50rem;
grid-template-columns: 1fr;
}
}
td:nth-child(1) {
font-weight: bold;
color: var(--subtext);
padding-right: 1rem;
}
td:nth-child(2) {
color: var(--text);
}

Binary file not shown.

View file

@ -1,56 +0,0 @@
<?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;
}
}
?>

View file

@ -1,41 +0,0 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class People_controller extends Controller {
// the people model
private $people_model;
// format model
protected $format_model;
function __construct($load) {
parent::__construct($load);
$this->people_model = $this->load->model('apps/people');
$this->format_model = $this->load->model('format');
}
public function index(): void {
parent::index();
$data = $this->people_model->get_data();
$this->view('header', $data);
$this->view('apps/people/main', $data);
$this->view('footer', $data);
}
/**
* @return array<string,mixed>
*/
public function people(): array {
$data = $this->people_model->get_users();
$this->view('apps/people/people', $data);
$max = 0;
foreach ($data['users'] as $user) {
$max = max($max, $user['id']);
}
return $data;
}
}
?>

View file

@ -1,13 +0,0 @@
<?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;
}
}

View file

@ -1,88 +0,0 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class People_model extends Model {
private $request_model;
function __construct($load) {
parent::__construct($load);
$this->request_model = $this->load->model('request');
}
private function get_filted_query($select) {
$filter_username = $this->request_model->get_str('filter_username', FALSE);
$filter_fisrt_name = $this->request_model->get_str('filter_first_name', FALSE);
$filter_last_name = $this->request_model->get_str('filter_last_name', FALSE);
$filter_email = $this->request_model->get_str('filter_email', FALSE);
$max = $this->request_model->get_int('max', FALSE);
$query = $this->db
->select($select)
->from('api.user u');
if ($filter_username) {
$query = $query
->where('u.username')
->like('%' . $filter_username . '%');
}
if ($filter_fisrt_name) {
$query = $query
->where('u.first_name')
->like('%'. $filter_fisrt_name . '%');
}
if ($filter_last_name) {
$query = $query
->where('u.last_name')
->like('%' . $filter_last_name . '%');
}
if ($filter_email) {
$query = $query
->where('u.email')
->like('%' . $filter_email . '%');
}
if ($max) {
$query = $query
->where('u.id')
->le($max);
}
return $query;
}
public function get_users(): array {
$page = $this->request_model->get_int('page', 0);
$page_size = 24;
$offset = $page_size * $page;
$users = $this->get_filted_query('*')
->order_by('u.id', 'DESC')
->offset($offset)
->limit($page_size)
->rows();
$count = $this->get_filted_query('COUNT(u.id) AS count')
->row()['count'];
$max = 0;
foreach ($users as $user) {
$max = max($max, $user['id']);
}
return array(
'users' => $users,
'count' => $count,
'page_size' => $page_size,
'max_id' => $max
);
}
public function get_data(): array {
$data = parent::get_data();
$data['title'] = lang('title');
return $data;
}
}

View file

@ -1,86 +0,0 @@
<?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>

View file

@ -1,35 +0,0 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<a
class="card profile"
href="/profile?id=<?=$user['id']?>"
>
<div class="row">
<?php $this->view('template/pfp', array('user' => $user, 'link' => FALSE)); ?>
<div class="col ml">
<strong class=""><?=$this->format_model->name($user)?></strong>
<span class="dim"><?=lang('joined') . ' ' . $this->format_model->date($user['created'])?></span>
<span class="dim"><?=lang('seen') . ' ' . $this->format_model->date($user['seen'])?></span>
</div>
</div>
<hr>
<table>
<tr>
<td><?=lang('tbl_username')?></td>
<td><?=$user['username']?></td>
<tr>
<tr>
<td><?=lang('tbl_email')?></td>
<td><?=$user['email']?></td>
<tr>
<tr>
<td><?=lang('tbl_gender')?></td>
<td><?=$user['gender']?></td>
<tr>
<tr>
<td><?=lang('tbl_uid')?></td>
<td><?=$user['id']?></td>
<tr>
</table>
</a>
<?

View file

@ -1,67 +0,0 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<div id="main-content" class="col">
<h1 class="title"><?=lang('title')?></h1>
<h3 class="desc"><?=lang('desc')?></h3>
<hr>
<div id="people-container" class="col">
<?php
$pdata = $this->people();
?>
</div>
<?php
$loaded = count($pdata['users']);
$page_size = $pdata['page_size'];
$total = $pdata['count'];
$max = $pdata['max_id'];
?>
<?php if ($loaded >= $page_size && $page_size < $total): ?>
<?=ilang('action_load_users',
id: 'action-load-users',
class: 'btn btn-line btn-wide mt mb',
attrs: array(
'loaded' => $loaded,
'pageSize' => $page_size,
'userCount' => $total,
'userMax' => $max
)
)?>
<script>
var urlParams = new URLSearchParams(window.location.search).toString();
$('#action-load-users').on('click', function() {
let me = $(this);
let page = me.attr('page');
if (!page) {
page = '1';
}
let newPage = Number(page) + 1;
me.attr('page', newPage + '');
let loaded = Number(me.attr('loaded'));
let pageSize = Number(me.attr('pageSize'));
let userCount = Number(me.attr('userCount'));
let userMax = Number(me.attr('userMax'));
let url = '/people/people?page=' + page + '&max=' + userMax + '&' + urlParams;
$.get(url, function (data) {
if (data === '') {
me.remove();
return;
}
let container = $('#people-container');
container.append(data);
loaded += pageSize;
if (loaded >= userCount) {
me.remove();
} else {
me.attr('loaded', loaded + '');
}
});
});
</script>
<?php endif ?>
</div>

View file

@ -1,7 +0,0 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<?php
foreach($users as $user) {
$this->view('apps/people/card', array('user' => $user));
}
?>

View file

@ -1,23 +0,0 @@
<!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>

View file

@ -1,173 +0,0 @@
<?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>

View file

@ -1,17 +0,0 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<?php
$class = isset($class) ? $class : '';
$link = isset($link) ? $link : TRUE;
?>
<?php if($link): ?>
<a class="image-loading pfp <?=$class?>" href="/profile?id=<?=$user['id']?>">
<?php else: ?>
<div class="image-loading pfp <?=$class?>">
<?php endif; ?>
<img src="/api/rpc/profile_avatar?user_id=<?=$user['id']?>"/>
<?php if ($link): ?>
</a>
<?php else: ?>
</div>
<?php endif; ?>

View file

@ -1,34 +0,0 @@
<?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';
?>

View file

@ -1,17 +0,0 @@
<?php
$lang['title'] = 'Directory';
$lang['desc'] = 'Explore other people on xssbook!';
$lang['joined'] = 'Joined: ';
$lang['seen'] = 'Seen: ';
$lang['tbl_username'] = 'Username';
$lang['tbl_email'] = 'Email ';
$lang['tbl_uid'] = 'User ID ';
$lang['tbl_gender'] = 'Gender ';
$lang['action_load_users_text'] = 'Load more users';
$lang['action_load_users_tip'] = 'Load more users';
?>

View file

@ -14,7 +14,7 @@ class _index_controller extends Controller {
if ($this->main->session) { if ($this->main->session) {
$this->redirect('/home'); $this->redirect('/home');
} else { } else {
$this->redirect('/auth/login'); $this->redirect('/login');
} }
} }

View file

@ -21,39 +21,6 @@ class Post_controller extends Controller {
$this->view('template/posts'); $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> * @return array<string,mixed>
*/ */
@ -63,23 +30,28 @@ class Post_controller extends Controller {
$offset = $page * $this->page_size; $offset = $page * $this->page_size;
$user = $this->main->user(); $user = $this->main->user();
$uid = isset($user) ? $user['id'] : NULL;
$query = $this->db; $query = $this->db;
$query = $this->db if ($user) {
->select('p.*, l.id as like_id') $query = $query->select('p.*, l.post_id IS NOT NULL as liked');
->from('api.post p') } else {
->join('api.like l', 'p.id = l.post_id AND l.user_id') $query = $query->select('p.*, FALSE as liked');
->eq($uid); }
$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']);
}
if ($max) { if ($max) {
$query = $query $query = $query
->where('p.id')->le($max); ->where('id')->le($max);
} }
$posts = $query $posts = $query
->order_by('p.id', 'DESC')
->limit($this->page_size) ->limit($this->page_size)
->offset($offset) ->offset($offset)
->rows(); ->rows();
@ -101,6 +73,7 @@ class Post_controller extends Controller {
->from('api.post p') ->from('api.post p')
->row()['pc']; ->row()['pc'];
return array( return array(
'loaded' => count($posts), 'loaded' => count($posts),
'total' => $pc, 'total' => $pc,
@ -109,36 +82,6 @@ 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> * @return array<string,mixed>
*/ */
@ -162,7 +105,6 @@ class Post_controller extends Controller {
} }
$comments = $query $comments = $query
->order_by('id', 'ASC')
->limit($this->page_size) ->limit($this->page_size)
->offset($offset) ->offset($offset)
->rows(); ->rows();
@ -170,17 +112,6 @@ class Post_controller extends Controller {
$users = $this->cache_model->get_users($comments); $users = $this->cache_model->get_users($comments);
$max = 0; $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) { foreach ($comments as $comment) {
$max = max($max, $comment['id']); $max = max($max, $comment['id']);
$data = array(); $data = array();

View file

@ -8,12 +8,11 @@ class Error_controller extends Controller {
$this->error_model = $this->load->model('apps/error'); $this->error_model = $this->load->model('apps/error');
} }
public function index(): void { public function index() {
parent::index(); parent::index();
$data = $this->error_model->get_data(); $data = $this->error_model->get_data();
$this->view('header', $data); $this->view('header', $data);
$this->view('apps/error/main', $data); $this->view('apps/error/main', $data);
$this->view('footer', $data);
} }
} }

View file

@ -18,7 +18,6 @@ class Home_controller extends Controller {
$data = $this->home_model->get_data(); $data = $this->home_model->get_data();
$this->view('header', $data); $this->view('header', $data);
$this->view('apps/home/main', $data); $this->view('apps/home/main', $data);
$this->view('footer', $data);
} }
} }

View file

@ -20,14 +20,6 @@ class Modal_controller extends Controller {
public function new_post(): void { public function new_post(): void {
$this->modal('new_post'); $this->modal('new_post');
} }
public function register(): void {
$this->load->app_lang(
$this->main->info['lang'],
'auth'
);
$this->modal('register');
}
} }
?> ?>

View file

@ -7,18 +7,22 @@ class Error_model extends Model {
private function get_msg(&$data) { private function get_msg(&$data) {
if (!array_key_exists('code', $_GET)) { if (!array_key_exists('code', $_GET)) {
http_response_code(500);
$data['msg'] = lang('error'); $data['msg'] = lang('error');
$data['title'] = '500'; $data['title'] = '500';
} else { } else {
$code = $_GET['code']; $code = $_GET['code'];
http_response_code($code);
$data['title'] = $code; $data['title'] = $code;
$msg = lang('error_' . $code, FALSE); switch ($code) {
if (!$msg) { case '404':
$msg = lang('error'); $data['msg'] = lang('error_404');
break;
case '500':
$data['msg'] = lang('error_500');
break;
default:
$data['msg'] = lang('error');
break;
} }
$data['msg'] = $msg;
} }
} }

View file

@ -39,8 +39,7 @@ class Format_model extends Model {
* @returns the formatted date * @returns the formatted date
*/ */
public function date($date) { public function date($date) {
$date=date_create($date); return $date;
return date_format($date, "Y-m-d H:i");
} }
} }

View file

@ -53,7 +53,7 @@ class Main_model {
*/ */
private function asset_stamp($path): int { private function asset_stamp($path): int {
$root = $GLOBALS['webroot']; $root = $GLOBALS['webroot'];
$path = $root . '/../public/' . $path; $path = $root . '/public/' . $path;
return filemtime($path); return filemtime($path);
} }

View file

@ -1,6 +1,6 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?> <?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?> <?php /* vi: syntax=php */ ?>
<div id="main-content"> <div id="error">
<h1><?=$title?></h1> <h1><?=$title?></h1>
<span><?=$msg?></span> <span><?=$msg?></span>
</div> </div>

View file

@ -7,7 +7,7 @@
<?php $this->view('template/pfp', array('user' => $self))?> <?php $this->view('template/pfp', array('user' => $self))?>
<a <a
id="action-new-post" id="action-new-post"
class="btn btn-alt btn-wide ml" class="input btn-fake ml"
autocomplete="off" autocomplete="off"
aria-label="<?=lang('action_new_post_tip')?>" aria-label="<?=lang('action_new_post_tip')?>"
> >

View file

@ -1,8 +1,4 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?> <?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?> <?php /* vi: syntax=php */ ?>
<footer>
Freya Murphy © 2023 | <a href="https://freya.cat">freya.cat</a>
</footer>
<body> <body>
</html> </html>

View file

@ -2,8 +2,28 @@
<?php /* vi: syntax=php */ ?> <?php /* vi: syntax=php */ ?>
<?php <?php
$self = $this->main->user(); $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"> <header class="nav">
<div class="nav-left"> <div class="nav-left">
<span class="logo">xssbook</span> <span class="logo">xssbook</span>
@ -11,7 +31,7 @@
<div class="nav-center" :class="{hidden: !visible}"> <div class="nav-center" :class="{hidden: !visible}">
<a <a
id="action-home" id="action-home"
class="btn" class="header-entry btn btn-hover btn-action btn-blue"
href="/home" href="/home"
title="<?=lang('action_home_tip')?>" title="<?=lang('action_home_tip')?>"
> >
@ -20,7 +40,7 @@
</a> </a>
<a <a
id="action-people" id="action-people"
class="btn" class="header-entry btn btn-hover btn-action btn-blue"
href="/people" href="/people"
title="<?=lang('action_people_tip')?>" title="<?=lang('action_people_tip')?>"
> >
@ -29,7 +49,7 @@
</a> </a>
<a <a
id="action-chat" id="action-chat"
class="btn" class="header-entry btn btn-hover btn-action btn-blue"
href="/chat" href="/chat"
title="<?=lang('action_chat_tip')?>" title="<?=lang('action_chat_tip')?>"
> >
@ -50,7 +70,7 @@
'class' => 'pfp-sm ml', 'class' => 'pfp-sm ml',
)); ?> )); ?>
<?php else: ?> <?php else: ?>
<?=ilang('action_login', class: 'btn', href: '/auth/login')?> <?=ilang('action_login', class: 'btn btn-action', href: '/auth/login')?>
<?php endif; ?> <?php endif; ?>
</div> </div>
<script> <script>
@ -60,3 +80,5 @@
}); });
</script> </script>
</header> </header>
<div id="toast-container">
</div>

View file

@ -22,7 +22,7 @@
<div class="modal-footer"> <div class="modal-footer">
<?=ilang('action_submit', <?=ilang('action_submit',
id: 'new-post-submit', id: 'new-post-submit',
class: 'btn btn-wide btn-submit', class: 'btn-action',
attrs: array('type' => 'submit'), attrs: array('type' => 'submit'),
button: TRUE button: TRUE
)?> )?>
@ -32,28 +32,14 @@
$('#new-post-form').submit(function(e) { $('#new-post-form').submit(function(e) {
e.preventDefault(); e.preventDefault();
let content = $('#new-post-content').val(); 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({ $.ajax({
url: '/api/post', url: '/api/post',
method: 'POST', method: 'POST',
data: JSON.stringify({ content }), data: JSON.stringify({ content }),
success: onPost success: function(data) {
window.location.reload();
},
}); });
}); });
</script> </script>

View file

@ -8,7 +8,7 @@
<div class="ml col sub-card"> <div class="ml col sub-card">
<div class="row"> <div class="row">
<strong><?=$format_model->name($user)?></strong> <strong><?=$format_model->name($user)?></strong>
<span class="dim ml"><?=$format_model->date($comment['created'])?></span> <span class="dim ml"><?=$format_model->date($comment['date'])?></span>
</div> </div>
<?=$comment['content']?> <?=$comment['content']?>
</div> </div>

View file

@ -0,0 +1,8 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<?php
$class = isset($class) ? $class : '';
?>
<a class="image-loading pfp <?=$class?>" href="/profile?id=<?=$user['id']?>">
<img src="/api/rpc/avatar?user_id=<?=$user['id']?>" />
</a>

View file

@ -5,7 +5,7 @@
<?php $this->view('template/pfp', array('user' => $user))?> <?php $this->view('template/pfp', array('user' => $user))?>
<div class="col ml"> <div class="col ml">
<strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong> <strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong>
<span class="dim"><?=$post['created']?></span> <span class="dim"><?=$post['date']?></span>
</div> </div>
</div> </div>
<p> <p>
@ -13,28 +13,20 @@
</p> </p>
<?php <?php
$self = $this->main->user(); $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): ?> <?php if ($self): ?>
<hr> <hr>
<div class="row"> <div class="row">
<?=ilang('action_like', <?=ilang('action_like', class: 'grow btn btn-hover btn-action')?>
class: 'btn btn-wide action-like ' . $liked, <?=ilang('action_comment', class: 'grow btn btn-hover btn-action action-comment',
attrs: $post_attrs click: '$(\'#new-comment-' . $post['id'] . '\').focus()'
)?>
<?=ilang('action_comment', class: 'btn btn-wide action-comment',
click: '$(\'#action-new-comment-' . $post['id'] . '\').focus()'
)?> )?>
</div> </div>
<hr> <hr>
<?php else: ?>
<hr>
<?php endif; ?> <?php endif; ?>
<div class="col comments pb"> <div class="col comments">
<?php <?php
$_GET = array('id' => $post['id']); $_GET = array('id' => $post['id']);
$cdata = $this->comments(); $cdata = $this->comments();
@ -60,17 +52,16 @@
?> ?>
</div> </div>
<?php if ($self): ?> <?php if ($self): ?>
<div class="row pb"> <div class="row grow mt">
<?php $this->view('template/pfp', array('user' => $self))?> <?php $this->view('template/pfp', array('user' => $user))?>
<form class="ml action-new-comment-form row"> <form class="ml action-new-comment-form">
<input <input
type="hidden" type="hidden"
name="id" name="id"
value="<?=$post['id']?>" value="<?=$post['id']?>"
> >
<input <input
id="action-new-comment-<?=$post['id']?>" class="action-new-comment input"
class="action-new-comment btn btn-wide btn-alt"
postId="<?=$post['id']?>" postId="<?=$post['id']?>"
autocomplete="off" autocomplete="off"
type="text" type="text"
@ -82,5 +73,3 @@
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>

View file

@ -10,7 +10,7 @@
if ($loaded >= $page_size && $page_size < $total) { if ($loaded >= $page_size && $page_size < $total) {
ilang('action_load_posts', ilang('action_load_posts',
id: 'action-load-posts', id: 'action-load-posts',
class: 'btn btn-line btn-wide mb', class: 'btn btn-line mb',
attrs: array( attrs: array(
'loaded' => $loaded, 'loaded' => $loaded,
'pageSize' => $page_size, 'pageSize' => $page_size,

View file

@ -11,16 +11,9 @@
array_push($params, $hint); array_push($params, $hint);
} }
$lang_msg = lang($msg, FALSE, sub: $params); $msg = lang($msg, sub: $params);
if(!$lang_msg) {
$lang_msg = $msg;
} else {
$lang_msg = ucfirst($lang_msg);
}
?> ?>
<div class="toast error"> <div class="toast error">
<?=$lang_msg?> <?=ucfirst($msg)?>
<?=ilang('action_close', class: 'action-close-toast')?> <?=ilang('action_close', class: 'action-close-toast')?>
</div> </div>

View file

@ -30,16 +30,6 @@ class Aesthetic {
'css/post.css' 'css/post.css'
], ],
), ),
'auth' => array(
'css' => [
'css/auth.css'
],
),
'people' => array(
'css' => [
'css/people.css'
],
),
); );
} }
/** /**

View file

@ -3,7 +3,5 @@
$routes = array(); $routes = array();
$routes['home'] = 'apps/home'; $routes['home'] = 'apps/home';
$routes['error'] = 'apps/error'; $routes['error'] = 'apps/error';
$routes['auth'] = 'apps/auth';
$routes['people'] = 'apps/people';
$routes[''] = '_index'; $routes[''] = '_index';

View file

@ -45,20 +45,5 @@ 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();
}
} }
?> ?>

View file

@ -46,8 +46,6 @@ class DatabaseQuery {
if (!$this->where) { if (!$this->where) {
$this->where = TRUE; $this->where = TRUE;
$this->query .= "WHERE "; $this->query .= "WHERE ";
} else {
$this->query .= "AND ";
} }
$this->query .= "$cond "; $this->query .= "$cond ";
return $this; return $this;
@ -87,8 +85,6 @@ class DatabaseQuery {
if (!$this->where) { if (!$this->where) {
$this->where = TRUE; $this->where = TRUE;
$this->query .= "WHERE "; $this->query .= "WHERE ";
} else {
$this->query .= "AND ";
} }
if (empty($array)) { if (empty($array)) {
$this->query .= "FALSE\n"; $this->query .= "FALSE\n";
@ -99,6 +95,16 @@ class DatabaseQuery {
return $this; return $this;
} }
public function and() {
$this->query .= "AND ";
return $this;
}
public function or() {
$this->query .= "OR ";
return $this;
}
public function join($table, $on, $type = 'LEFT') { public function join($table, $on, $type = 'LEFT') {
$this->query .= "$type JOIN $table ON $on\n"; $this->query .= "$type JOIN $table ON $on\n";
return $this; return $this;
@ -116,11 +122,6 @@ class DatabaseQuery {
return $this; return $this;
} }
public function order_by($column, $order = 'ASC') {
$this->query .= "ORDER BY " . $column . ' ' . $order . ' ';
return $this;
}
public function rows() { public function rows() {
$stmt = $this->conn->prepare($this->query); $stmt = $this->conn->prepare($this->query);
try { try {

View file

@ -69,7 +69,7 @@ function ilang($key,
} }
echo '>' . $text . '</span>'; echo '>' . $text . '</span>';
} }
if ($click || $button) { if ($click) {
echo '</button>'; echo '</button>';
} else { } else {
echo '</a>'; echo '</a>';

View file

@ -1,6 +1,5 @@
<?php /* Copyright (c) 2024 Freya Murphy */ <?php /* Copyright (c) 2024 Freya Murphy */
session_save_path('/var/lib/php/session');
session_start(); session_start();
$webroot = dirname(__FILE__); $webroot = dirname(__FILE__);

View file

@ -6,7 +6,6 @@ $lang['api_column_first_name'] = 'first name';
$lang['api_column_last_name'] = 'last name'; $lang['api_column_last_name'] = 'last name';
$lang['api_column_middle_name'] = 'middle name'; $lang['api_column_middle_name'] = 'middle name';
$lang['api_column_email'] = 'email'; $lang['api_column_email'] = 'email';
$lang['api_column_password'] = 'password';
$lang['api_column_gender'] = 'gender'; $lang['api_column_gender'] = 'gender';
$lang['api_column_join_date'] = 'join date'; $lang['api_column_join_date'] = 'join date';
$lang['api_column_birth_date'] = 'birth date'; $lang['api_column_birth_date'] = 'birth date';
@ -23,10 +22,5 @@ $lang['api_null_value'] = '%s cannot be empty';
$lang['api_unique_value'] = '%s is not available (not unique)'; $lang['api_unique_value'] = '%s is not available (not unique)';
$lang['api_min_value'] = '%s length cannot be less than %s'; $lang['api_min_value'] = '%s length cannot be less than %s';
$lang['api_max_value'] = '%s length cannot exceed %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';
?> ?>

View file

@ -1,44 +1,17 @@
:root { :root {
--white: #E4E6EB; --primary: #242424 !important;
--blue: #1778f2; --secondary: #181818 !important;
--red: #f02849; --hover: #1b1b1b !important;
--green: #30ab5a; --light: #3e4042 !important;
--mild: #1b1b1b !important;
--blue-alt: #1D85FC; --medium: #e2ded6 !important;
--green-alt: #39B463; --extreme: #e2ded6 !important;
--logo: #1778f2 !important;
--font: Helvetica; --error: #f02849 !important;
} --success: #30ab5a !important;
--text: #ffffff !important;
:root { --banner: #6b6b6b !important;
--base :#18191A; --popup: #242424cc !important;
--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 { @font-face {
@ -60,8 +33,14 @@
font-display: swap; font-display: swap;
} }
@font-face {
font-family: sfprobold;
src: url("/public/font/sfprobold.otf") format("opentype");
font-display: swap;
}
body { body {
background-color: var(--surface0); background-color: var(--secondary);
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0; margin: 0;
@ -69,132 +48,93 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
color: var(--text); color: var(--text);
font-family: var(--font); font-family: sfpro;
}
#main-content {
background-color: var(--base);
padding-top: 1rem;
} }
header { header {
top: 0; top: 0;
position: sticky; position: sticky;
height: 3.5rem; height: 3.5rem;
background-color: var(--surface0); background-color: var(--primary);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 0 1rem; padding: 0 1rem;
border-bottom: 1px solid var(--surface1);
} }
header .logo { header .logo {
font-family: facebook; font-family: facebook;
color: var(--blue); color: var(--logo);
font-size: 2.25rem; font-size: 2.25rem;
height: 100%; height: 100%;
line-height: 2rem; line-height: 2rem;
margin-top: .75rem; 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 { a, button, input {
background: none; background: none;
border: none; border: none;
display: flex;
flex-direction: row;
align-items: center;
font-family: sfprobold;
color: inherit; color: inherit;
text-decoration: none;
font-size: 1rem;
} }
a, button { a, button {
cursor: pointer; cursor: pointer;
} }
.btn { form button {
color: var(--btntext); padding: .5rem;
display: flex; border-radius: .5rem;
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;
} }
.btn:hover { input:focus {
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; border: none;
outline: none; outline: none;
} }
.btn-submit { .header-entry {
color: var(--white); display: flex;
background-color: var(--blue); flex-direction: row;
flex-grow: 1; text-decoration: none;
padding: .5rem; align-items: center;
color: var(--text);
} }
.btn-submit:hover { .nav .header-entry {
background-color: var(--blue-alt); height: 100%;
} }
.btn-success { .nav-center .header-entry:hover {
color: var(--white); background-color: var(--hover);
background-color: var(--green);
flex-grow: 1;
padding: .5rem;
} }
.btn-success:hover { .btn-action {
background-color: var(--green-alt); 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;
} }
.nav, .nav,
@ -227,13 +167,12 @@ input.btn:focus {
} }
@media (min-width: 800px) { @media (min-width: 800px) {
.nav-center .btn > span { .header-entry > span {
display: none; display: none;
} }
.nav-center .btn { .nav-center .header-entry {
padding: 0 3rem; padding: 0 3rem;
height: 100%;
} }
#action-hamburger { #action-hamburger {
@ -248,7 +187,7 @@ input.btn:focus {
flex-direction: column; flex-direction: column;
top: 100%; top: 100%;
height: fit-content; height: fit-content;
background-color: var(--surface0); background-color: var(--primary);
width: 100%; width: 100%;
left: 0; left: 0;
transform: translateX(0%); transform: translateX(0%);
@ -259,18 +198,18 @@ input.btn:focus {
display: inherit !important; display: inherit !important;
} }
.nav-center .btn { .nav-center .header-entry {
width: calc(100% - 3rem); width: calc(100% - 3rem);
padding: .75rem 0rem !important; padding: .75rem 0rem !important;
padding-left: 3rem !important; padding-left: 3rem !important;
justify-content: flex-start; justify-content: flex-start;
} }
.nav-center .btn > span { .nav-center .header-entry > span {
margin-left: 1rem; margin-left: 1rem;
} }
.nav-center .btn.active { .nav-center .header-entry.active {
border-bottom: none; border-bottom: none;
} }
} }
@ -279,6 +218,11 @@ input.btn:focus {
display: block; display: block;
} }
.nav-right .header-entry {
padding: 0;
padding-left: 1.5rem;
}
@keyframes shimmer { @keyframes shimmer {
to { to {
background-position-x: 0%; background-position-x: 0%;
@ -298,32 +242,40 @@ input.btn:focus {
} }
.image-loading { .image-loading {
background: linear-gradient(-45deg, var(--surface0) 0%, var(--base) 25%, var(--surface0) 50%); background: linear-gradient(-45deg, var(--secondary) 0%, var(--primary) 25%, var(--secondary) 50%);
background-size: 500%; background-size: 500%;
background-position-x: 150%; background-position-x: 150%;
animation: shimmer 1s linear infinite; animation: shimmer 1s linear infinite;
} }
.card { .card {
background-color: var(--surface0); background-color: var(--primary);
border-radius: .5rem; border-radius: .5rem;
padding: 1rem; padding: 1rem;
} }
.card p {
margin-bottom: 0;
}
.card form { .card form {
flex-grow: 1; flex-grow: 1;
} }
.card .sub-card { .card .sub-card {
background-color: var(--surface1); background-color: var(--secondary);
border-radius: .5rem; border-radius: .5rem;
padding: .75rem; 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 { .row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -362,12 +314,8 @@ input.btn:focus {
margin-bottom: .75rem; margin-bottom: .75rem;
} }
.pb {
padding-bottom: 1rem;
}
.dim { .dim {
color: var(--subtext); color: var(--medium);
} }
.modal-container { .modal-container {
@ -381,7 +329,7 @@ input.btn:focus {
} }
.modal { .modal {
background-color: var(--surface0); background-color: var(--primary);
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
@ -424,14 +372,18 @@ input.btn:focus {
} }
.modal-header { .modal-header {
font-weight: bold; font-family: sfprobold;
position: relative; position: relative;
border-bottom: 1px solid var(--surface1); border-bottom: 1px solid var(--light);
text-align: center;
margin: 0 1rem;
border-radius: .5rem .5rem 0 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding-left: 1rem;
cursor: grab; cursor: grab;
padding: 1rem 0; padding: 1rem;
} }
.modal-content { .modal-content {
@ -450,9 +402,9 @@ input.btn:focus {
.float-right { .float-right {
position: absolute; position: absolute;
top: 50%; transform: translate(0%, -50%);
left: 100%; top: 45%;
transform: translate(-125%, -50%); right: 0;
} }
.mi { .mi {
@ -469,6 +421,17 @@ input.btn:focus {
font-size: 2rem; 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 { #toast-container {
position: fixed; position: fixed;
top: 4rem; top: 4rem;
@ -479,92 +442,20 @@ input.btn:focus {
} }
.toast { .toast {
color: var(--white);
padding: .75rem; padding: .75rem;
margin: .5rem; margin: .5rem;
border-radius: .5rem; border-radius: .5rem;
min-width: 15rem; min-width: 15rem;
font-family: sfpro;
animation: fadeIn .1s, slideIn .25s linear; animation: fadeIn .1s, slideIn .25s linear;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.toast.error { .toast.error {
background-color: var(--red); background-color: var(--error);
} }
.toast.success { .toast.success {
background-color: var(--green); background-color: var(--success);
}
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;
} }

View file

@ -1,16 +1,16 @@
#main-content { #error {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 10rem 0; margin-top: 10rem;
} }
#main-content h1 { #error h1 {
color: var(--blue); color: var(--logo);
font-family: Facebook; font-family: Facebook;
font-size: 5rem; font-size: 5rem;
} }
#main-content span { #error span {
font-size: 2rem; font-size: 2rem;
} }

View file

@ -1,7 +1,9 @@
#main-content { #main-content {
width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-top: 1rem;
} }
.card { .card {
@ -13,6 +15,7 @@
border: none; border: none;
resize: none; resize: none;
outline: none; outline: none;
font-family: sfpro;
font-size: 1.5rem; font-size: 1.5rem;
margin: 1rem 0; margin: 1rem 0;
width: 100%; width: 100%;
@ -20,5 +23,4 @@
flex-grow: 1; flex-grow: 1;
background-color: transparent; background-color: transparent;
color: var(--text); color: var(--text);
font-family: var(--font);
} }

View file

@ -1,13 +1,16 @@
.post hr {
color: var(--light);
margin: 0;
}
.post hr:nth-of-type(1) {
margin-top: .5rem;
}
.action-load-comments { .action-load-comments {
margin-left: 4rem; margin-left: 4rem;
} }
#action-load-posts { #action-load-posts {
width: 100%;
justify-content: center; justify-content: center;
} }
.post {
padding-bottom: 0;
}

View file

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Some files were not shown because too many files have changed in this diff Show more