Parcourir la source

- Post editing

- Post deleting
- Simple search functionality
- CSS now uses vars
- Fixing text box font size on iOS
master
Rocketsoup il y a 3 ans
Parent
révision
522c793825
4 fichiers modifiés avec 324 ajouts et 104 suppressions
  1. 169
    26
      htdocs/index.php
  2. 150
    74
      htdocs/journal.css
  3. BIN
      journal.db
  4. 5
    4
      source/create-tables.sql

+ 169
- 26
htdocs/index.php Voir le fichier

@@ -97,16 +97,18 @@ function trace(string $message): void {
97 97
 
98 98
 /// Represents a journal post.
99 99
 class Post {
100
-	public int $post_id;
100
+	public ?int $post_id;
101 101
 	public string $body;
102 102
 	public int $author_id;
103 103
 	public int $created;
104
+	public ?int $updated;
104 105
 
105 106
 	function __construct(array $row) {
106 107
 		$this->post_id = $row['rowid'];
107 108
 		$this->body = $row['body'];
108 109
 		$this->author_id = $row['author_id'];
109 110
 		$this->created = $row['created'];
111
+		$this->updated = $row['updated'];
110 112
 	}
111 113
 
112 114
 	/// Normalizes the body of a post.
@@ -167,6 +169,7 @@ class Post {
167 169
 	public static function get_posts(
168 170
 			int $user_id,
169 171
 			int $count = RECENT_POSTS_PER_PAGE,
172
+			?string $query = null,
170 173
 			?int $before_time = null): array {
171 174
 		$sql = 'SELECT rowid, * FROM posts WHERE author_id=:author_id';
172 175
 		$args = array(
@@ -177,6 +180,16 @@ class Post {
177 180
 			$sql .= ' AND created < :before_time';
178 181
 			$args[':before_time'] = $before_time;
179 182
 		}
183
+		$search_where = '';
184
+		if ($query) {
185
+			foreach (explode(' ', $query) as $i => $term) {
186
+				if (strlen($term) == 0) continue;
187
+				$symbol = ":wordpattern{$i}";
188
+				$search_where .= " AND body LIKE $symbol ESCAPE '!'";
189
+				$args[$symbol] = '%' . Database::escape_like($term, '!') . '%';
190
+			}
191
+			$sql .= $search_where;
192
+		}
180 193
 		$sql .= ' ORDER BY created DESC LIMIT :count;';
181 194
 		$posts = Database::query_objects('Post', $sql, $args);
182 195
 
@@ -189,12 +202,8 @@ class Post {
189 202
 		if ($before_time) {
190 203
 			// We're paged forward. Check if there are newer posts.
191 204
 			$sql = 'SELECT rowid, * FROM posts WHERE author_id=:author_id AND ' .
192
-				'created >= :before_time ORDER BY created ASC LIMIT :count;';
193
-			$args = array(
194
-				':author_id' => $user_id,
195
-				':count' => $count + 1, // to see if it's the newest page
196
-				':before_time' => $before_time,
197
-			);
205
+				'created >= :before_time ' . $search_where . ' ORDER BY created ASC LIMIT :count;';
206
+			// Reusing same $args
198 207
 			$newer_posts = Database::query_objects('Post', $sql, $args);
199 208
 			if (sizeof($newer_posts) > $count) {
200 209
 				$prev_date = $newer_posts[array_key_last($newer_posts)]->created;
@@ -203,9 +212,48 @@ class Post {
203 212
 				$prev_page = BASE_URL;
204 213
 			}
205 214
 		}
215
+		if ($query) {
216
+			if ($prev_page) {
217
+				$prev_page .= (str_contains($prev_page, '?') ? '&' : '?') .
218
+					'search=' . urlencode($query);
219
+			}
220
+			if ($next_page) {
221
+				$next_page .= (str_contains($next_page, '?') ? '&' : '?') .
222
+					'search=' . urlencode($query);
223
+			}
224
+		}
206 225
 
207 226
 		return array($posts, $prev_page, $next_page);
208 227
 	}
228
+
229
+	/// Fetches a post by its post ID.
230
+	/// @param int $post_id  ID of the post.
231
+	/// @return ?Post  The Post, or null if not found.
232
+	public static function get_by_id(int $post_id): ?Post {
233
+		$sql = 'SELECT rowid, * FROM posts WHERE rowid=:post_id;';
234
+		$args = array(':post_id' => $post_id);
235
+		return Database::query_object('Post', $sql, $args);
236
+	}
237
+
238
+	/// Deletes this post.
239
+	public function delete(): void {
240
+		$sql = 'DELETE FROM posts WHERE rowid=:post_id;';
241
+		$args = array(':post_id' => $this->post_id);
242
+		Database::query($sql, $args);
243
+		$this->post_id = null;
244
+	}
245
+
246
+	/// Update text of post.
247
+	public function update(string $new_body): void {
248
+		$new_body = self::normalize_body($new_body);
249
+		$sql = 'UPDATE posts SET body=:body, updated=:updated WHERE rowid=:rowid;';
250
+		$args = array(
251
+			':body' => $new_body,
252
+			':updated' => time(),
253
+			':rowid' => $this->post_id,
254
+		);
255
+		Database::query($sql, $args);
256
+	}
209 257
 }
210 258
 
211 259
 /// Represents a user.
@@ -477,9 +525,12 @@ class Database {
477 525
 	///                 and named column values.
478 526
 	public static function query(string $sql, array $params = array()): bool|array {
479 527
 		$db = new SQLite3(DB_PATH);
528
+		trace('SQL: ' . $sql);
480 529
 		$stmt = $db->prepare($sql);
481 530
 		foreach ($params as $name => $value) {
482
-			$stmt->bindValue($name, $value, self::sqlite_type($value));
531
+			$type = self::sqlite_type($value);
532
+			$stmt->bindValue($name, $value, $type);
533
+			trace("\tbind {$name} => {$value} ({$type})");
483 534
 		}
484 535
 		$result = $stmt->execute();
485 536
 		if (gettype($result) == 'bool') {
@@ -514,6 +565,18 @@ class Database {
514 565
 				fatal_error("Bad datatype in sqlite statement");
515 566
 		}
516 567
 	}
568
+
569
+	/// Escapes a string for use in a LIKE clause.
570
+	/// @param string $value   String to escape.
571
+	/// @param string $escape  Character to use for escaping. Default is !
572
+	/// @return string  Escaped string
573
+	public static function escape_like(string $value, string $escape='!'): string {
574
+		$s = $value;
575
+		$s = str_replace($escape, $escape . $escape, $s); // escape char
576
+		$s = str_replace('%', $escape . '%', $s); // any chars
577
+		$s = str_replace('_', $escape . '_', $s); // one char
578
+		return $s;
579
+	}
517 580
 }
518 581
 
519 582
 /// App configuration management. All config is stored in the 'config' table in
@@ -717,11 +780,13 @@ class HTMLPage {
717 780
 		if (User::$current) {
718 781
 			print(<<<HTML
719 782
 							<div class="menu-container">
720
-								<details>
721
-									<summary><span>☰</span></summary>
783
+								<details class="menu">
784
+									<summary class="no-indicator menu-button"><span>☰</span></summary>
722 785
 
723 786
 									<ul>
724
-										<a href="?logout"><li>Log out</li></a>
787
+										<li><a href="?search">Search</a></li>
788
+										<li class="menu-divider"></li>
789
+										<li class="logout-item destructive"><a href="?logout">Log out</a></li>
725 790
 									</ul>
726 791
 								</details>
727 792
 							</div>
@@ -758,21 +823,42 @@ class HTMLPage {
758 823
 	}
759 824
 
760 825
 	public static function render_post_form(): void {
761
-		$body = array_key_exists(SESSION_KEY_POST_BODY, $_SESSION) ? $_SESSION[SESSION_KEY_POST_BODY] : '';
762
-		unset($_SESSION[SESSION_KEY_POST_BODY]);
826
+		$action = 'post';
827
+		$verb = 'Post';
828
+		$body = '';
829
+		if ($edit_id = validate($_GET, 'edit', INPUT_TYPE_INT, required: false)) {
830
+			if ($post = Post::get_by_id($edit_id)) {
831
+				$body = $post->body;
832
+				$action = 'edit';
833
+				$verb = 'Update';
834
+			} else {
835
+				unset($edit_id);
836
+			}
837
+		} elseif (array_key_exists(SESSION_KEY_POST_BODY, $_SESSION)) {
838
+			$body = $_SESSION[SESSION_KEY_POST_BODY];
839
+			unset($_SESSION[SESSION_KEY_POST_BODY]);
840
+		}
841
+		$body_html = htmlentities($body);
763 842
 		print(<<<HTML
764 843
 			<form id="post-form" method="POST">
765
-				<div class="text-container"><textarea name="body" placeholder="Your journal post here…">$body</textarea></div>
766
-				<input type="hidden" name="action" value="post" />
767
-				<div class="submit-post"><input type="submit" value="Post" /></div>
844
+				<div class="text-container"><textarea name="body" placeholder="Your journal post here…">$body_html</textarea></div>
845
+				<input type="hidden" name="action" value="{$action}" />
846
+			HTML
847
+		);
848
+		if ($edit_id) {
849
+			print("<input type=\"hidden\" name=\"edit_id\" value=\"{$edit_id}\" />");
850
+		}
851
+		print(<<<HTML
852
+				<div class="submit-post"><input type="submit" value="{$verb}" /></div>
768 853
 			</form>
769 854
 			HTML
770 855
 		);
771 856
 	}
772 857
 
773 858
 	public static function render_recent_posts(): void {
859
+		$query = validate($_GET, 'search', INPUT_TYPE_STRING | INPUT_TYPE_TRIMMED, required: false);
774 860
 		$before = validate($_GET, 'before', INPUT_TYPE_INT, required: false);
775
-		[ $posts, $prev_url, $next_url ] = Post::get_posts(User::$current->user_id, before_time: $before);
861
+		[ $posts, $prev_url, $next_url ] = Post::get_posts(User::$current->user_id, query: $query, before_time: $before);
776 862
 		print("<div class=\"post-container\">\n");
777 863
 		if ($prev_url !== null) {
778 864
 			print("<div class=\"previous\"><a href=\"{$prev_url}\">Previous</a></div>\n");
@@ -788,12 +874,18 @@ class HTMLPage {
788 874
 
789 875
 	/// Encodes a post body as HTML. Inserts linebreaks and paragraph tags.
790 876
 	private static function post_body_html(string $body): string {
877
+		$html_p_start = "<p>";
878
+		$html_p_end = "</p>\n";
879
+		$html_newline = "<br/>\n";
791 880
 		$body_html = htmlentities($body);
792 881
 		// Single newlines are turned into linebreaks.
793
-		$body_html = str_replace("\n", "<br/>\n", $body_html);
882
+		$body_html = str_replace("\n", $html_newline, $body_html);
794 883
 		// Pairs of newlines are turned into paragraph separators.
795
-		$paragraphs = explode("<br/>\n<br/>\n", $body_html);
796
-		$body_html = "<p>" . implode("</p>\n\n<p>", $paragraphs) . "</p>\n";
884
+		$body_html = $html_p_start .
885
+			str_replace($html_newline . $html_newline,
886
+				$html_p_end . $html_p_start,
887
+				$body_html) .
888
+			$html_p_end;
797 889
 		return $body_html;
798 890
 	}
799 891
 
@@ -804,17 +896,51 @@ class HTMLPage {
804 896
 			<div class="post">
805 897
 				<article>
806 898
 					<div class="post-body">{$body_html}</div>
807
-					<footer>
808
-						<div class="post-date">
809
-							Posted {$date}
810
-						</div>
811
-					</footer>
899
+					<div class="post-footer">
900
+						<footer class="secondary-text">
901
+							<div class="post-date">
902
+								Posted {$date}
903
+			HTML
904
+		);
905
+		if ($post->updated && $post->updated != $post->created) {
906
+			print('<br/>(updated ' . localized_date_string($post->updated) . ')');
907
+		}
908
+		print(<<<HTML
909
+							</div>
910
+							<details class="post-actions menu">
911
+								<summary class="no-indicator menu-button"><span>actions</span></summary>
912
+
913
+								<ul>
914
+									<li><a href="?edit={$post->post_id}">Edit</a></li>
915
+									<li class="menu-divider"></li>
916
+									<li class="post-action-delete destructive"><a href="?delete={$post->post_id}">Delete</a></li>
917
+								</ul>
918
+							</details>
919
+						</footer>
920
+					</div>
812 921
 				</article>
813 922
 			</div>
814 923
 			HTML
815 924
 		);
816 925
 	}
817 926
 
927
+	public static function render_search_form(): void {
928
+		$q = validate($_GET, 'search', INPUT_TYPE_STRING | INPUT_TYPE_TRIMMED, required: false);
929
+		$q_html = htmlentities($q);
930
+		$cancel = BASE_URL;
931
+		print(<<<HTML
932
+			<form id="search-form" method="GET">
933
+				<div>
934
+					<label for="search">Search:</label>
935
+					<input type="text" name="search" id="search" value="{$q_html}" autocapitalize="off" />
936
+					<input type="submit" value="Search" />
937
+					<a href="{$cancel}">Cancel</a>
938
+				</div>
939
+			</form>
940
+			HTML
941
+		);
942
+	}
943
+
818 944
 	public static function render_sign_in_form(): void {
819 945
 		print(<<<HTML
820 946
 			<form id="signin-form" method="POST">
@@ -888,7 +1014,15 @@ switch ($_SERVER['REQUEST_METHOD']) {
888 1014
 		HTMLPage::render_page_start();
889 1015
 		HTMLPage::render_error_if_needed();
890 1016
 		if (User::$current) {
891
-			HTMLPage::render_post_form();
1017
+			if ($delete_id = validate($_GET, 'delete', INPUT_TYPE_INT, required: false)) {
1018
+				Post::get_by_id($delete_id)?->delete();
1019
+				HTMLPage::redirect_home();
1020
+			}
1021
+			if (array_key_exists('search', $_GET)) {
1022
+				HTMLPage::render_search_form();
1023
+			} else {
1024
+				HTMLPage::render_post_form();
1025
+			}
892 1026
 			HTMLPage::render_recent_posts();
893 1027
 		} elseif (User::any_exist()) {
894 1028
 			HTMLPage::render_sign_in_form();
@@ -912,6 +1046,15 @@ switch ($_SERVER['REQUEST_METHOD']) {
912 1046
 				}
913 1047
 				Post::create($body, $author, $created);
914 1048
 				break;
1049
+			case 'edit':
1050
+				$body = validate($_POST, 'body', $nonempty_str_type);
1051
+				$edit_id = validate($_POST, 'edit_id', INPUT_TYPE_INT);
1052
+				if (!User::$current) {
1053
+					// Not logged in. Save body for populating once they sign in.
1054
+					fatal_error('Please sign in to edit.');
1055
+				}
1056
+				Post::get_by_id($edit_id)?->update($body);
1057
+				break;
915 1058
 			case 'createaccount':
916 1059
 				$username = validate($_POST, 'username', INPUT_TYPE_USERNAME);
917 1060
 				$password = validate($_POST, 'password', $password_type);

+ 150
- 74
htdocs/journal.css Voir le fichier

@@ -1,29 +1,133 @@
1
+/* Colors */
2
+
1 3
 :root {
2
-	background-color: white;
3
-	color: black;
4
+	--page-background: #fff;
5
+	--text-color: #000;
6
+	--secondary-text-color: #888;
7
+	--nav-background: #fff;
8
+	--menu-background: #eee;
9
+	--menu-border: #ccc;
10
+
11
+	--highlight: #d83;
12
+	--error-text: #d00;
13
+	--error-background: #fcc;
14
+	--error-border: #400;
15
+	--callout-text: #000;
16
+	--callout-background: #ffc;
17
+	--callout-border: #886;
18
+	--destructive: #e00;
19
+	--destructive-contrast: #fff;
20
+}
21
+
22
+@media(prefers-color-scheme: dark) {
23
+	:root {
24
+		--page-background: #222;
25
+		--text-color: #fff;
26
+		--secondary-text-color: #888;
27
+		--nav-background: #444;
28
+		--menu-background: #444;
29
+		--menu-border: #000;
30
+	}
4 31
 }
32
+
33
+/* Base */
34
+
5 35
 :root, input, textarea {
6 36
 	font-family: Garamond, Times New Roman, serif;
7 37
 	line-height: 1.25;
8 38
 	font-size: 12pt;
9 39
 }
40
+body, textarea, input, select {
41
+	background-color: var(--page-background);
42
+	color: var(--text-color);
43
+}
10 44
 body {
11 45
 	margin: 0;
12 46
 	padding: 0;
13 47
 }
14 48
 a {
15
-	color: #d83;
49
+	color: var(--highlight);
16 50
 	text-decoration: none;
17 51
 }
52
+summary {
53
+	-webkit-user-select: none;
54
+	user-select: none;
55
+}
56
+summary.no-indicator {
57
+	list-style-type: none;
58
+}
59
+summary.no-indicator::-webkit-details-marker {
60
+	display: none;
61
+}
62
+.secondary-text {
63
+	color: var(--secondary-text-color);
64
+	font-size: 0.8rem;
65
+}
66
+details.menu ul {
67
+	position: relative;
68
+	z-index: 1;
69
+	margin: 0;
70
+	padding: 0;
71
+	list-style-type: none;
72
+	border: 1px solid var(--menu-border);
73
+	background-color: var(--menu-background);
74
+	box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.25);
75
+	text-align: start;
76
+}
77
+details.menu li a {
78
+	color: var(--highlight);
79
+}
80
+details.menu li a:hover {
81
+	background-color: var(--highlight);
82
+	color: black;
83
+}
84
+details.menu li {
85
+}
86
+details.menu li a {
87
+	padding: 0.4em 1em;
88
+	display: inline-block;
89
+	position: relative;
90
+	width: 10ch;
91
+}
92
+details.menu li + li {
93
+	border-top: 1px solid var(--menu-border);
94
+}
95
+details.menu li.menu-divider {
96
+	padding-top: 0.75em;
97
+}
98
+details.menu li.destructive a {
99
+	color: var(--destructive);
100
+}
101
+details.menu li.destructive a:hover {
102
+	background-color: var(--destructive);
103
+	color: var(--destructive-contrast);
104
+}
105
+summary.menu-button:hover {
106
+	color: var(--highlight);
107
+}
108
+summary.menu-button > span:first-child {
109
+	display: inline-block;
110
+	line-height: 1em;
111
+	padding: 0.5rem 0.7em;
112
+	margin: 0;
113
+	border-radius: 0.5em;
114
+}
115
+details[open] summary.menu-button > span:first-child {
116
+	background-color: var(--highlight);
117
+	color: white;
118
+}
119
+
120
+
121
+/* Navigation */
18 122
 
19 123
 .top-nav {
20 124
 	position: fixed;
21 125
 	top: 0;
22
-	left: 0;
23
-	right: 0;
126
+	inset-inline-start: 0;
127
+	inset-inline-end: 0;
24 128
 	height: 2em;
25 129
 	text-align: center;
26
-	background-color: white;
130
+	background-color: var(--nav-background);
27 131
 	box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.25);
28 132
 }
29 133
 .title {
@@ -39,79 +143,42 @@ a {
39 143
 	right: 0;
40 144
 }
41 145
 .menu-container summary {
42
-	list-style-type: none;
43 146
 	text-align: right;
44
-	-webkit-user-select: none;
45
-	user-select: none;
46
-}
47
-.menu-container summary:hover {
48
-	color: #d83;
49
-}
50
-.menu-container summary span {
51
-	display: inline-block;
52
-	line-height: 1rem;
53
-	padding: 0.5rem 0.7rem;
54
-	margin: 0;
55
-	border-radius: 0.5rem;
56
-}
57
-.menu-container details {
58
-	text-align: left;
59
-}
60
-.menu-container details[open] summary span {
61
-	background-color: #d83;
62
-	color: white;
63
-}
64
-.menu-container ul {
65
-	margin: 0;
66
-	padding: 0;
67
-	list-style-type: none;
68
-	border: 1px solid #ccc;
69
-	background-color: #eee;
70
-	box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.25);
71
-}
72
-.menu-container a li {
73
-	color: #d83;
74
-}
75
-.menu-container a:hover li {
76
-	background-color: #d83;
77
-	color: black;
78
-}
79
-.menu-container li {
80
-	padding: 0.4em 1em;
81
-}
82
-.menu-container a + a li {
83
-	border-top: 1px solid #ccc;
84
-}
85
-summary::-webkit-details-marker {
86
-	display: none;
87 147
 }
88 148
 
149
+/* Content container */
150
+
89 151
 .content {
90 152
 	max-width: 66ch;
91 153
 	margin: 2.5em auto 2em auto;
92 154
 	padding: 0.5em;
93 155
 }
94 156
 
157
+/* Errors and alerts */
158
+
95 159
 .error {
96 160
 	padding: 1em;
97
-	border: 1px solid #400;
98
-	background-color: #fcc;
99
-	color: #d00;
161
+	border: 1px solid var(--error-border);
162
+	background-color: var(--error-background);
163
+	color: var(--error-text);
100 164
 	font-weight: bold;
101 165
 	margin-bottom: 1em;
102 166
 }
103 167
 .important {
104 168
 	padding: 1em;
105
-	border: 1px solid #886;
106
-	background-color: #ffc;
107
-	color: black;
169
+	border: 1px solid var(--callout-border);
170
+	background-color: var(--callout-background);
171
+	color: var(--callout-text);
108 172
 	margin-bottom: 1em;
109 173
 }
110 174
 
175
+/* Post form */
176
+
111 177
 textarea {
112 178
 	width: 100%;
113 179
 	height: 8em;
114 180
 	font-family: inherit;
181
+	font-size: 1rem;
115 182
 	margin: 0;
116 183
 	box-sizing: border-box;
117 184
 }
@@ -126,6 +193,8 @@ input[type="submit"] {
126 193
 	min-width: 15ch;
127 194
 }
128 195
 
196
+/* Posts */
197
+
129 198
 .post-container {
130 199
 	margin-top: 1.5rem;
131 200
 }
@@ -140,27 +209,34 @@ input[type="submit"] {
140 209
 	margin: 0;
141 210
 }
142 211
 .post-date {
143
-	font-size: 80%;
144
-	color: #888;
145 212
 	margin-top: 0.5em;
146 213
 }
147
-
148
-@media(prefers-color-scheme: dark) {
149
-	:root, textarea, input, select {
150
-		background-color: #222;
151
-		color: white;
152
-	}
153
-	.top-nav {
154
-		background-color: #444;
155
-	}
156
-	.menu-container ul {
157
-		border: 1px solid black;
158
-		background-color: #444;
159
-	}
160
-	.menu-container a + a li {
161
-		border-top: 1px solid #000;
162
-	}
214
+.post-footer {
215
+	position: relative;
216
+}
217
+.post-actions {
218
+	position: absolute;
219
+	top: 0;
220
+	inset-inline-end: 0;
221
+}
222
+.post-actions summary {
223
+	text-align: end;
224
+}
225
+.post-actions summary > span:first-child {
226
+	padding-top: 0;
227
+	padding-bottom: 0;
163 228
 }
229
+details.menu li.post-action-delete a {
230
+	color: var(--destructive);
231
+}
232
+details.menu li.post-action-delete a:hover {
233
+	background-color: var(--destructive);
234
+	color: var(--destructive-contrast);
235
+}
236
+
237
+
238
+/* Mobile */
239
+
164 240
 @media screen and (max-width: 450px) {
165 241
 	:root {
166 242
 		font-size: 16pt;

BIN
journal.db Voir le fichier


+ 5
- 4
source/create-tables.sql Voir le fichier

@@ -23,6 +23,7 @@ DROP TABLE IF EXISTS posts;
23 23
 CREATE TABLE posts (
24 24
 	body TEXT,                    -- main text of the post
25 25
 	created INTEGER,              -- timestamp when the post was created
26
+	updated INTEGER,              -- timestamp when the post was last edited
26 27
 	author_id INTEGER NOT NULL,   -- user_id of author
27 28
 	FOREIGN KEY (author_id) REFERENCES users (user_id)
28 29
 );
@@ -30,10 +31,10 @@ CREATE INDEX IF NOT EXISTS idx_posts_created ON posts (created DESC);
30 31
 
31 32
 DROP TABLE IF EXISTS sessions;
32 33
 CREATE TABLE sessions (
33
-	token TEXT UNIQUE NOT NULL,
34
-	user_id INTEGER NOT NULL,
35
-	created INTEGER,
36
-	updated INTEGER,
34
+	token TEXT UNIQUE NOT NULL,  -- random hex string
35
+	user_id INTEGER NOT NULL,    -- owner of the token
36
+	created INTEGER,             -- timestamp when the session was created
37
+	updated INTEGER,             -- timestamp when the session was last used
37 38
 	FOREIGN KEY (user_id) REFERENCES users (user_id)
38 39
 );
39 40
 CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token ON sessions (token);

Chargement…
Annuler
Enregistrer