Skip to content
This repository was archived by the owner on Sep 17, 2024. It is now read-only.

Commit 6c094f1

Browse files
committed
eat: Create the auth_username_pasword module
1 parent 91cb526 commit 6c094f1

File tree

9 files changed

+245
-0
lines changed

9 files changed

+245
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface Config {
2+
enable: boolean;
3+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "Auth Username Password",
3+
"description": "Authenticate users with a username/password combination.",
4+
"icon": "key",
5+
"tags": [
6+
"core",
7+
"auth",
8+
"user"
9+
],
10+
"authors": [
11+
"rivet-gg",
12+
"Blckbrry-Pi"
13+
],
14+
"status": "stable",
15+
"dependencies": {
16+
"users": {},
17+
"rate_limit": {},
18+
"user_passwords": {}
19+
},
20+
"scripts": {
21+
"sign_up": {
22+
"name": "Sign Up with Username and Password",
23+
"description": "Sign up a new user with a username and password.",
24+
"public": true
25+
},
26+
"sign_in": {
27+
"name": "Sign In with Username and Password",
28+
"description": "Sign in a user with a username and password.",
29+
"public": true
30+
}
31+
},
32+
"errors": {
33+
"provider_disabled": {
34+
"name": "Provider Disabled"
35+
},
36+
"username_or_password_invalid": {
37+
"name": "Username or Password Invalid"
38+
},
39+
"user_already_exists": {
40+
"name": "User Already Exists"
41+
}
42+
}
43+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { TokenWithSecret } from "../../tokens/utils/types.ts";
2+
import { RuntimeError } from "../module.gen.ts";
3+
import { ScriptContext } from "../module.gen.ts";
4+
5+
export interface Request {
6+
username: string;
7+
password: string;
8+
}
9+
10+
export interface Response {
11+
token: TokenWithSecret;
12+
}
13+
14+
export async function run(
15+
ctx: ScriptContext,
16+
req: Request,
17+
): Promise<Response> {
18+
await ctx.modules.rateLimit.throttlePublic({});
19+
20+
if (!ctx.config.enable) throw new RuntimeError("provider_disabled");
21+
22+
const { users: [user] } = await ctx.modules.users.fetchByUname({
23+
usernames: [req.username],
24+
});
25+
26+
if (!user) throw new RuntimeError("invalid_username_or_password");
27+
28+
const { id } = user;
29+
30+
try {
31+
await ctx.modules.userPasswords.verify({
32+
userId: id,
33+
password: req.password,
34+
});
35+
} catch (e) {
36+
if (e instanceof RuntimeError) {
37+
throw new RuntimeError("invalid_username_or_password");
38+
} else {
39+
throw e;
40+
}
41+
}
42+
43+
return await ctx.modules.users.createToken({ userId: id });
44+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { TokenWithSecret } from "../../tokens/utils/types.ts";
2+
import { RuntimeError } from "../module.gen.ts";
3+
import { ScriptContext } from "../module.gen.ts";
4+
5+
export interface Request {
6+
username: string;
7+
password: string;
8+
}
9+
10+
export interface Response {
11+
token: TokenWithSecret;
12+
}
13+
14+
export async function run(
15+
ctx: ScriptContext,
16+
req: Request,
17+
): Promise<Response> {
18+
await ctx.modules.rateLimit.throttlePublic({});
19+
20+
if (!ctx.config.enable) throw new RuntimeError("provider_disabled");
21+
22+
const { users: [existing] } = await ctx.modules.users.fetchByUname({
23+
usernames: [req.username],
24+
});
25+
26+
if (existing) throw new RuntimeError("user_already_exists");
27+
28+
const { user } = await ctx.modules.users.create({
29+
username: req.username,
30+
});
31+
32+
await ctx.modules.userPasswords.add({
33+
userId: user.id,
34+
password: req.password,
35+
});
36+
37+
// Sign in the user
38+
return await ctx.modules.authUsernamePassword.signIn(req);
39+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { test, TestContext } from "../module.gen.ts";
2+
import { assertExists, assertEquals, assertRejects } from "https://deno.land/std@0.217.0/assert/mod.ts";
3+
import { faker } from "https://deno.land/x/deno_faker@v1.0.3/mod.ts";
4+
import { RuntimeError } from "../module.gen.ts";
5+
6+
test("test_sign_up", async (ctx: TestContext) => {
7+
const username = faker.internet.userName();
8+
const password = faker.internet.password();
9+
10+
// MARK: Should be able to sign up and things should match
11+
const { token } = await ctx.modules.authUsernamePassword.signUp({
12+
username,
13+
password,
14+
});
15+
16+
const { userId } = await ctx.modules.users.authenticateToken({
17+
userToken: token.token,
18+
});
19+
20+
21+
const { users: [user] } = await ctx.modules.users.fetchByUname({
22+
usernames: [username],
23+
});
24+
25+
assertExists(user);
26+
assertEquals(user.id, userId);
27+
28+
// MARK: Should not be able to sign up with the same username
29+
const error = await assertRejects(async () => {
30+
await ctx.modules.authUsernamePassword.signUp({
31+
username,
32+
password,
33+
});
34+
}, RuntimeError);
35+
assertEquals(error.code, "user_already_exists");
36+
});
37+
38+
test("test_sign_in", async (ctx: TestContext) => {
39+
const username = faker.internet.userName();
40+
const password = faker.internet.password();
41+
42+
// MARK: Sign up
43+
await ctx.modules.authUsernamePassword.signUp({
44+
username,
45+
password,
46+
});
47+
48+
// MARK: Can sign in with correct credentials
49+
const { token } = await ctx.modules.authUsernamePassword.signIn({
50+
username,
51+
password,
52+
});
53+
54+
const { userId } = await ctx.modules.users.authenticateToken({
55+
userToken: token.token,
56+
});
57+
58+
const { users: [user] } = await ctx.modules.users.fetchByUname({
59+
usernames: [username],
60+
});
61+
62+
assertExists(user);
63+
assertEquals(user.id, userId);
64+
assertEquals(user.username, username);
65+
66+
// MARK: Can't sign in with wrong password
67+
const error = await assertRejects(async () => {
68+
await ctx.modules.authUsernamePassword.signIn({
69+
username,
70+
password: faker.internet.password(),
71+
});
72+
}, RuntimeError);
73+
assertEquals(error.code, "invalid_username_or_password");
74+
});
75+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface Verification {
2+
id: string;
3+
}
4+
5+
export interface Session {
6+
token: string;
7+
expireAt: string;
8+
}

modules/users/module.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"name": "Fetch User",
2121
"public": true
2222
},
23+
"fetch_by_uname": {
24+
"name": "Fetch User by Username"
25+
},
2326
"create": {
2427
"name": "Create User"
2528
},
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ScriptContext } from "../module.gen.ts";
2+
import { User } from "../utils/types.ts";
3+
4+
export interface Request {
5+
usernames: string[];
6+
}
7+
8+
export interface Response {
9+
users: User[];
10+
}
11+
12+
export async function run(
13+
ctx: ScriptContext,
14+
req: Request,
15+
): Promise<Response> {
16+
await ctx.modules.rateLimit.throttlePublic({});
17+
18+
const users = await ctx.db.user.findMany({
19+
where: { username: { in: req.usernames } },
20+
orderBy: { username: "desc" },
21+
});
22+
23+
return { users };
24+
}

tests/basic/backend.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@
3838
}
3939
}
4040
},
41+
"auth_username_password": {
42+
"registry": "local",
43+
"config": {
44+
"enable": true
45+
}
46+
},
4147
"identities": {
4248
"registry": "local"
4349
},

0 commit comments

Comments
 (0)