Compare commits

..

3 commits

109 changed files with 1712 additions and 333 deletions

View file

@ -1,4 +1,4 @@
FROM php:fpm-alpine 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-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,6 +87,7 @@ 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

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

@ -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$;

View file

@ -5,7 +5,7 @@ services:
ports: ports:
- '80:80' - '80:80'
volumes: volumes:
- ./web:/opt/xssbook - ./src:/opt/xssbook:ro
- ./conf/nginx:/etc/nginx/conf.d:ro - ./conf/nginx:/etc/nginx/conf.d:ro
depends_on: depends_on:
- rest - rest
@ -18,8 +18,9 @@ services:
env_file: env_file:
- ./conf/postgres/database.env - ./conf/postgres/database.env
volumes: volumes:
- ./web:/opt/xssbook - ./src:/opt/xssbook:ro
- ./data/status:/status - ./data/status:/status:ro
- ./data/session:/var/lib/php/session
depends_on: depends_on:
- db - db
@ -33,7 +34,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'
- ./db:/db:ro - ./src/db:/db:ro
rest: rest:
build: ./build/postgrest build: ./build/postgrest
@ -47,7 +48,7 @@ services:
env_file: env_file:
- ./conf/postgres/database.env - ./conf/postgres/database.env
volumes: volumes:
- ./db:/db:ro - ./src/db:/db:ro
- ./data/status:/status - ./data/status:/status
depends_on: depends_on:
- db - db

View file

@ -50,11 +50,12 @@ 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_avatar BYTEA, profile_bio TEXT DEFAULT ''::text NOT NULL,
profile_banner BYTEA, created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
profile_bio TEXT DEFAULT ''::text 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; ALTER TABLE admin.user OWNER TO xssbook;
@ -78,7 +79,9 @@ 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,
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,
deleted BOOLEAN DEFAULT FALSE NOT NULL
); );
ALTER TABLE admin.post OWNER TO xssbook; ALTER TABLE admin.post OWNER TO xssbook;
@ -101,7 +104,9 @@ CREATE TABLE admin.comment (
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,
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,
deleted BOOLEAN DEFAULT FALSE NOT NULL
); );
ALTER TABLE admin.comment OWNER TO xssbook; ALTER TABLE admin.comment OWNER TO xssbook;
@ -115,15 +120,28 @@ 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,
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 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;
@ -133,16 +151,32 @@ 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,
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 admin.follow OWNER TO xssbook;
ALTER TABLE ONLY admin.follow 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 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;
@ -150,16 +184,64 @@ 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 (name); ADD CONSTRAINT media_pkey PRIMARY KEY (id);
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,10 +4,24 @@ CREATE VIEW api.comment AS
c.user_id, c.user_id,
c.post_id, c.post_id,
c.content, c.content,
c.date c.created,
c.modified
FROM FROM
admin.comment c 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 GRANT SELECT ON TABLE api.comment
TO rest_anon, rest_user; TO rest_anon, rest_user;

View file

@ -11,9 +11,10 @@ BEGIN
PERFORM _api.raise_deny(); PERFORM _api.raise_deny();
END IF; END IF;
DELETE FROM admin.comment UPDATE admin.comment SET
WHERE user_id = _user_id deleted = TRUE,
AND id = OLD.id; modified = clock_timestamp()
WHERE id = OLD.id;
END END
$BODY$; $BODY$;
@ -21,7 +22,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 DELETE ON TABLE admin.comment GRANT UPDATE 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,7 +34,9 @@ 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,8 +27,9 @@ BEGIN
END IF; END IF;
IF _changed THEN IF _changed THEN
UPDATE admin.comment UPDATE admin.comment SET
SET content = NEW.content content = NEW.content,
modified = clock_timestamp()
WHERE id = OLD.id; WHERE id = OLD.id;
END IF; END IF;

View file

@ -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;

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -8,6 +8,7 @@ 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
@ -28,7 +29,13 @@ BEGIN
RETURN NULL; RETURN NULL;
END IF; 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 END
$BODY$; $BODY$;
@ -36,3 +43,5 @@ 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

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

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

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

View file

@ -11,9 +11,10 @@ BEGIN
PERFORM _api.raise_deny(); PERFORM _api.raise_deny();
END IF; END IF;
DELETE FROM admin.post UPDATE admin.post SET
WHERE user_id = _user_id deleted = TRUE,
AND id = OLD.id; modified = clock_timestamp()
WHERE id = OLD.id;
END END
$BODY$; $BODY$;
@ -21,7 +22,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 DELETE ON TABLE admin.post GRANT UPDATE 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,7 +22,9 @@ 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,8 +27,9 @@ BEGIN
END IF; END IF;
IF _changed THEN IF _changed THEN
UPDATE admin.post UPDATE admin.post SET
SET content = NEW.content content = NEW.content,
modified = clock_timestamp()
WHERE id = OLD.id; WHERE id = OLD.id;
END IF; END IF;

View file

@ -27,7 +27,6 @@ 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;
@ -41,6 +40,16 @@ 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

@ -9,13 +9,15 @@ 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_avatar, u.profile_bio,
u.profile_banner, u.created,
u.profile_bio u.modified,
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,7 +11,9 @@ BEGIN
PERFORM _api.raise_deny(); PERFORM _api.raise_deny();
END IF; END IF;
DELETE FROM admin.user UPDATE admin.user SET
deleted = TRUE,
modified = clock_timestamp()
WHERE id = _user_id; WHERE id = _user_id;
END END
$BODY$; $BODY$;
@ -20,7 +22,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 DELETE ON TABLE admin.user GRANT UPDATE 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,7 +104,9 @@ 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,7 +145,8 @@ 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,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;

View file

@ -1,5 +1,5 @@
CREATE FUNCTION _api.serve_media( CREATE FUNCTION _api.serve_media(
_name TEXT _media_id INTEGER
) )
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.name = _name INTO _headers; WHERE m.id = _media_id 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.name = _name WHERE m.id = _media_id
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(TEXT) GRANT EXECUTE ON FUNCTION _api.serve_media(INTEGER)
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;

45
src/public/css/auth.css Normal file
View file

@ -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;
}
}

View file

@ -1,17 +1,44 @@
:root { :root {
--primary: #242424 !important; --white: #E4E6EB;
--secondary: #181818 !important; --blue: #1778f2;
--hover: #1b1b1b !important; --red: #f02849;
--light: #3e4042 !important; --green: #30ab5a;
--mild: #1b1b1b !important;
--medium: #e2ded6 !important; --blue-alt: #1D85FC;
--extreme: #e2ded6 !important; --green-alt: #39B463;
--logo: #1778f2 !important;
--error: #f02849 !important; --font: Helvetica;
--success: #30ab5a !important; }
--text: #ffffff !important;
--banner: #6b6b6b !important; :root {
--popup: #242424cc !important; --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 { @font-face {
@ -33,14 +60,8 @@
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(--secondary); background-color: var(--surface0);
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0; margin: 0;
@ -48,93 +69,132 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
color: var(--text); color: var(--text);
font-family: sfpro; font-family: var(--font);
}
#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(--primary); background-color: var(--surface0);
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(--logo); color: var(--blue);
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;
} }
form button { .btn {
padding: .5rem; color: var(--btntext);
border-radius: .5rem; display: flex;
align-items: center;
align-content: center;
flex-direction: row;
font-weight: bold;
font-size: 1rem;
text-decoration: none;
padding: .4rem .6rem;
border-radius: .25rem;
background-color: transparent;
width: fit-content;
} }
input:focus { .btn:hover {
background-color: var(--surface1);
}
.btn-alt {
background-color: var(--surface1);
}
.btn-alt:hover {
background-color: var(--surface2);
}
.btn-wide {
width: auto;
flex-grow: 1;
justify-content: center;
}
.btn-line:hover {
background-color: inherit;
text-decoration: underline;
}
.btn-blue {
color: var(--blue-alt);
}
input.btn:focus {
border: none; border: none;
outline: none; outline: none;
} }
.header-entry { .btn-submit {
display: flex; color: var(--white);
flex-direction: row; background-color: var(--blue);
text-decoration: none; flex-grow: 1;
align-items: center; padding: .5rem;
color: var(--text);
} }
.nav .header-entry { .btn-submit:hover {
height: 100%; background-color: var(--blue-alt);
} }
.nav-center .header-entry:hover { .btn-success {
background-color: var(--hover); color: var(--white);
background-color: var(--green);
flex-grow: 1;
padding: .5rem;
} }
.btn-action { .btn-success:hover {
justify-content: center; background-color: var(--green-alt);
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,
@ -167,12 +227,13 @@ input:focus {
} }
@media (min-width: 800px) { @media (min-width: 800px) {
.header-entry > span { .nav-center .btn > span {
display: none; display: none;
} }
.nav-center .header-entry { .nav-center .btn {
padding: 0 3rem; padding: 0 3rem;
height: 100%;
} }
#action-hamburger { #action-hamburger {
@ -187,7 +248,7 @@ input:focus {
flex-direction: column; flex-direction: column;
top: 100%; top: 100%;
height: fit-content; height: fit-content;
background-color: var(--primary); background-color: var(--surface0);
width: 100%; width: 100%;
left: 0; left: 0;
transform: translateX(0%); transform: translateX(0%);
@ -198,18 +259,18 @@ input:focus {
display: inherit !important; display: inherit !important;
} }
.nav-center .header-entry { .nav-center .btn {
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 .header-entry > span { .nav-center .btn > span {
margin-left: 1rem; margin-left: 1rem;
} }
.nav-center .header-entry.active { .nav-center .btn.active {
border-bottom: none; border-bottom: none;
} }
} }
@ -218,11 +279,6 @@ input: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%;
@ -242,40 +298,32 @@ input:focus {
} }
.image-loading { .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-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(--primary); background-color: var(--surface0);
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(--secondary); background-color: var(--surface1);
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;
@ -314,8 +362,12 @@ input:focus {
margin-bottom: .75rem; margin-bottom: .75rem;
} }
.pb {
padding-bottom: 1rem;
}
.dim { .dim {
color: var(--medium); color: var(--subtext);
} }
.modal-container { .modal-container {
@ -329,7 +381,7 @@ input:focus {
} }
.modal { .modal {
background-color: var(--primary); background-color: var(--surface0);
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
@ -372,18 +424,14 @@ input:focus {
} }
.modal-header { .modal-header {
font-family: sfprobold; font-weight: bold;
position: relative; position: relative;
border-bottom: 1px solid var(--light); border-bottom: 1px solid var(--surface1);
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; padding: 1rem 0;
} }
.modal-content { .modal-content {
@ -402,9 +450,9 @@ input:focus {
.float-right { .float-right {
position: absolute; position: absolute;
transform: translate(0%, -50%); top: 50%;
top: 45%; left: 100%;
right: 0; transform: translate(-125%, -50%);
} }
.mi { .mi {
@ -421,17 +469,6 @@ input: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;
@ -442,20 +479,92 @@ button[type="submit"]:hover {
} }
.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(--error); background-color: var(--red);
} }
.toast.success { .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;
} }

View file

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

View file

@ -1,9 +1,7 @@
#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 {
@ -15,7 +13,6 @@
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%;
@ -23,4 +20,5 @@
flex-grow: 1; flex-grow: 1;
background-color: transparent; background-color: transparent;
color: var(--text); color: var(--text);
font-family: var(--font);
} }

69
src/public/css/people.css Normal file
View file

@ -0,0 +1,69 @@
.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);
}

View file

@ -1,16 +1,13 @@
.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

Binary file not shown.

View file

@ -12,7 +12,8 @@ var $$ = (selector) => {
'on', 'on',
'click', 'click',
'submit', 'submit',
'each' 'each',
'error'
]; ];
let vtable = {}; let vtable = {};
@ -58,11 +59,19 @@ var $$ = (selector) => {
/// ajax error handle /// ajax error handle
/// ///
var errorToast = (xhr) => { var errorToastAjax = (xhr) => {
let data = xhr.responseJSON; let data = xhr.responseJSON;
let msg = data.message;
let detail = data.details; let msg, detail, hint;
let hint = data.hint;
if (data) {
msg = data.message;
detail = data.details;
hint = data.hint;
} else {
msg = 'api_unknown';
}
let query = '?msg=' + msg; let query = '?msg=' + msg;
if (detail) { if (detail) {
@ -77,6 +86,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() { $$('.action-close-toast').on('click', function() {
$(this).parent().remove(); $(this).parent().remove();
}); });
@ -94,12 +110,14 @@ $$('.action-close-toast').each(function() {
$.ajaxSetup({ $.ajaxSetup({
headers: (() => { headers: (() => {
let ajaxHeaders = {}; let ajaxHeaders = {
ajaxHeaders['Content-Type'] = 'application/json'; 'Content-Type': 'application/json',
'Prefer': 'return=representation'
};
if (jwtStr) { if (jwtStr) {
ajaxHeaders['Authorization'] = 'Bearer ' + jwtStr ajaxHeaders['Authorization'] = 'Bearer ' + jwtStr
} }
return ajaxHeaders; return ajaxHeaders;
})(), })(),
error: errorToast error: errorToastAjax
}) })

View file

@ -70,12 +70,59 @@ $$('.action-new-comment-form').on('submit', function(e) {
let input = me.find('.action-new-comment'); let input = me.find('.action-new-comment');
let content = input.val(); let content = input.val();
let post_id = input.attr('postId'); 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({ $.ajax({
url: '/api/comment', url: '/api/comment',
method: 'POST', method: 'POST',
data: JSON.stringify({ post_id, content }), data: JSON.stringify({ post_id, content }),
success: function(_data) { success: onComment
window.location.reload();
},
}); });
}); });
$$('.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,
});
}
});

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('/login'); $this->redirect('/auth/login');
} }
} }

View file

@ -21,6 +21,39 @@ 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>
*/ */
@ -30,28 +63,23 @@ 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;
if ($user) { $query = $this->db
$query = $query->select('p.*, l.post_id IS NOT NULL as liked'); ->select('p.*, l.id as like_id')
} else { ->from('api.post p')
$query = $query->select('p.*, FALSE as liked'); ->join('api.like l', 'p.id = l.post_id AND l.user_id')
} ->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('id')->le($max); ->where('p.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();
@ -73,7 +101,6 @@ 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,
@ -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> * @return array<string,mixed>
*/ */
@ -105,6 +162,7 @@ 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();
@ -112,6 +170,17 @@ 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

@ -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;
}
}
?>

View file

@ -8,11 +8,12 @@ class Error_controller extends Controller {
$this->error_model = $this->load->model('apps/error'); $this->error_model = $this->load->model('apps/error');
} }
public function index() { public function index(): void {
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,6 +18,7 @@ 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

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

@ -20,6 +20,14 @@ 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

@ -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;
}
}

View file

@ -7,22 +7,18 @@ 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;
switch ($code) { $msg = lang('error_' . $code, FALSE);
case '404': if (!$msg) {
$data['msg'] = lang('error_404'); $msg = lang('error');
break;
case '500':
$data['msg'] = lang('error_500');
break;
default:
$data['msg'] = lang('error');
break;
} }
$data['msg'] = $msg;
} }
} }

View file

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

@ -39,7 +39,8 @@ class Format_model extends Model {
* @returns the formatted date * @returns the formatted date
*/ */
public function date($date) { public function date($date) {
return $date; $date=date_create($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

@ -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>

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="error"> <div id="main-content">
<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="input btn-fake ml" class="btn btn-alt btn-wide ml"
autocomplete="off" autocomplete="off"
aria-label="<?=lang('action_new_post_tip')?>" aria-label="<?=lang('action_new_post_tip')?>"
> >

View file

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

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

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

View file

@ -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>

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-action', class: 'btn btn-wide btn-submit',
attrs: array('type' => 'submit'), attrs: array('type' => 'submit'),
button: TRUE button: TRUE
)?> )?>
@ -32,14 +32,28 @@
$('#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: function(data) { success: onPost
window.location.reload();
},
}); });
}); });
</script> </script>

View file

@ -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>

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['date'])?></span> <span class="dim ml"><?=$format_model->date($comment['created'])?></span>
</div> </div>
<?=$comment['content']?> <?=$comment['content']?>
</div> </div>

View file

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

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

View file

@ -11,9 +11,16 @@
array_push($params, $hint); 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"> <div class="toast error">
<?=ucfirst($msg)?> <?=$lang_msg?>
<?=ilang('action_close', class: 'action-close-toast')?> <?=ilang('action_close', class: 'action-close-toast')?>
</div> </div>

View file

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

View file

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

View file

@ -46,6 +46,8 @@ 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;
@ -85,6 +87,8 @@ 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";
@ -95,16 +99,6 @@ 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;
@ -122,6 +116,11 @@ 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 {

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