login and register, liking on homepage
This commit is contained in:
parent
ef7b0e26fa
commit
3a82baec9d
99 changed files with 1250 additions and 281 deletions
|
@ -1,4 +1,4 @@
|
|||
FROM php:fpm-alpine
|
||||
RUN apk add --no-cache postgresql-dev
|
||||
RUN apk add --no-cache postgresql-dev runuser
|
||||
RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql
|
||||
RUN docker-php-ext-install pdo pdo_pgsql
|
||||
|
|
|
@ -87,6 +87,7 @@ server {
|
|||
}
|
||||
|
||||
location / {
|
||||
root /opt/xssbook/web;
|
||||
include fastcgi_params;
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
||||
|
|
|
@ -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$;
|
|
@ -5,7 +5,7 @@ services:
|
|||
ports:
|
||||
- '80:80'
|
||||
volumes:
|
||||
- ./web:/opt/xssbook
|
||||
- ./src:/opt/xssbook:ro
|
||||
- ./conf/nginx:/etc/nginx/conf.d:ro
|
||||
depends_on:
|
||||
- rest
|
||||
|
@ -18,8 +18,9 @@ services:
|
|||
env_file:
|
||||
- ./conf/postgres/database.env
|
||||
volumes:
|
||||
- ./web:/opt/xssbook
|
||||
- ./data/status:/status
|
||||
- ./src:/opt/xssbook:ro
|
||||
- ./data/status:/status:ro
|
||||
- ./data/session:/var/lib/php/session
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
|
@ -33,7 +34,7 @@ services:
|
|||
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
||||
volumes:
|
||||
- './data/schemas:/var/lib/postgresql/data'
|
||||
- ./db:/db:ro
|
||||
- ./src/db:/db:ro
|
||||
|
||||
rest:
|
||||
build: ./build/postgrest
|
||||
|
@ -47,7 +48,7 @@ services:
|
|||
env_file:
|
||||
- ./conf/postgres/database.env
|
||||
volumes:
|
||||
- ./db:/db:ro
|
||||
- ./src/db:/db:ro
|
||||
- ./data/status:/status
|
||||
depends_on:
|
||||
- db
|
||||
|
|
|
@ -50,11 +50,12 @@ CREATE TABLE admin.user (
|
|||
middle_name TEXT DEFAULT ''::text NOT NULL,
|
||||
email TEXT DEFAULT ''::text NOT NULL,
|
||||
gender TEXT DEFAULT ''::text NOT NULL,
|
||||
join_date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||
birth_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
profile_avatar BYTEA,
|
||||
profile_banner BYTEA,
|
||||
profile_bio TEXT DEFAULT ''::text NOT NULL
|
||||
profile_bio TEXT DEFAULT ''::text NOT NULL,
|
||||
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||
seen TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||
deleted BOOLEAN DEFAULT FALSE NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE admin.user OWNER TO xssbook;
|
||||
|
@ -75,10 +76,12 @@ CREATE SEQUENCE IF NOT EXISTS sys.post_id_seq
|
|||
ALTER TABLE sys.post_id_seq OWNER TO xssbook;
|
||||
|
||||
CREATE TABLE admin.post (
|
||||
id INTEGER DEFAULT nextval('sys.post_id_seq'::regclass) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
content TEXT DEFAULT ''::text NOT NULL,
|
||||
date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
|
||||
id INTEGER DEFAULT nextval('sys.post_id_seq'::regclass) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
content TEXT DEFAULT ''::text NOT NULL,
|
||||
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||
deleted BOOLEAN DEFAULT FALSE NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE admin.post OWNER TO xssbook;
|
||||
|
@ -97,11 +100,13 @@ CREATE SEQUENCE IF NOT EXISTS sys.comment_id_seq
|
|||
CACHE 1;
|
||||
|
||||
CREATE TABLE admin.comment (
|
||||
id INTEGER DEFAULT nextval('sys.comment_id_seq'::regclass) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
post_id INTEGER NOT NULL,
|
||||
content TEXT DEFAULT ''::text NOT NULL,
|
||||
date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
|
||||
id INTEGER DEFAULT nextval('sys.comment_id_seq'::regclass) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
post_id INTEGER NOT NULL,
|
||||
content TEXT DEFAULT ''::text NOT NULL,
|
||||
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||
deleted BOOLEAN DEFAULT FALSE NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE admin.comment OWNER TO xssbook;
|
||||
|
@ -115,15 +120,28 @@ ALTER TABLE ONLY admin.comment
|
|||
ALTER TABLE ONLY admin.comment
|
||||
ADD CONSTRAINT comment_post_id_fkey FOREIGN KEY (post_id) REFERENCES admin.post (id) ON DELETE CASCADE;
|
||||
|
||||
CREATE SEQUENCE IF NOT EXISTS sys.like_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
CREATE TABLE admin.like (
|
||||
id INTEGER DEFAULT nextval('sys.like_id_seq'::regclass) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
post_id INTEGER,
|
||||
comment_id INTEGER,
|
||||
date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
|
||||
value BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE admin.like OWNER TO xssbook;
|
||||
|
||||
ALTER TABLE ONLY admin.like
|
||||
ADD CONSTRAINT like_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY admin.like
|
||||
ADD CONSTRAINT like_user_id_fkey FOREIGN KEY (user_id) REFERENCES admin.user (id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -133,16 +151,32 @@ ALTER TABLE ONLY admin.like
|
|||
ALTER TABLE ONLY admin.like
|
||||
ADD CONSTRAINT like_comment_id_fkey FOREIGN KEY (comment_id) REFERENCES admin.comment (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY admin.like
|
||||
ADD CONSTRAINT like_post_id_unique UNIQUE (user_id, post_id);
|
||||
|
||||
ALTER TABLE ONLY admin.like
|
||||
ADD CONSTRAINT like_comment_id_unique UNIQUE (user_id, comment_id);
|
||||
|
||||
CREATE SEQUENCE IF NOT EXISTS sys.follow_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
CREATE TABLE admin.follow (
|
||||
id INTEGER DEFAULT nextval('sys.follow_id_seq'::regclass) NOT NULL,
|
||||
follower_id INTEGER NOT NULL,
|
||||
followee_id INTEGER NOT NULL,
|
||||
date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
|
||||
value BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE admin.follow OWNER TO xssbook;
|
||||
|
||||
ALTER TABLE ONLY admin.follow
|
||||
ADD CONSTRAINT follow_pkey PRIMARY KEY (follower_id, followee_id);
|
||||
ADD CONSTRAINT follow_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY admin.follow
|
||||
ADD CONSTRAINT follow_follower_id FOREIGN KEY (follower_id) REFERENCES admin.user (id) ON DELETE CASCADE;
|
||||
|
@ -150,6 +184,9 @@ ALTER TABLE ONLY admin.follow
|
|||
ALTER TABLE ONLY admin.follow
|
||||
ADD CONSTRAINT follow_followee_id FOREIGN KEY (followee_id) REFERENCES admin.user (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY admin.follow
|
||||
ADD CONSTRAINT follow_follower_unique UNIQUE (follower_id, followee_id);
|
||||
|
||||
CREATE TABLE admin.media (
|
||||
name TEXT NOT NULL,
|
||||
content BYTEA NOT NULL,
|
|
@ -4,10 +4,24 @@ CREATE VIEW api.comment AS
|
|||
c.user_id,
|
||||
c.post_id,
|
||||
c.content,
|
||||
c.date
|
||||
c.created,
|
||||
c.modified
|
||||
FROM
|
||||
admin.comment c
|
||||
ORDER BY id ASC;
|
||||
LEFT JOIN
|
||||
admin.post p
|
||||
ON
|
||||
p.id = c.post_id
|
||||
LEFT JOIN
|
||||
admin.user u
|
||||
ON
|
||||
u.id = c.user_id
|
||||
WHERE
|
||||
c.deleted <> TRUE AND
|
||||
p.deleted <> TRUE AND
|
||||
u.deleted <> TRUE
|
||||
ORDER BY
|
||||
id ASC;
|
||||
|
||||
GRANT SELECT ON TABLE api.comment
|
||||
TO rest_anon, rest_user;
|
|
@ -11,9 +11,10 @@ BEGIN
|
|||
PERFORM _api.raise_deny();
|
||||
END IF;
|
||||
|
||||
DELETE FROM admin.comment
|
||||
WHERE user_id = _user_id
|
||||
AND id = OLD.id;
|
||||
UPDATE admin.comment SET
|
||||
deleted = TRUE,
|
||||
modified = clock_timestamp()
|
||||
WHERE id = OLD.id;
|
||||
END
|
||||
$BODY$;
|
||||
|
||||
|
@ -21,7 +22,7 @@ GRANT EXECUTE ON FUNCTION _api.comment_delete()
|
|||
TO rest_user;
|
||||
GRANT DELETE ON TABLE api.comment
|
||||
TO rest_user;
|
||||
GRANT DELETE ON TABLE admin.comment
|
||||
GRANT UPDATE ON TABLE admin.comment
|
||||
TO rest_user;
|
||||
|
||||
CREATE TRIGGER api_comment_delete_trgr
|
|
@ -34,7 +34,9 @@ BEGIN
|
|||
_user_id,
|
||||
NEW.post_id,
|
||||
NEW.content
|
||||
);
|
||||
)
|
||||
RETURNING id
|
||||
INTO NEW.id;
|
||||
|
||||
RETURN NEW;
|
||||
END
|
|
@ -27,8 +27,9 @@ BEGIN
|
|||
END IF;
|
||||
|
||||
IF _changed THEN
|
||||
UPDATE admin.comment
|
||||
SET content = NEW.content
|
||||
UPDATE admin.comment SET
|
||||
content = NEW.content,
|
||||
modified = clock_timestamp()
|
||||
WHERE id = OLD.id;
|
||||
END IF;
|
||||
|
16
src/db/rest/like/api_like.sql
Normal file
16
src/db/rest/like/api_like.sql
Normal 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;
|
32
src/db/rest/like/api_like_delete.sql
Normal file
32
src/db/rest/like/api_like_delete.sql
Normal 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();
|
51
src/db/rest/like/api_like_insert.sql
Normal file
51
src/db/rest/like/api_like_insert.sql
Normal 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();
|
44
src/db/rest/like/api_like_update.sql
Normal file
44
src/db/rest/like/api_like_update.sql
Normal 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();
|
|
@ -8,6 +8,7 @@ DECLARE
|
|||
_payload JSON;
|
||||
_valid BOOLEAN;
|
||||
_jwt_secret TEXT;
|
||||
_user_id INTEGER;
|
||||
BEGIN
|
||||
SELECT jwt_secret INTO _jwt_secret
|
||||
FROM sys.database_info
|
||||
|
@ -28,7 +29,13 @@ BEGIN
|
|||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
RETURN _payload->>'user_id';
|
||||
_user_id = _payload->>'user_id';
|
||||
|
||||
UPDATE admin.user
|
||||
SET seen = clock_timestamp()
|
||||
WHERE id = _user_id;
|
||||
|
||||
RETURN _user_id;
|
||||
END
|
||||
$BODY$;
|
||||
|
||||
|
@ -36,3 +43,5 @@ GRANT EXECUTE ON FUNCTION _api.verify_jwt(TEXT)
|
|||
TO rest_anon, rest_user;
|
||||
GRANT SELECT ON TABLE sys.database_info
|
||||
TO rest_anon, rest_user;
|
||||
GRANT UPDATE ON TABLE admin.user
|
||||
TO rest_anon, rest_user;
|
|
@ -3,7 +3,8 @@ CREATE VIEW api.post AS
|
|||
p.id,
|
||||
p.user_id,
|
||||
p.content,
|
||||
p.date,
|
||||
p.created,
|
||||
p.modified,
|
||||
COALESCE(c.cc, 0)
|
||||
AS comment_count
|
||||
FROM
|
||||
|
@ -16,8 +17,19 @@ CREATE VIEW api.post AS
|
|||
admin.comment c
|
||||
GROUP BY
|
||||
c.post_id
|
||||
) c ON p.id = c.post_id
|
||||
ORDER BY p.id DESC;
|
||||
) c
|
||||
ON
|
||||
p.id = c.post_id
|
||||
LEFT JOIN
|
||||
admin.user u
|
||||
ON
|
||||
u.id = p.user_id
|
||||
WHERE
|
||||
p.deleted <> TRUE
|
||||
AND
|
||||
u.deleted <> TRUE
|
||||
ORDER BY
|
||||
p.id DESC;
|
||||
|
||||
GRANT SELECT ON TABLE api.post
|
||||
TO rest_anon, rest_user;
|
|
@ -11,9 +11,10 @@ BEGIN
|
|||
PERFORM _api.raise_deny();
|
||||
END IF;
|
||||
|
||||
DELETE FROM admin.post
|
||||
WHERE user_id = _user_id
|
||||
AND id = OLD.id;
|
||||
UPDATE admin.post SET
|
||||
deleted = TRUE,
|
||||
modified = clock_timestamp()
|
||||
WHERE id = OLD.id;
|
||||
END
|
||||
$BODY$;
|
||||
|
||||
|
@ -21,7 +22,7 @@ GRANT EXECUTE ON FUNCTION _api.post_delete()
|
|||
TO rest_user;
|
||||
GRANT DELETE ON TABLE api.post
|
||||
TO rest_user;
|
||||
GRANT DELETE ON TABLE admin.post
|
||||
GRANT UPDATE ON TABLE admin.post
|
||||
TO rest_user;
|
||||
|
||||
CREATE TRIGGER api_post_delete_trgr
|
|
@ -22,7 +22,9 @@ BEGIN
|
|||
) VALUES (
|
||||
_user_id,
|
||||
NEW.content
|
||||
);
|
||||
)
|
||||
RETURNING id
|
||||
INTO NEW.id;
|
||||
|
||||
RETURN NEW;
|
||||
END
|
|
@ -27,8 +27,9 @@ BEGIN
|
|||
END IF;
|
||||
|
||||
IF _changed THEN
|
||||
UPDATE admin.post
|
||||
SET content = NEW.content
|
||||
UPDATE admin.post SET
|
||||
content = NEW.content,
|
||||
modified = clock_timestamp()
|
||||
WHERE id = OLD.id;
|
||||
END IF;
|
||||
|
|
@ -41,6 +41,12 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user;
|
|||
\i /db/rest/comment/api_comment_update.sql;
|
||||
\i /db/rest/comment/api_comment_delete.sql;
|
||||
|
||||
-- like
|
||||
\i /db/rest/like/api_like.sql;
|
||||
\i /db/rest/like/api_like_insert.sql;
|
||||
\i /db/rest/like/api_like_update.sql;
|
||||
\i /db/rest/like/api_like_delete.sql;
|
||||
|
||||
-- login
|
||||
\i /db/rest/login/_api_sign_jwt.sql;
|
||||
\i /db/rest/login/_api_verify_jwt.sql;
|
|
@ -9,13 +9,15 @@ CREATE VIEW api.user AS
|
|||
u.middle_name,
|
||||
u.email,
|
||||
u.gender,
|
||||
u.join_date,
|
||||
u.birth_date,
|
||||
u.profile_avatar,
|
||||
u.profile_banner,
|
||||
u.profile_bio
|
||||
u.profile_bio,
|
||||
u.created,
|
||||
u.modified,
|
||||
u.seen
|
||||
FROM
|
||||
admin.user u;
|
||||
admin.user u
|
||||
WHERE
|
||||
u.deleted <> TRUE;
|
||||
|
||||
GRANT SELECT ON TABLE api.user
|
||||
TO rest_anon, rest_user;
|
|
@ -11,8 +11,10 @@ BEGIN
|
|||
PERFORM _api.raise_deny();
|
||||
END IF;
|
||||
|
||||
DELETE FROM admin.user
|
||||
WHERE id = _user_id;
|
||||
UPDATE admin.user SET
|
||||
deleted = TRUE,
|
||||
modified = clock_timestamp()
|
||||
WHERE id = _user_id;
|
||||
END
|
||||
$BODY$;
|
||||
|
||||
|
@ -20,7 +22,7 @@ GRANT EXECUTE ON FUNCTION _api.user_delete()
|
|||
TO rest_user;
|
||||
GRANT DELETE ON TABLE api.user
|
||||
TO rest_user;
|
||||
GRANT DELETE ON TABLE admin.user
|
||||
GRANT UPDATE ON TABLE admin.user
|
||||
TO rest_user;
|
||||
|
||||
CREATE TRIGGER api_user_delete_trgr
|
|
@ -104,7 +104,9 @@ BEGIN
|
|||
NEW.gender,
|
||||
NEW.birth_date,
|
||||
NEW.profile_bio
|
||||
);
|
||||
)
|
||||
RETURNING id
|
||||
INTO NEW.id;
|
||||
|
||||
NEW.password := NULL;
|
||||
|
|
@ -145,7 +145,8 @@ BEGIN
|
|||
email = NEW.email,
|
||||
gender = NEW.gender,
|
||||
birth_date = NEW.birth_date,
|
||||
profile_bio = NEW.profile_bio
|
||||
profile_bio = NEW.profile_bio,
|
||||
modified = clock_timestamp()
|
||||
WHERE id = OLD.id;
|
||||
END IF;
|
||||
|
22
src/db/rest/util/_api_get_user_id.sql
Normal file
22
src/db/rest/util/_api_get_user_id.sql
Normal 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
45
src/public/css/auth.css
Normal 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;
|
||||
}
|
||||
}
|
|
@ -1,17 +1,44 @@
|
|||
:root {
|
||||
--primary: #242424 !important;
|
||||
--secondary: #181818 !important;
|
||||
--hover: #1b1b1b !important;
|
||||
--light: #3e4042 !important;
|
||||
--mild: #1b1b1b !important;
|
||||
--medium: #e2ded6 !important;
|
||||
--extreme: #e2ded6 !important;
|
||||
--logo: #1778f2 !important;
|
||||
--error: #f02849 !important;
|
||||
--success: #30ab5a !important;
|
||||
--text: #ffffff !important;
|
||||
--banner: #6b6b6b !important;
|
||||
--popup: #242424cc !important;
|
||||
--white: #E4E6EB;
|
||||
--blue: #1778f2;
|
||||
--red: #f02849;
|
||||
--green: #30ab5a;
|
||||
|
||||
--blue-alt: #1D85FC;
|
||||
--green-alt: #39B463;
|
||||
|
||||
--font: Helvetica;
|
||||
}
|
||||
|
||||
:root {
|
||||
--base :#18191A;
|
||||
--surface0: #242526;
|
||||
--surface1: #3A3B3C;
|
||||
--surface2: #4E4F50;
|
||||
|
||||
--text: #E4E6EB;
|
||||
--subtext: #B0B3B8;
|
||||
--btntext: #E4E6EB;
|
||||
}
|
||||
|
||||
/**
|
||||
:root {
|
||||
--base: #f0f2f5;
|
||||
--surface0: #ffffff;
|
||||
--surface1: #f0f2f5;
|
||||
--surface2: #dadde1;
|
||||
|
||||
--text: #000000;
|
||||
--subtext: #1d2129;
|
||||
--btntext: #606770;
|
||||
}
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'Helvetica Neue';
|
||||
font-style: normal;
|
||||
src: url("/public/font/helvetica-neue.otf") format("opentype");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
@ -33,14 +60,8 @@
|
|||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: sfprobold;
|
||||
src: url("/public/font/sfprobold.otf") format("opentype");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--secondary);
|
||||
background-color: var(--surface0);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
|
@ -48,93 +69,132 @@ body {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text);
|
||||
font-family: sfpro;
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
#main-content {
|
||||
background-color: var(--base);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
header {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
height: 3.5rem;
|
||||
background-color: var(--primary);
|
||||
background-color: var(--surface0);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
border-bottom: 1px solid var(--surface1);
|
||||
}
|
||||
|
||||
header .logo {
|
||||
font-family: facebook;
|
||||
color: var(--logo);
|
||||
color: var(--blue);
|
||||
font-size: 2.25rem;
|
||||
height: 100%;
|
||||
line-height: 2rem;
|
||||
margin-top: .75rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
color: var(--subtext);
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
color: var(--surface2);
|
||||
background-color: var(--surface2);
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
a, button, input, div {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a, button, input {
|
||||
background: none;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-family: sfprobold;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
a, button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
form button {
|
||||
padding: .5rem;
|
||||
border-radius: .5rem;
|
||||
.btn {
|
||||
color: var(--btntext);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-direction: row;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
|
||||
padding: .4rem .6rem;
|
||||
border-radius: .25rem;
|
||||
background-color: transparent;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
.btn:hover {
|
||||
background-color: var(--surface1);
|
||||
}
|
||||
|
||||
.btn-alt {
|
||||
background-color: var(--surface1);
|
||||
}
|
||||
|
||||
.btn-alt:hover {
|
||||
background-color: var(--surface2);
|
||||
}
|
||||
|
||||
.btn-wide {
|
||||
width: auto;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-line:hover {
|
||||
background-color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-blue {
|
||||
color: var(--blue-alt);
|
||||
}
|
||||
|
||||
input.btn:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.header-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-decoration: none;
|
||||
align-items: center;
|
||||
color: var(--text);
|
||||
.btn-submit {
|
||||
color: var(--white);
|
||||
background-color: var(--blue);
|
||||
flex-grow: 1;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.nav .header-entry {
|
||||
height: 100%;
|
||||
.btn-submit:hover {
|
||||
background-color: var(--blue-alt);
|
||||
}
|
||||
|
||||
.nav-center .header-entry:hover {
|
||||
background-color: var(--hover);
|
||||
.btn-success {
|
||||
color: var(--white);
|
||||
background-color: var(--green);
|
||||
flex-grow: 1;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: .35rem;
|
||||
margin: .25rem;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
.btn-blue:hover {
|
||||
color: var(--logo);
|
||||
}
|
||||
|
||||
.header .btn-blue {
|
||||
border-bottom: 1px solid var(--logo);
|
||||
}
|
||||
|
||||
.btn-line:hover {
|
||||
text-decoration: underline;
|
||||
.btn-success:hover {
|
||||
background-color: var(--green-alt);
|
||||
}
|
||||
|
||||
.nav,
|
||||
|
@ -167,12 +227,13 @@ input:focus {
|
|||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.header-entry > span {
|
||||
.nav-center .btn > span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-center .header-entry {
|
||||
.nav-center .btn {
|
||||
padding: 0 3rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#action-hamburger {
|
||||
|
@ -187,7 +248,7 @@ input:focus {
|
|||
flex-direction: column;
|
||||
top: 100%;
|
||||
height: fit-content;
|
||||
background-color: var(--primary);
|
||||
background-color: var(--surface0);
|
||||
width: 100%;
|
||||
left: 0;
|
||||
transform: translateX(0%);
|
||||
|
@ -198,18 +259,18 @@ input:focus {
|
|||
display: inherit !important;
|
||||
}
|
||||
|
||||
.nav-center .header-entry {
|
||||
.nav-center .btn {
|
||||
width: calc(100% - 3rem);
|
||||
padding: .75rem 0rem !important;
|
||||
padding-left: 3rem !important;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.nav-center .header-entry > span {
|
||||
.nav-center .btn > span {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.nav-center .header-entry.active {
|
||||
.nav-center .btn.active {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
@ -218,11 +279,6 @@ input:focus {
|
|||
display: block;
|
||||
}
|
||||
|
||||
.nav-right .header-entry {
|
||||
padding: 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
to {
|
||||
background-position-x: 0%;
|
||||
|
@ -242,40 +298,32 @@ input:focus {
|
|||
}
|
||||
|
||||
.image-loading {
|
||||
background: linear-gradient(-45deg, var(--secondary) 0%, var(--primary) 25%, var(--secondary) 50%);
|
||||
background: linear-gradient(-45deg, var(--surface0) 0%, var(--base) 25%, var(--surface0) 50%);
|
||||
background-size: 500%;
|
||||
background-position-x: 150%;
|
||||
animation: shimmer 1s linear infinite;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--primary);
|
||||
background-color: var(--surface0);
|
||||
border-radius: .5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card .sub-card {
|
||||
background-color: var(--secondary);
|
||||
background-color: var(--surface1);
|
||||
border-radius: .5rem;
|
||||
padding: .75rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
width: calc(100% - 20px);
|
||||
background-color: var(--secondary);
|
||||
font-family: sfpro;
|
||||
}
|
||||
|
||||
.input:hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -314,8 +362,12 @@ input:focus {
|
|||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
.pb {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dim {
|
||||
color: var(--medium);
|
||||
color: var(--subtext);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
|
@ -329,7 +381,7 @@ input:focus {
|
|||
}
|
||||
|
||||
.modal {
|
||||
background-color: var(--primary);
|
||||
background-color: var(--surface0);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
@ -372,18 +424,14 @@ input:focus {
|
|||
}
|
||||
|
||||
.modal-header {
|
||||
font-family: sfprobold;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--light);
|
||||
text-align: center;
|
||||
margin: 0 1rem;
|
||||
border-radius: .5rem .5rem 0 0;
|
||||
border-bottom: 1px solid var(--surface1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-left: 1rem;
|
||||
cursor: grab;
|
||||
padding: 1rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
|
@ -402,9 +450,9 @@ input:focus {
|
|||
|
||||
.float-right {
|
||||
position: absolute;
|
||||
transform: translate(0%, -50%);
|
||||
top: 45%;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
transform: translate(-125%, -50%);
|
||||
}
|
||||
|
||||
.mi {
|
||||
|
@ -421,17 +469,6 @@ input:focus {
|
|||
font-size: 2rem;
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
text-align: center;
|
||||
background-color: var(--logo);
|
||||
flex-grow: 1;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
button[type="submit"]:hover {
|
||||
background-color: var(--logo);
|
||||
}
|
||||
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
top: 4rem;
|
||||
|
@ -442,20 +479,92 @@ button[type="submit"]:hover {
|
|||
}
|
||||
|
||||
.toast {
|
||||
color: var(--white);
|
||||
padding: .75rem;
|
||||
margin: .5rem;
|
||||
border-radius: .5rem;
|
||||
min-width: 15rem;
|
||||
font-family: sfpro;
|
||||
animation: fadeIn .1s, slideIn .25s linear;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
background-color: var(--error);
|
||||
background-color: var(--red);
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
background-color: var(--success);
|
||||
background-color: var(--green);
|
||||
}
|
||||
|
||||
form input:not(.btn) {
|
||||
display: block;
|
||||
font-size: 1.1rem;
|
||||
outline: 2px solid var(--surface2);
|
||||
border-radius: .25rem;
|
||||
padding: .75rem;
|
||||
}
|
||||
|
||||
form input:not(.btn):focus {
|
||||
outline-color: var(--blue);
|
||||
}
|
||||
|
||||
form .rel label:not(.static) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(.5rem, -40%);
|
||||
color: var(--subtext);
|
||||
transition: all 0.2s ease-out;
|
||||
pointer-events: none;
|
||||
width: fit-content;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
input:focus + label:not(.static),
|
||||
input:not(:placeholder-shown) + label:not(.static) {
|
||||
color: var(--text);
|
||||
top: 0;
|
||||
padding: .5rem;
|
||||
padding-top: 0;
|
||||
font-size: .75rem;
|
||||
transform: translate(.5rem, -25%);
|
||||
background-color: var(--surface0);
|
||||
}
|
||||
|
||||
.rel {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rel input {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
input[type=radio] {
|
||||
padding: 3rem !important;
|
||||
}
|
||||
|
||||
.radio {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.radio label {
|
||||
border: 1px solid var(--surface2);
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
padding: .75rem;
|
||||
border-radius: .25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio input {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
transform: translate(-250%, -70%);
|
||||
width: fit-content;
|
||||
outline: none !important;
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
#error {
|
||||
#main-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 10rem;
|
||||
padding: 10rem 0;
|
||||
}
|
||||
|
||||
#error h1 {
|
||||
color: var(--logo);
|
||||
#main-content h1 {
|
||||
color: var(--blue);
|
||||
font-family: Facebook;
|
||||
font-size: 5rem;
|
||||
}
|
||||
|
||||
#error span {
|
||||
#main-content span {
|
||||
font-size: 2rem;
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
#main-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
|
@ -15,7 +13,6 @@
|
|||
border: none;
|
||||
resize: none;
|
||||
outline: none;
|
||||
font-family: sfpro;
|
||||
font-size: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
width: 100%;
|
||||
|
@ -23,4 +20,5 @@
|
|||
flex-grow: 1;
|
||||
background-color: transparent;
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
}
|
|
@ -1,16 +1,13 @@
|
|||
.post hr {
|
||||
color: var(--light);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.post hr:nth-of-type(1) {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.action-load-comments {
|
||||
margin-left: 4rem;
|
||||
}
|
||||
|
||||
#action-load-posts {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.post {
|
||||
padding-bottom: 0;
|
||||
}
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
BIN
src/public/font/helvetica-neue.otf
Normal file
BIN
src/public/font/helvetica-neue.otf
Normal file
Binary file not shown.
|
@ -58,11 +58,19 @@ var $$ = (selector) => {
|
|||
/// ajax error handle
|
||||
///
|
||||
|
||||
var errorToast = (xhr) => {
|
||||
var errorToastAjax = (xhr) => {
|
||||
let data = xhr.responseJSON;
|
||||
let msg = data.message;
|
||||
let detail = data.details;
|
||||
let hint = data.hint;
|
||||
|
||||
let msg, detail, hint;
|
||||
|
||||
if (data) {
|
||||
msg = data.message;
|
||||
detail = data.details;
|
||||
hint = data.hint;
|
||||
} else {
|
||||
msg = 'api_unknown';
|
||||
}
|
||||
|
||||
|
||||
let query = '?msg=' + msg;
|
||||
if (detail) {
|
||||
|
@ -77,6 +85,13 @@ var errorToast = (xhr) => {
|
|||
})
|
||||
}
|
||||
|
||||
var errorToast = (msg) => {
|
||||
let url = '/template/toast?msg=' + msg;
|
||||
$.get(url, function (data) {
|
||||
$('#toast-container').prepend(data);
|
||||
})
|
||||
}
|
||||
|
||||
$$('.action-close-toast').on('click', function() {
|
||||
$(this).parent().remove();
|
||||
});
|
||||
|
@ -94,12 +109,14 @@ $$('.action-close-toast').each(function() {
|
|||
|
||||
$.ajaxSetup({
|
||||
headers: (() => {
|
||||
let ajaxHeaders = {};
|
||||
ajaxHeaders['Content-Type'] = 'application/json';
|
||||
let ajaxHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Prefer': 'return=representation'
|
||||
};
|
||||
if (jwtStr) {
|
||||
ajaxHeaders['Authorization'] = 'Bearer ' + jwtStr
|
||||
}
|
||||
return ajaxHeaders;
|
||||
})(),
|
||||
error: errorToast
|
||||
error: errorToastAjax
|
||||
})
|
|
@ -70,12 +70,59 @@ $$('.action-new-comment-form').on('submit', function(e) {
|
|||
let input = me.find('.action-new-comment');
|
||||
let content = input.val();
|
||||
let post_id = input.attr('postId');
|
||||
|
||||
const getComment = function(data) {
|
||||
if (data) {
|
||||
let container = me.closest('.post').find('.comments');
|
||||
container.prepend(data);
|
||||
}
|
||||
input.val('');
|
||||
}
|
||||
|
||||
const onComment = function(data) {
|
||||
let id = data[0].id;
|
||||
$.get({
|
||||
url: '/_util/post/comment?id=' + id,
|
||||
success: getComment
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/api/comment',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({ post_id, content }),
|
||||
success: function(_data) {
|
||||
window.location.reload();
|
||||
},
|
||||
success: onComment
|
||||
});
|
||||
});
|
||||
|
||||
$$('.action-like').on('click', function() {
|
||||
let me = $(this);
|
||||
let liked = me.hasClass('btn-blue');
|
||||
let like_id = me.attr('likeId');
|
||||
let post_id = me.attr('postId');
|
||||
|
||||
const onPatch = () => {
|
||||
me.toggleClass('btn-blue');
|
||||
}
|
||||
|
||||
const onPost = (data) => {
|
||||
me.attr('likeId', data[0].id + '');
|
||||
me.toggleClass('btn-blue');
|
||||
}
|
||||
|
||||
if (like_id) {
|
||||
$.ajax({
|
||||
url: '/api/like?id=eq.' + like_id,
|
||||
method: 'PATCH',
|
||||
data: JSON.stringify({ value: !liked }),
|
||||
success: onPatch
|
||||
});
|
||||
} else {
|
||||
$.ajax({
|
||||
url: '/api/like',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({ post_id, value: true }),
|
||||
success: onPost,
|
||||
});
|
||||
}
|
||||
});
|
|
@ -14,7 +14,7 @@ class _index_controller extends Controller {
|
|||
if ($this->main->session) {
|
||||
$this->redirect('/home');
|
||||
} else {
|
||||
$this->redirect('/login');
|
||||
$this->redirect('/auth/login');
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,39 @@ class Post_controller extends Controller {
|
|||
$this->view('template/posts');
|
||||
}
|
||||
|
||||
public function post(): void {
|
||||
$pid = $this->request_model->get_int('id', 0);
|
||||
|
||||
$post = $this->db
|
||||
->select('p.*, l.id as like_id')
|
||||
->from('api.post p')
|
||||
->join('api.like l', 'p.id = l.post_id AND l.user_id')
|
||||
->eq($pid)
|
||||
->where('p.id')
|
||||
->eq($pid)
|
||||
->row();
|
||||
|
||||
if (!$post) {
|
||||
return;
|
||||
}
|
||||
|
||||
$users = $this->cache_model->get_users([$post]);
|
||||
$uid = $post['user_id'];
|
||||
|
||||
if (!array_key_exists($uid, $users)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $users[$uid];
|
||||
|
||||
$data = array(
|
||||
'user' => $user,
|
||||
'page_size' => $this->page_size,
|
||||
'post' => $post
|
||||
);
|
||||
$this->view('template/post', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
|
@ -30,28 +63,23 @@ class Post_controller extends Controller {
|
|||
$offset = $page * $this->page_size;
|
||||
|
||||
$user = $this->main->user();
|
||||
$uid = isset($user) ? $user['id'] : NULL;
|
||||
|
||||
$query = $this->db;
|
||||
|
||||
if ($user) {
|
||||
$query = $query->select('p.*, l.post_id IS NOT NULL as liked');
|
||||
} else {
|
||||
$query = $query->select('p.*, FALSE as liked');
|
||||
}
|
||||
|
||||
$query = $query->from('api.post p');
|
||||
|
||||
if ($user) {
|
||||
$query = $query->join('admin.like l', 'p.id = l.post_id AND l.user_id')
|
||||
->eq($user['id']);
|
||||
}
|
||||
$query = $this->db
|
||||
->select('p.*, l.id as like_id')
|
||||
->from('api.post p')
|
||||
->join('api.like l', 'p.id = l.post_id AND l.user_id')
|
||||
->eq($uid);
|
||||
|
||||
if ($max) {
|
||||
$query = $query
|
||||
->where('id')->le($max);
|
||||
->where('p.id')->le($max);
|
||||
}
|
||||
|
||||
$posts = $query
|
||||
->order_by('p.id', 'DESC')
|
||||
->limit($this->page_size)
|
||||
->offset($offset)
|
||||
->rows();
|
||||
|
@ -73,7 +101,6 @@ class Post_controller extends Controller {
|
|||
->from('api.post p')
|
||||
->row()['pc'];
|
||||
|
||||
|
||||
return array(
|
||||
'loaded' => count($posts),
|
||||
'total' => $pc,
|
||||
|
@ -82,6 +109,36 @@ class Post_controller extends Controller {
|
|||
);
|
||||
}
|
||||
|
||||
public function comment(): void {
|
||||
$cid = $this->request_model->get_int('id', 0);
|
||||
|
||||
$comment = $this->db
|
||||
->select('*')
|
||||
->from('api.comment')
|
||||
->where('id')
|
||||
->eq($cid)
|
||||
->row();
|
||||
|
||||
if (!$comment) {
|
||||
return;
|
||||
}
|
||||
|
||||
$users = $this->cache_model->get_users([$comment]);
|
||||
$uid = $comment['user_id'];
|
||||
|
||||
if (!array_key_exists($uid, $users)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $users[$uid];
|
||||
|
||||
$data = array(
|
||||
'user' => $user,
|
||||
'comment' => $comment
|
||||
);
|
||||
$this->view('template/comment', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
|
@ -105,6 +162,7 @@ class Post_controller extends Controller {
|
|||
}
|
||||
|
||||
$comments = $query
|
||||
->order_by('id', 'ASC')
|
||||
->limit($this->page_size)
|
||||
->offset($offset)
|
||||
->rows();
|
||||
|
@ -112,6 +170,17 @@ class Post_controller extends Controller {
|
|||
$users = $this->cache_model->get_users($comments);
|
||||
$max = 0;
|
||||
|
||||
// only add this hr when not logged in
|
||||
// otherwise its added automatically by
|
||||
// the like and comment buttons
|
||||
if (
|
||||
count($comments) &&
|
||||
$page == 0 &&
|
||||
$this->main->session === NULL
|
||||
) {
|
||||
echo '<hr>';
|
||||
}
|
||||
|
||||
foreach ($comments as $comment) {
|
||||
$max = max($max, $comment['id']);
|
||||
$data = array();
|
56
src/web/_controller/apps/auth.php
Normal file
56
src/web/_controller/apps/auth.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
|
@ -8,11 +8,12 @@ class Error_controller extends Controller {
|
|||
$this->error_model = $this->load->model('apps/error');
|
||||
}
|
||||
|
||||
public function index() {
|
||||
public function index(): void {
|
||||
parent::index();
|
||||
$data = $this->error_model->get_data();
|
||||
$this->view('header', $data);
|
||||
$this->view('apps/error/main', $data);
|
||||
$this->view('footer', $data);
|
||||
}
|
||||
|
||||
}
|
|
@ -18,6 +18,7 @@ class Home_controller extends Controller {
|
|||
$data = $this->home_model->get_data();
|
||||
$this->view('header', $data);
|
||||
$this->view('apps/home/main', $data);
|
||||
$this->view('footer', $data);
|
||||
}
|
||||
|
||||
}
|
|
@ -20,6 +20,14 @@ class Modal_controller extends Controller {
|
|||
public function new_post(): void {
|
||||
$this->modal('new_post');
|
||||
}
|
||||
|
||||
public function register(): void {
|
||||
$this->load->app_lang(
|
||||
$this->main->info['lang'],
|
||||
'auth'
|
||||
);
|
||||
$this->modal('register');
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
13
src/web/_model/apps/auth.php
Normal file
13
src/web/_model/apps/auth.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -7,22 +7,18 @@ class Error_model extends Model {
|
|||
|
||||
private function get_msg(&$data) {
|
||||
if (!array_key_exists('code', $_GET)) {
|
||||
http_response_code(500);
|
||||
$data['msg'] = lang('error');
|
||||
$data['title'] = '500';
|
||||
} else {
|
||||
$code = $_GET['code'];
|
||||
http_response_code($code);
|
||||
$data['title'] = $code;
|
||||
switch ($code) {
|
||||
case '404':
|
||||
$data['msg'] = lang('error_404');
|
||||
break;
|
||||
case '500':
|
||||
$data['msg'] = lang('error_500');
|
||||
break;
|
||||
default:
|
||||
$data['msg'] = lang('error');
|
||||
break;
|
||||
$msg = lang('error_' . $code, FALSE);
|
||||
if (!$msg) {
|
||||
$msg = lang('error');
|
||||
}
|
||||
$data['msg'] = $msg;
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ class Main_model {
|
|||
*/
|
||||
private function asset_stamp($path): int {
|
||||
$root = $GLOBALS['webroot'];
|
||||
$path = $root . '/public/' . $path;
|
||||
$path = $root . '/../public/' . $path;
|
||||
return filemtime($path);
|
||||
}
|
||||
|
86
src/web/_views/apps/auth/login.php
Normal file
86
src/web/_views/apps/auth/login.php
Normal 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>
|
|
@ -1,6 +1,6 @@
|
|||
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
|
||||
<?php /* vi: syntax=php */ ?>
|
||||
<div id="error">
|
||||
<div id="main-content">
|
||||
<h1><?=$title?></h1>
|
||||
<span><?=$msg?></span>
|
||||
</div>
|
|
@ -7,7 +7,7 @@
|
|||
<?php $this->view('template/pfp', array('user' => $self))?>
|
||||
<a
|
||||
id="action-new-post"
|
||||
class="input btn-fake ml"
|
||||
class="btn btn-alt btn-wide ml"
|
||||
autocomplete="off"
|
||||
aria-label="<?=lang('action_new_post_tip')?>"
|
||||
>
|
|
@ -1,4 +1,8 @@
|
|||
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
|
||||
<?php /* vi: syntax=php */ ?>
|
||||
<footer>
|
||||
Freya Murphy © 2023 | <a href="https://freya.cat">freya.cat</a>
|
||||
</footer>
|
||||
<body>
|
||||
|
||||
</html>
|
|
@ -2,28 +2,8 @@
|
|||
<?php /* vi: syntax=php */ ?>
|
||||
<?php
|
||||
$self = $this->main->user();
|
||||
$this->view('header_empty', $data);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
<?php if ($this->main->session): ?>
|
||||
var jwtStr = <?=json_encode($this->main->session['jwt'])?>;
|
||||
<?php else: ?>
|
||||
var jwtStr = null;
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
<?php
|
||||
foreach ($js_files as $js) {
|
||||
echo $this->main->link_js($js);
|
||||
}
|
||||
foreach ($css_files as $css) {
|
||||
echo $this->main->link_css($css);
|
||||
}
|
||||
?>
|
||||
<title><?=$title?></title>
|
||||
</head>
|
||||
<body>
|
||||
<header class="nav">
|
||||
<div class="nav-left">
|
||||
<span class="logo">xssbook</span>
|
||||
|
@ -31,7 +11,7 @@
|
|||
<div class="nav-center" :class="{hidden: !visible}">
|
||||
<a
|
||||
id="action-home"
|
||||
class="header-entry btn btn-hover btn-action btn-blue"
|
||||
class="btn"
|
||||
href="/home"
|
||||
title="<?=lang('action_home_tip')?>"
|
||||
>
|
||||
|
@ -40,7 +20,7 @@
|
|||
</a>
|
||||
<a
|
||||
id="action-people"
|
||||
class="header-entry btn btn-hover btn-action btn-blue"
|
||||
class="btn"
|
||||
href="/people"
|
||||
title="<?=lang('action_people_tip')?>"
|
||||
>
|
||||
|
@ -49,7 +29,7 @@
|
|||
</a>
|
||||
<a
|
||||
id="action-chat"
|
||||
class="header-entry btn btn-hover btn-action btn-blue"
|
||||
class="btn"
|
||||
href="/chat"
|
||||
title="<?=lang('action_chat_tip')?>"
|
||||
>
|
||||
|
@ -70,7 +50,7 @@
|
|||
'class' => 'pfp-sm ml',
|
||||
)); ?>
|
||||
<?php else: ?>
|
||||
<?=ilang('action_login', class: 'btn btn-action', href: '/auth/login')?>
|
||||
<?=ilang('action_login', class: 'btn', href: '/auth/login')?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<script>
|
||||
|
@ -80,5 +60,3 @@
|
|||
});
|
||||
</script>
|
||||
</header>
|
||||
<div id="toast-container">
|
||||
</div>
|
23
src/web/_views/header_empty.php
Normal file
23
src/web/_views/header_empty.php
Normal 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>
|
|
@ -22,7 +22,7 @@
|
|||
<div class="modal-footer">
|
||||
<?=ilang('action_submit',
|
||||
id: 'new-post-submit',
|
||||
class: 'btn-action',
|
||||
class: 'btn btn-wide btn-submit',
|
||||
attrs: array('type' => 'submit'),
|
||||
button: TRUE
|
||||
)?>
|
||||
|
@ -32,14 +32,28 @@
|
|||
$('#new-post-form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
let content = $('#new-post-content').val();
|
||||
let me = $(this);
|
||||
|
||||
const getPost = function(data) {
|
||||
if (data) {
|
||||
$('#post-container').prepend(data);
|
||||
}
|
||||
me.closest('.modal-container').remove();
|
||||
}
|
||||
|
||||
const onPost = function(data) {
|
||||
let id = data[0].id;
|
||||
$.get({
|
||||
url: '/_util/post/post?id=' + id,
|
||||
success: getPost
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/api/post',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({ content }),
|
||||
success: function(data) {
|
||||
window.location.reload();
|
||||
},
|
||||
success: onPost
|
||||
});
|
||||
});
|
||||
</script>
|
173
src/web/_views/modal/register.php
Normal file
173
src/web/_views/modal/register.php
Normal 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>
|
|
@ -8,7 +8,7 @@
|
|||
<div class="ml col sub-card">
|
||||
<div class="row">
|
||||
<strong><?=$format_model->name($user)?></strong>
|
||||
<span class="dim ml"><?=$format_model->date($comment['date'])?></span>
|
||||
<span class="dim ml"><?=$format_model->date($comment['created'])?></span>
|
||||
</div>
|
||||
<?=$comment['content']?>
|
||||
</div>
|
|
@ -5,7 +5,7 @@
|
|||
<?php $this->view('template/pfp', array('user' => $user))?>
|
||||
<div class="col ml">
|
||||
<strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong>
|
||||
<span class="dim"><?=$post['date']?></span>
|
||||
<span class="dim"><?=$post['created']?></span>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
|
@ -13,20 +13,28 @@
|
|||
</p>
|
||||
<?php
|
||||
$self = $this->main->user();
|
||||
$liked = $post['like_id'] ? 'btn-blue' : '';
|
||||
$post_attrs = array(
|
||||
'postId' => $post['id']
|
||||
);
|
||||
if ($post['like_id'] !== NULL) {
|
||||
$post_attrs['likeId'] = $post['like_id'];
|
||||
}
|
||||
?>
|
||||
<?php if ($self): ?>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<?=ilang('action_like', class: 'grow btn btn-hover btn-action')?>
|
||||
<?=ilang('action_comment', class: 'grow btn btn-hover btn-action action-comment',
|
||||
click: '$(\'#new-comment-' . $post['id'] . '\').focus()'
|
||||
<?=ilang('action_like',
|
||||
class: 'btn btn-wide action-like ' . $liked,
|
||||
attrs: $post_attrs
|
||||
)?>
|
||||
<?=ilang('action_comment', class: 'btn btn-wide action-comment',
|
||||
click: '$(\'#action-new-comment-' . $post['id'] . '\').focus()'
|
||||
)?>
|
||||
</div>
|
||||
<hr>
|
||||
<?php else: ?>
|
||||
<hr>
|
||||
<?php endif; ?>
|
||||
<div class="col comments">
|
||||
<div class="col comments pb">
|
||||
<?php
|
||||
$_GET = array('id' => $post['id']);
|
||||
$cdata = $this->comments();
|
||||
|
@ -52,16 +60,17 @@
|
|||
?>
|
||||
</div>
|
||||
<?php if ($self): ?>
|
||||
<div class="row grow mt">
|
||||
<div class="row pb">
|
||||
<?php $this->view('template/pfp', array('user' => $user))?>
|
||||
<form class="ml action-new-comment-form">
|
||||
<form class="ml action-new-comment-form row">
|
||||
<input
|
||||
type="hidden"
|
||||
name="id"
|
||||
value="<?=$post['id']?>"
|
||||
>
|
||||
<input
|
||||
class="action-new-comment input"
|
||||
id="action-new-comment-<?=$post['id']?>"
|
||||
class="action-new-comment btn btn-wide btn-alt"
|
||||
postId="<?=$post['id']?>"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
|
@ -73,3 +82,5 @@
|
|||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
if ($loaded >= $page_size && $page_size < $total) {
|
||||
ilang('action_load_posts',
|
||||
id: 'action-load-posts',
|
||||
class: 'btn btn-line mb',
|
||||
class: 'btn btn-line btn-wide mb',
|
||||
attrs: array(
|
||||
'loaded' => $loaded,
|
||||
'pageSize' => $page_size,
|
|
@ -11,9 +11,16 @@
|
|||
array_push($params, $hint);
|
||||
}
|
||||
|
||||
$msg = lang($msg, sub: $params);
|
||||
$lang_msg = lang($msg, FALSE, sub: $params);
|
||||
|
||||
if(!$lang_msg) {
|
||||
$lang_msg = $msg;
|
||||
} else {
|
||||
$lang_msg = ucfirst($lang_msg);
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="toast error">
|
||||
<?=ucfirst($msg)?>
|
||||
<?=$lang_msg?>
|
||||
<?=ilang('action_close', class: 'action-close-toast')?>
|
||||
</div>
|
|
@ -30,6 +30,11 @@ class Aesthetic {
|
|||
'css/post.css'
|
||||
],
|
||||
),
|
||||
'auth' => array(
|
||||
'css' => [
|
||||
'css/auth.css'
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
/**
|
|
@ -3,5 +3,6 @@
|
|||
$routes = array();
|
||||
$routes['home'] = 'apps/home';
|
||||
$routes['error'] = 'apps/error';
|
||||
$routes['auth'] = 'apps/auth';
|
||||
|
||||
$routes[''] = '_index';
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
|
@ -122,6 +122,11 @@ class DatabaseQuery {
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function order_by($column, $order = 'ASC') {
|
||||
$this->query .= "ORDER BY " . $column . ' ' . $order . ' ';
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function rows() {
|
||||
$stmt = $this->conn->prepare($this->query);
|
||||
try {
|
|
@ -69,7 +69,7 @@ function ilang($key,
|
|||
}
|
||||
echo '>' . $text . '</span>';
|
||||
}
|
||||
if ($click) {
|
||||
if ($click || $button) {
|
||||
echo '</button>';
|
||||
} else {
|
||||
echo '</a>';
|
|
@ -1,5 +1,6 @@
|
|||
<?php /* Copyright (c) 2024 Freya Murphy */
|
||||
|
||||
session_save_path('/var/lib/php/session');
|
||||
session_start();
|
||||
|
||||
$webroot = dirname(__FILE__);
|
|
@ -6,6 +6,7 @@ $lang['api_column_first_name'] = 'first name';
|
|||
$lang['api_column_last_name'] = 'last name';
|
||||
$lang['api_column_middle_name'] = 'middle name';
|
||||
$lang['api_column_email'] = 'email';
|
||||
$lang['api_column_password'] = 'password';
|
||||
$lang['api_column_gender'] = 'gender';
|
||||
$lang['api_column_join_date'] = 'join date';
|
||||
$lang['api_column_birth_date'] = 'birth date';
|
||||
|
@ -22,5 +23,10 @@ $lang['api_null_value'] = '%s cannot be empty';
|
|||
$lang['api_unique_value'] = '%s is not available (not unique)';
|
||||
$lang['api_min_value'] = '%s length cannot be less than %s';
|
||||
$lang['api_max_value'] = '%s length cannot exceed %s';
|
||||
$lang['api_invalid_login'] = 'Invalid username or password';
|
||||
$lang['api_unknown'] = 'An unknown error as occurred';
|
||||
|
||||
// toast messages
|
||||
$lang['toast_date_empty'] = 'Birthday cannot be empty';
|
||||
|
||||
?>
|
34
src/web/lang/en_US/apps/auth.php
Normal file
34
src/web/lang/en_US/apps/auth.php
Normal 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';
|
||||
|
||||
?>
|
Loading…
Reference in a new issue