Skip to content

Commit 51ca7c5

Browse files
committed
feat: add initial user endpoints
1 parent 67004a6 commit 51ca7c5

5 files changed

Lines changed: 120 additions & 6 deletions

File tree

deno.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"imports": {
1919
"@/": "./",
2020
"$fresh/": "https://deno.land/x/fresh@1.7.3/",
21+
"@felix/bcrypt": "jsr:@felix/bcrypt@^1.0.5",
22+
"@std/ulid": "jsr:@std/ulid@^1.0.0",
2123
"@surrealdb/surrealdb": "jsr:@surrealdb/surrealdb@^1.3.2",
2224
"preact": "https://esm.sh/preact@10.22.0",
2325
"preact/": "https://esm.sh/preact@10.22.0/",
@@ -26,7 +28,8 @@
2628
"tailwindcss": "npm:tailwindcss@3.4.1",
2729
"tailwindcss/": "npm:/tailwindcss@3.4.1/",
2830
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
29-
"$std/": "https://deno.land/std@0.216.0/"
31+
"$std/": "https://deno.land/std@0.216.0/",
32+
"zod": "https://deno.land/x/zod@v3.24.3/mod.ts"
3033
},
3134
"compilerOptions": {
3235
"jsx": "react-jsx",

example.rest

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# See documentation at:
2+
# - https://developer.fluro.io
3+
# - https://marketplace.visualstudio.com/items?itemName=humao.rest-client
4+
5+
6+
@baseUrl = http://localhost:8000
7+
@contentType = application/json
8+
9+
###
10+
# Get all Users
11+
GET {{baseUrl}}/api/users
12+
content-type: {{contentType}}
13+
14+
###
15+
# Create new User
16+
POST {{baseUrl}}/api/users
17+
content-type: {{contentType}}
18+
19+
{
20+
"email": "example@gmail.com",
21+
"password": "sample",
22+
"gender": "male"
23+
}

routes/api/users.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,49 @@
11
import { Handlers } from "$fresh/server.ts";
22
import { db, initDB } from "@/lib/surreal.ts";
3+
import { RecordId } from "@surrealdb/surrealdb";
4+
import { ulid } from "@std/ulid";
5+
import { hash } from "@felix/bcrypt";
6+
import { PublicUser, PublicUserSchema } from "@/types.ts";
7+
8+
await initDB();
39

410
export const handler: Handlers<null> = {
511
async GET(_req, _ctx) {
6-
await initDB();
12+
const results: PublicUser[][] = await db.query(`
13+
SELECT id, name FROM user;
14+
`);
15+
return new Response(JSON.stringify(results[0], null, 2), {
16+
headers: { "Content-Type": "application/json" },
17+
});
18+
},
719

8-
const users = await db.select("user");
20+
async POST(req, _ctx) {
21+
const body = await req.json();
22+
const { email, password } = body;
923

10-
return new Response(JSON.stringify(users, null, 2), {
11-
headers: { "Content-Type": "application/json" },
24+
if (!email || !password) {
25+
return new Response(
26+
JSON.stringify({ error: "Email and password are required." }),
27+
{ status: 400, headers: { "Content-Type": "application/json" } },
28+
);
29+
}
30+
31+
const hashedPassword = await hash(password);
32+
const emailVerificationToken = ulid();
33+
34+
const user = await db.create<PublicUser>(new RecordId("user", ulid()), {
35+
email,
36+
password_hash: hashedPassword,
37+
email_verification_token: emailVerificationToken,
1238
});
39+
const parsed = PublicUserSchema.parse(user);
40+
41+
return new Response(
42+
JSON.stringify(parsed, null, 2),
43+
{
44+
headers: { "Content-Type": "application/json" },
45+
status: 201,
46+
},
47+
);
1348
},
1449
};

schema.surrealql

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,45 @@
11
OPTION IMPORT;
22

3+
-- Delete all existing tables
4+
REMOVE TABLE user;
5+
REMOVE TABLE skill;
6+
REMOVE TABLE session;
7+
REMOVE TABLE has_skill;
8+
REMOVE TABLE has_session;
9+
10+
-- USERS
311
DEFINE TABLE user TYPE NORMAL SCHEMAFULL PERMISSIONS NONE;
4-
DEFINE FIELD name ON user TYPE string PERMISSIONS FULL;
12+
DEFINE FIELD id ON user TYPE string ASSERT $value != NONE PERMISSIONS NONE;
13+
DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value) PERMISSIONS FULL;
14+
DEFINE FIELD email_verified ON user TYPE bool DEFAULT false PERMISSIONS FULL;
15+
DEFINE FIELD password_hash ON user TYPE string PERMISSIONS NONE;
16+
DEFINE FIELD name ON user TYPE option<string> DEFAULT NONE PERMISSIONS FULL;
17+
DEFINE FIELD gender ON user TYPE option<string> DEFAULT NONE PERMISSIONS FULL;
18+
DEFINE FIELD picture ON user TYPE option<string> DEFAULT NONE PERMISSIONS FULL;
19+
DEFINE FIELD email_verification_token ON user TYPE string PERMISSIONS NONE;
20+
DEFINE FIELD password_reset_token ON user TYPE option<string> DEFAULT NONE PERMISSIONS NONE;
21+
DEFINE FIELD password_reset_expiry ON user TYPE option<datetime> DEFAULT NONE PERMISSIONS NONE;
22+
DEFINE FIELD oauth_providers ON user TYPE option<array> DEFAULT [] PERMISSIONS FULL;
523
DEFINE FIELD date_created ON user TYPE datetime DEFAULT time::now() PERMISSIONS FULL;
624
DEFINE FIELD date_updated ON user TYPE datetime DEFAULT time::now() PERMISSIONS FULL;
25+
26+
-- SKILLS
27+
DEFINE TABLE skill TYPE NORMAL SCHEMAFULL PERMISSIONS NONE;
28+
DEFINE FIELD name ON skill TYPE string ASSERT $value != NONE PERMISSIONS FULL;
29+
DEFINE FIELD category ON skill TYPE string PERMISSIONS FULL;
30+
DEFINE FIELD description ON skill TYPE string PERMISSIONS FULL;
31+
32+
-- SESSIONS
33+
DEFINE TABLE session TYPE NORMAL SCHEMAFULL PERMISSIONS NONE;
34+
DEFINE FIELD token ON session TYPE string ASSERT $value != NONE PERMISSIONS NONE;
35+
DEFINE FIELD issued_at ON session TYPE datetime DEFAULT time::now() PERMISSIONS FULL;
36+
DEFINE FIELD expires_at ON session TYPE datetime PERMISSIONS FULL;
37+
DEFINE FIELD ip_address ON session TYPE string PERMISSIONS FULL;
38+
DEFINE FIELD user_agent ON session TYPE string PERMISSIONS FULL;
39+
40+
-- EDGE: USER -> SKILL (HAS_SKILL)
41+
DEFINE TABLE has_skill SCHEMALESS PERMISSIONS NONE;
42+
DEFINE FIELD proficiency ON has_skill TYPE string;
43+
44+
-- EDGE: USER -> SESSION (HAS_SESSION)
45+
DEFINE TABLE has_session SCHEMALESS PERMISSIONS NONE;

types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { z } from "zod";
2+
import { RecordId } from "@surrealdb/surrealdb";
3+
4+
export const PublicUserSchema = z.object({
5+
id: z.instanceof(RecordId).optional(),
6+
email: z.string().email(),
7+
password_hash: z.string().optional(),
8+
email_verification_token: z.string().optional(),
9+
email_verified: z.boolean().optional(),
10+
name: z.string().optional(),
11+
date_created: z.date().optional(),
12+
});
13+
14+
export type PublicUser = z.infer<typeof PublicUserSchema>;

0 commit comments

Comments
 (0)