a mini social media app for small communities

no more sql injection on searches! (hopefully) i also have a more scalable system for sql functions/procedures/etc

Changed files
+70 -27
doc
src
+19
doc/resources.md
··· 9 9 ## database design 10 10 11 11 - https://stackoverflow.com/questions/59505855/liked-posts-design-specifics 12 + - my programmer brain automatically assumed "oh i can just store a list 13 + in the user table!" turns out, that is a bad implementation. 14 + - i do have scalability concerns with the current implementation, but i 15 + can address those in the near future. 12 16 13 17 ## sql 14 18 19 + postgresql documentation: https://www.postgresql.org/docs/ 20 + 15 21 - https://stackoverflow.com/questions/11144394/order-sql-by-strongest-like 22 + - helped me develop the initial search system, which is subject to be 23 + overhauled, but for now, this helped a lot. 24 + 25 + ## sql security 26 + 27 + ![xkcd comic #327](https://imgs.xkcd.com/comics/exploits_of_a_mom.png) 28 + 29 + source: xkcd, <https://xkcd.com/327/> 30 + 31 + - sql injections 32 + - https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html#other-examples-of-safe-prepared-statements 33 + - https://cheatsheetseries.owasp.org/cheatsheets/Query_Parameterization_Cheat_Sheet.html#using-net-built-in-feature 34 + - https://www.slideshare.net/slideshow/sql-injection-myths-and-fallacies/3729931#3
+1
doc/todo.md
··· 36 36 - could be used so that a github webhook can send a message when a new commit is pushed to beep! 37 37 - [ ] site:log new accounts, account deletions, etc etc in an admin-accessible site log 38 38 - this should be set up to only log things when an admin enables it in the site config, so as to only log when necessary 39 + - [ ] site:implement a database keep-alive system 39 40 40 41 ## ideas 41 42
+17
src/beep_sql/beep_sql.v
··· 1 + module beep_sql 2 + 3 + import os 4 + import db.pg 5 + 6 + fn load_procedures(mut db pg.DB) { 7 + os.walk('src/beep_sql/procedures/', fn [mut db] (it string) { 8 + println('-> loading procedure: ${it}') 9 + db.exec(os.read_file(it) or { panic(err) }) or { panic(err) } 10 + }) 11 + } 12 + 13 + pub fn load(mut db pg.DB) { 14 + println('-> loading sql code') 15 + load_procedures(mut db) 16 + println('<- done') 17 + }
+12
src/beep_sql/procedures/search_posts.sql
··· 1 + CREATE OR REPLACE FUNCTION search_for_posts (IN Query TEXT, IN Count INT, IN Index INT) 2 + RETURNS SETOF "Post" 3 + AS $$ 4 + SELECT * 5 + FROM "Post" 6 + WHERE title LIKE CONCAT('%', Query, '%') OR body LIKE CONCAT('%', Query, '%') 7 + ORDER BY (CASE 8 + WHEN title LIKE CONCAT('%', Query, '%') THEN 1 9 + WHEN body LIKE CONCAT('%', Query, '%') THEN 2 10 + END) 11 + LIMIT Count OFFSET Index; 12 + $$ LANGUAGE SQL;
+12
src/beep_sql/procedures/search_users.sql
··· 1 + CREATE OR REPLACE FUNCTION search_for_users (IN Query TEXT, IN Count INT, IN Index INT) 2 + RETURNS SETOF "User" 3 + AS $$ 4 + SELECT * 5 + FROM "User" 6 + WHERE username LIKE CONCAT('%', Query, '%') OR nickname LIKE CONCAT('%', Query, '%') 7 + ORDER BY (CASE 8 + WHEN username LIKE CONCAT('%', Query, '%') THEN 1 9 + WHEN nickname LIKE CONCAT('%', Query, '%') THEN 2 10 + END) 11 + LIMIT Count OFFSET Index; 12 + $$ LANGUAGE SQL;
+4 -14
src/database/post.v
··· 161 161 } 162 162 163 163 // search_for_posts searches for posts matching the given query. 164 - // todo: query options/filters, such as user:beep, !excluded-text, etc 164 + // todo: levenshtein distance, query options/filters (user:beep, !excluded-text, 165 + // etc) 165 166 pub fn (app &DatabaseAccess) search_for_posts(query string, limit int, offset int) []PostSearchResult { 166 - //TODO: SANATIZE 167 - sql_query := "\ 168 - SELECT *, CASE 169 - WHEN title LIKE '%${query}%' THEN 1 170 - WHEN body LIKE '%${query}%' THEN 2 171 - END AS priority 172 - FROM \"Post\" 173 - WHERE title LIKE '%${query}%' OR body LIKE '%${query}%' 174 - ORDER BY priority ASC LIMIT ${limit} OFFSET ${offset}" 175 - 176 - queried_posts := app.db.q_strings(sql_query) or { 177 - eprintln('search_for_posts error in app.db.q_strings: ${err}') 167 + queried_posts := app.db.exec_param_many('SELECT * FROM search_for_posts($1, $2, $3)', [query, limit.str(), offset.str()]) or { 168 + eprintln('search_for_posts error in app.db.error: ${err}') 178 169 [] 179 170 } 180 - 181 171 posts := queried_posts.map(|it| Post.from_row(it)) 182 172 return PostSearchResult.from_post_list(app, posts) 183 173 }
+2 -13
src/database/user.v
··· 210 210 // search_for_users searches for posts matching the given query. 211 211 // todo: query options/filters, such as created-after:<date>, created-before:<date>, etc 212 212 pub fn (app &DatabaseAccess) search_for_users(query string, limit int, offset int) []User { 213 - //TODO: SANATIZE 214 - sql_query := "\ 215 - SELECT *, CASE 216 - WHEN username LIKE '%${query}%' THEN 1 217 - WHEN nickname LIKE '%${query}%' THEN 2 218 - END AS priority 219 - FROM \"User\" 220 - WHERE username LIKE '%${query}%' OR nickname LIKE '%${query}%' 221 - ORDER BY priority ASC LIMIT ${limit} OFFSET ${offset}" 222 - 223 - queried_users := app.db.q_strings(sql_query) or { 224 - eprintln('search_for_users error in app.db.q_strings: ${err}') 213 + queried_users := app.db.exec_param_many('SELECT * FROM search_for_users($1, $2, $3)', [query, limit.str(), offset.str()]) or { 214 + eprintln('search_for_users error in app.db.error: ${err}') 225 215 [] 226 216 } 227 - 228 217 users := queried_users.map(|it| User.from_row(it)) 229 218 return users 230 219 }
+3
src/main.v
··· 6 6 import entity 7 7 import os 8 8 import webapp { App, Context, StringValidator } 9 + import beep_sql 9 10 10 11 fn main() { 11 12 config := webapp.load_config_from(os.args[1]) ··· 23 24 defer { 24 25 db.close() 25 26 } 27 + 28 + beep_sql.load(mut db) 26 29 27 30 mut app := &App{ 28 31 config: config