login and register, liking on homepage

This commit is contained in:
Freya Murphy 2024-04-01 11:09:25 -04:00
parent ef7b0e26fa
commit 3a82baec9d
Signed by: freya
GPG key ID: 744AB800E383AE52
99 changed files with 1250 additions and 281 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,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;
@ -75,10 +76,12 @@ 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,
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;
@ -97,11 +100,13 @@ 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,
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,6 +184,9 @@ 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 TABLE admin.media ( CREATE TABLE admin.media (
name TEXT NOT NULL, name TEXT NOT NULL,
content BYTEA NOT NULL, content BYTEA NOT NULL,

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

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

@ -41,6 +41,12 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user;
\i /db/rest/comment/api_comment_update.sql; \i /db/rest/comment/api_comment_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;
-- 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,8 +11,10 @@ BEGIN
PERFORM _api.raise_deny(); PERFORM _api.raise_deny();
END IF; END IF;
DELETE FROM admin.user UPDATE admin.user SET
WHERE id = _user_id; deleted = TRUE,
modified = clock_timestamp()
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;

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

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

@ -58,11 +58,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 +85,13 @@ var errorToast = (xhr) => {
}) })
} }
var errorToast = (msg) => {
let url = '/template/toast?msg=' + msg;
$.get(url, function (data) {
$('#toast-container').prepend(data);
})
}
$$('.action-close-toast').on('click', function() { $$('.action-close-toast').on('click', function() {
$(this).parent().remove(); $(this).parent().remove();
}); });
@ -94,12 +109,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

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

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

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

@ -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' => $user))?>
<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,11 @@ class Aesthetic {
'css/post.css' 'css/post.css'
], ],
), ),
'auth' => array(
'css' => [
'css/auth.css'
],
),
); );
} }
/** /**

View file

@ -3,5 +3,6 @@
$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[''] = '_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

@ -122,6 +122,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 {

View file

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

View file

@ -1,5 +1,6 @@
<?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,6 +6,7 @@ $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';
@ -22,5 +23,10 @@ $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

@ -0,0 +1,34 @@
<?php
$lang['login'] = 'Login';
$lang['login_branding'] = 'Connect with javascript and the world around you on XSSBook.';
$lang['ph_username'] = 'Username';
$lang['ph_password'] = 'Password';
$lang['ph_first_name'] = 'First Name';
$lang['ph_last_name'] = 'Last Name';
$lang['ph_middle_name'] = 'Middle Name';
$lang['ph_username'] = 'Username';
$lang['ph_email'] = 'Email';
$lang['ph_password'] = 'Password';
$lang['ph_birth_date'] = 'Birthday';
$lang['ph_gender'] = 'Gender';
$lang['ph_gender_male'] = 'Male';
$lang['ph_gender_female'] = 'Female';
$lang['ph_gender_lettuce'] = 'Lettuce';
$lang['ph_basic_info'] = 'General Information';
$lang['action_login_tip'] = 'Login';
$lang['action_login_text'] = 'Login';
$lang['action_register_tip'] = 'Register';
$lang['action_register_text'] = 'Register';
$lang['action_create_account_tip'] = 'Create a new account';
$lang['action_create_account_text'] = 'Create new account';
$lang['action_forgot_passwd_tip'] = 'Reset your password';
$lang['action_forgot_passwd_text'] = 'Forgot password?';
$lang['register_modal_title'] = 'Create New Account';
$lang['action_register_text'] = 'Register';
$lang['action_register_tip'] = 'Register';
?>