@withstudiocms/auth-kit 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ import { createCipheriv, createDecipheriv } from "node:crypto";
2
+ import { DynamicBuffer } from "@oslojs/binary";
3
+ import { decodeBase64 } from "@oslojs/encoding";
4
+ import { Effect } from "@withstudiocms/effect";
5
+ import { useDecryptionError, useEncryptionError } from "../errors.js";
6
+ const Encryption = (CMS_ENCRYPTION_KEY) => Effect.gen(function* () {
7
+ const getKey = useEncryptionError(() => decodeBase64(CMS_ENCRYPTION_KEY));
8
+ const _key = yield* getKey;
9
+ yield* useEncryptionError(() => {
10
+ if (_key.byteLength !== 16) {
11
+ throw new Error("CMS_ENCRYPTION_KEY must decode to 16 bytes for aes-128-gcm");
12
+ }
13
+ });
14
+ const _algorithm = "aes-128-gcm";
15
+ const encrypt = Effect.fn("@withstudiocms/AuthKit/modules/encryption.encrypt")(
16
+ (data) => useEncryptionError(() => {
17
+ const iv = new Uint8Array(16);
18
+ crypto.getRandomValues(iv);
19
+ const cipher = createCipheriv(_algorithm, _key, iv);
20
+ const encrypted = new DynamicBuffer(0);
21
+ encrypted.write(iv);
22
+ encrypted.write(cipher.update(data));
23
+ encrypted.write(cipher.final());
24
+ encrypted.write(cipher.getAuthTag());
25
+ return encrypted.bytes();
26
+ })
27
+ );
28
+ const encryptToString = Effect.fn("@withstudiocms/AuthKit/modules/encryption.encryptToString")(
29
+ function* (data) {
30
+ const encoded = yield* useEncryptionError(() => new TextEncoder().encode(data));
31
+ return yield* encrypt(encoded);
32
+ }
33
+ );
34
+ const decrypt = Effect.fn("@withstudiocms/AuthKit/modules/encryption.decrypt")(
35
+ (data) => useDecryptionError(() => {
36
+ const IV_LEN = 16;
37
+ const TAG_LEN = 16;
38
+ const MAX_LEN = IV_LEN + TAG_LEN;
39
+ if (data.byteLength < MAX_LEN) {
40
+ throw new Error("Invalid data");
41
+ }
42
+ const decipher = createDecipheriv(_algorithm, _key, data.slice(0, IV_LEN));
43
+ decipher.setAuthTag(data.slice(data.byteLength - TAG_LEN));
44
+ const decrypted = new DynamicBuffer(0);
45
+ decrypted.write(decipher.update(data.slice(IV_LEN, data.byteLength - TAG_LEN)));
46
+ decrypted.write(decipher.final());
47
+ return decrypted.bytes();
48
+ })
49
+ );
50
+ const decryptToString = Effect.fn("@withstudiocms/AuthKit/modules/encryption.decryptToString")(
51
+ function* (data) {
52
+ const decoded = yield* decrypt(data);
53
+ return yield* useDecryptionError(() => new TextDecoder().decode(decoded));
54
+ }
55
+ );
56
+ return {
57
+ encrypt,
58
+ encryptToString,
59
+ decrypt,
60
+ decryptToString
61
+ };
62
+ });
63
+ export {
64
+ Encryption
65
+ };
@@ -0,0 +1,4 @@
1
+ export { Encryption as _Encryption } from './encryption.js';
2
+ export { Password as _Password } from './password.js';
3
+ export { Session as _Session } from './session.js';
4
+ export { User as _User } from './user.js';
@@ -0,0 +1,10 @@
1
+ import { Encryption } from "./encryption.js";
2
+ import { Password } from "./password.js";
3
+ import { Session } from "./session.js";
4
+ import { User } from "./user.js";
5
+ export {
6
+ Encryption as _Encryption,
7
+ Password as _Password,
8
+ Session as _Session,
9
+ User as _User
10
+ };
@@ -0,0 +1,18 @@
1
+ import { Effect } from '@withstudiocms/effect';
2
+ import type { IScrypt } from '../types.js';
3
+ /**
4
+ * Provides password hashing, verification, and strength checking utilities using Scrypt.
5
+ *
6
+ * @param Scrypt - An effectful Scrypt implementation for password hashing.
7
+ * @returns An Effect that yields an object containing:
8
+ * - `hashPassword`: Hashes a plain text password with optional salt.
9
+ * - `verifyPasswordHash`: Verifies a password against a hashed value.
10
+ * - `verifyPasswordStrength`: Checks if a password meets strength requirements.
11
+ *
12
+ * @module @withstudiocms/AuthKit/modules/password
13
+ */
14
+ export declare const Password: (Scrypt: IScrypt) => Effect.Effect<{
15
+ readonly hashPassword: (password: string, _salt?: string | undefined) => Effect.Effect<string, import("@withstudiocms/effect/scrypt").ScryptError, never>;
16
+ readonly verifyPasswordHash: (hash: string, password: string) => Effect.Effect<boolean, import("@withstudiocms/effect/scrypt").ScryptError | import("../errors.js").PasswordError, never>;
17
+ readonly verifyPasswordStrength: (pass: string) => Effect.Effect<string | true, import("../errors.js").PasswordError | import("../errors.js").CheckIfUnsafeError | import("@effect/platform/HttpClientError").ResponseError, never>;
18
+ }, never, never>;
@@ -0,0 +1,53 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { Effect } from "@withstudiocms/effect";
3
+ import {
4
+ breakSecurePassword,
5
+ buildSecurePassword,
6
+ checkPwnedDB,
7
+ constantTimeEqual,
8
+ PASS_GEN1_0_PREFIX,
9
+ verifyPasswordLength,
10
+ verifySafe
11
+ } from "../utils/password.js";
12
+ const Password = (Scrypt) => Effect.gen(function* () {
13
+ const scrypt = yield* Scrypt;
14
+ const hashPassword = Effect.fn("@withstudiocms/AuthKit/modules/password.hashPassword")(
15
+ function* (password, _salt) {
16
+ const salt = _salt || randomBytes(16).toString("hex");
17
+ const hash = yield* scrypt.run(password + salt);
18
+ return yield* buildSecurePassword({
19
+ generation: PASS_GEN1_0_PREFIX,
20
+ salt,
21
+ hash: hash.toString("hex")
22
+ });
23
+ }
24
+ );
25
+ const verifyPasswordHash = Effect.fn(
26
+ "@withstudiocms/AuthKit/modules/password.verifyPasswordHash"
27
+ )(function* (hash, password) {
28
+ const { salt } = yield* breakSecurePassword(hash);
29
+ const newHash = yield* hashPassword(password, salt);
30
+ return constantTimeEqual(hash, newHash);
31
+ });
32
+ const verifyPasswordStrength = Effect.fn(
33
+ "@withstudiocms/AuthKit/modules/password.verifyPasswordStrength"
34
+ )(function* (pass) {
35
+ const [lengthCheck, unsafeCheck, pwnedCheck] = yield* Effect.all([
36
+ verifyPasswordLength(pass),
37
+ verifySafe(pass),
38
+ checkPwnedDB(pass)
39
+ ]);
40
+ if (lengthCheck) return lengthCheck;
41
+ if (unsafeCheck) return unsafeCheck;
42
+ if (pwnedCheck) return pwnedCheck;
43
+ return true;
44
+ });
45
+ return {
46
+ hashPassword,
47
+ verifyPasswordHash,
48
+ verifyPasswordStrength
49
+ };
50
+ });
51
+ export {
52
+ Password
53
+ };
@@ -0,0 +1,34 @@
1
+ import { Effect } from '@withstudiocms/effect';
2
+ import type { APIContext, AstroGlobal } from 'astro';
3
+ import { SessionError } from '../errors.js';
4
+ import type { SessionConfig, SessionValidationResult } from '../types.js';
5
+ /**
6
+ * Creates a session management module with the provided configuration.
7
+ *
8
+ * This factory function returns an object containing various session-related
9
+ * effectful operations, such as generating session tokens, creating and validating
10
+ * sessions, managing session cookies, and handling OAuth session tokens.
11
+ *
12
+ * @param config - The session configuration object. This is merged with the default session configuration.
13
+ * @returns An Effect that yields an object with session management methods:
14
+ * - `generateSessionToken`: Generates a secure random session token.
15
+ * - `createSession`: Creates a new session for a user.
16
+ * - `validateSessionToken`: Validates a session token, extends expiration if needed, and deletes expired sessions.
17
+ * - `invalidateSession`: Deletes a session by its ID.
18
+ * - `setSessionTokenCookie`: Sets a session token cookie in the provided context.
19
+ * - `deleteSessionTokenCookie`: Deletes the session token cookie in the provided context.
20
+ * - `setOAuthSessionTokenCookie`: Sets an OAuth session token cookie in the provided context.
21
+ * - `createUserSession`: Creates a new user session and sets the session token cookie.
22
+ *
23
+ * @throws {SessionError} If required session tools are not provided in the configuration.
24
+ */
25
+ export declare const Session: (config: SessionConfig) => Effect.Effect<{
26
+ readonly generateSessionToken: () => Effect.Effect.AsEffect<Effect.Effect<string, SessionError, never>>;
27
+ readonly createSession: (token: string, userId: string) => Effect.Effect<import("../types.js").UserSession, SessionError, never>;
28
+ readonly validateSessionToken: (token: string) => Effect.Effect<SessionValidationResult, SessionError, never>;
29
+ readonly invalidateSession: (sessionId: string) => Effect.Effect.AsEffect<Effect.Effect<void, SessionError, never>>;
30
+ readonly setSessionTokenCookie: (context: APIContext<Record<string, any>, Record<string, string | undefined>> | AstroGlobal<Record<string, any>, import("astro/runtime/server/index.js").AstroComponentFactory, Record<string, string | undefined>>, token: string, expiresAt: Date, secure?: boolean | undefined) => Effect.Effect.AsEffect<Effect.Effect<void, SessionError, never>>;
31
+ readonly deleteSessionTokenCookie: (context: APIContext<Record<string, any>, Record<string, string | undefined>> | AstroGlobal<Record<string, any>, import("astro/runtime/server/index.js").AstroComponentFactory, Record<string, string | undefined>>, secure?: boolean | undefined) => Effect.Effect.AsEffect<Effect.Effect<void, SessionError, never>>;
32
+ readonly setOAuthSessionTokenCookie: (context: APIContext<Record<string, any>, Record<string, string | undefined>> | AstroGlobal<Record<string, any>, import("astro/runtime/server/index.js").AstroComponentFactory, Record<string, string | undefined>>, key: string, value: string, secure?: boolean | undefined) => Effect.Effect.AsEffect<Effect.Effect<void, SessionError, never>>;
33
+ readonly createUserSession: (userId: string, context: APIContext<Record<string, any>, Record<string, string | undefined>> | AstroGlobal<Record<string, any>, import("astro/runtime/server/index.js").AstroComponentFactory, Record<string, string | undefined>>, secure?: boolean | undefined) => Effect.Effect<void, SessionError, never>;
34
+ }, SessionError, never>;
@@ -0,0 +1,152 @@
1
+ import { encodeBase32LowerCaseNoPadding } from "@oslojs/encoding";
2
+ import { Effect } from "@withstudiocms/effect";
3
+ import { SessionError, useSessionError, useSessionErrorPromise } from "../errors.js";
4
+ import { defaultSessionConfig, makeExpirationDate, makeSessionId } from "../utils/session.js";
5
+ const Session = (config) => Effect.gen(function* () {
6
+ const { expTime, cookieName, sessionTools } = {
7
+ ...defaultSessionConfig,
8
+ ...config
9
+ };
10
+ if (!sessionTools) {
11
+ return yield* Effect.fail(
12
+ new SessionError({ cause: "Session tools must be provided in the configuration" })
13
+ );
14
+ }
15
+ if (!Number.isFinite(expTime) || expTime <= 0) {
16
+ return yield* Effect.fail(
17
+ new SessionError({
18
+ cause: "Invalid session config: expTime must be a positive number (ms)"
19
+ })
20
+ );
21
+ }
22
+ if (typeof cookieName !== "string" || cookieName.trim().length === 0) {
23
+ return yield* Effect.fail(
24
+ new SessionError({ cause: "Invalid session config: cookieName must be a non-empty string" })
25
+ );
26
+ }
27
+ const expTimeHalf = expTime / 2;
28
+ const defaultSecure = process.env.NODE_ENV === "production";
29
+ const generateSessionToken = Effect.fn(
30
+ "@withstudiocms/AuthKit/modules/session.generateSessionToken"
31
+ )(
32
+ () => useSessionError(() => {
33
+ const data = new Uint8Array(20);
34
+ const random = crypto.getRandomValues(data);
35
+ const returnable = encodeBase32LowerCaseNoPadding(random);
36
+ return returnable;
37
+ })
38
+ );
39
+ const createSession = Effect.fn("@withstudiocms/AuthKit/modules/session.createSession")(
40
+ function* (token, userId) {
41
+ const [sessionId, expirationDate] = yield* Effect.all([
42
+ makeSessionId(token),
43
+ makeExpirationDate(expTime)
44
+ ]);
45
+ return yield* useSessionErrorPromise(
46
+ () => sessionTools.createSession({ expiresAt: expirationDate, id: sessionId, userId })
47
+ );
48
+ }
49
+ );
50
+ const validateSessionToken = Effect.fn(
51
+ "@withstudiocms/AuthKit/modules/session.validateSessionToken"
52
+ )(function* (token) {
53
+ const sessionId = yield* makeSessionId(token);
54
+ const nullSession = {
55
+ session: null,
56
+ user: null
57
+ };
58
+ const result = yield* useSessionErrorPromise(
59
+ () => sessionTools.sessionAndUserData(sessionId)
60
+ );
61
+ if (!result.length) {
62
+ return nullSession;
63
+ }
64
+ const userSession = result[0];
65
+ if (!userSession) {
66
+ return nullSession;
67
+ }
68
+ const { user, session } = userSession;
69
+ const now = Date.now();
70
+ const expiresAtMs = session.expiresAt instanceof Date ? session.expiresAt.getTime() : new Date(session.expiresAt).getTime();
71
+ if (!Number.isFinite(expiresAtMs)) {
72
+ yield* useSessionErrorPromise(() => sessionTools.deleteSession(session.id));
73
+ return nullSession;
74
+ }
75
+ if (now >= expiresAtMs) {
76
+ yield* useSessionErrorPromise(() => sessionTools.deleteSession(session.id));
77
+ return nullSession;
78
+ }
79
+ let sessionOut = session;
80
+ if (now >= expiresAtMs - expTimeHalf) {
81
+ const expiresAt = new Date(now + expTime);
82
+ yield* useSessionErrorPromise(
83
+ () => sessionTools.updateSession(session.id, { ...session, expiresAt })
84
+ );
85
+ sessionOut = { ...session, expiresAt };
86
+ }
87
+ return { session: sessionOut, user };
88
+ });
89
+ const invalidateSession = Effect.fn("@withstudiocms/AuthKit/modules/session.invalidateSession")(
90
+ (sessionId) => useSessionErrorPromise(() => sessionTools.deleteSession(sessionId))
91
+ );
92
+ const setSessionTokenCookie = Effect.fn(
93
+ "@withstudiocms/AuthKit/modules/session.setSessionTokenCookie"
94
+ )(
95
+ (context, token, expiresAt, secure) => useSessionError(
96
+ () => context.cookies.set(cookieName, token, {
97
+ httpOnly: true,
98
+ sameSite: "lax",
99
+ secure: secure ?? defaultSecure,
100
+ expires: expiresAt,
101
+ path: "/"
102
+ })
103
+ )
104
+ );
105
+ const deleteSessionTokenCookie = Effect.fn(
106
+ "@withstudiocms/AuthKit/modules/session.deleteSessionTokenCookie"
107
+ )(
108
+ (context, secure) => useSessionError(
109
+ () => context.cookies.set(cookieName, "", {
110
+ httpOnly: true,
111
+ sameSite: "lax",
112
+ secure: secure ?? defaultSecure,
113
+ maxAge: 0,
114
+ path: "/"
115
+ })
116
+ )
117
+ );
118
+ const setOAuthSessionTokenCookie = Effect.fn(
119
+ "@withstudiocms/AuthKit/modules/session.setOAuthSessionTokenCookie"
120
+ )(
121
+ (context, key, value, secure) => useSessionError(
122
+ () => context.cookies.set(key, value, {
123
+ httpOnly: true,
124
+ sameSite: "lax",
125
+ secure: secure ?? defaultSecure,
126
+ maxAge: 60 * 10,
127
+ path: "/"
128
+ })
129
+ )
130
+ );
131
+ const createUserSession = Effect.fn("@withstudiocms/AuthKit/modules/session.createUserSession")(
132
+ function* (userId, context, secure) {
133
+ const sessionToken = yield* generateSessionToken();
134
+ const expirationDate = yield* makeExpirationDate(expTime);
135
+ yield* createSession(sessionToken, userId);
136
+ yield* setSessionTokenCookie(context, sessionToken, expirationDate, secure);
137
+ }
138
+ );
139
+ return {
140
+ generateSessionToken,
141
+ createSession,
142
+ validateSessionToken,
143
+ invalidateSession,
144
+ setSessionTokenCookie,
145
+ deleteSessionTokenCookie,
146
+ setOAuthSessionTokenCookie,
147
+ createUserSession
148
+ };
149
+ });
150
+ export {
151
+ Session
152
+ };
@@ -0,0 +1,85 @@
1
+ import { Effect } from '@withstudiocms/effect';
2
+ import type { APIContext, AstroGlobal } from 'astro';
3
+ import { UserError } from '../errors.js';
4
+ import type { CombinedUserData, UserConfig, UserData, UserSessionData } from '../types.js';
5
+ import { UserPermissionLevel } from '../types.js';
6
+ /**
7
+ * Factory function to create user-related operations for authentication and user management.
8
+ *
9
+ * This function initializes and returns a set of user management utilities, including
10
+ * username validation, avatar creation, user creation (local and OAuth), password management,
11
+ * user data retrieval, and permission checks. It requires configuration for password hashing,
12
+ * session management, and user tools.
13
+ *
14
+ * @param config - The configuration object for user management.
15
+ * @param config.Scrypt - The password hashing implementation.
16
+ * @param config.session - The session management configuration.
17
+ * @param config.userTools - Utilities for user data access, creation, and notification.
18
+ * @returns An Effect generator yielding an object with user management methods:
19
+ * - `verifyUsernameInput`: Validates a username against length, character, and safety rules.
20
+ * - `createUserAvatar`: Generates a Libravatar URL for a user's email.
21
+ * - `createLocalUser`: Creates a new local user with the provided details.
22
+ * - `createOAuthUser`: Creates a new user with OAuth credentials.
23
+ * - `updateUserPassword`: Updates a user's password.
24
+ * - `getUserPasswordHash`: Retrieves the password hash for a user.
25
+ * - `getUserFromEmail`: Retrieves a user by their email address.
26
+ * - `getUserData`: Retrieves user session data from the provided context.
27
+ * - `getUserPermissionLevel`: Gets the user's permission level as an enum.
28
+ * - `isUserAllowed`: Checks if a user has the required permission level.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const userModule = User({ Scrypt, session, userTools });
33
+ * ```
34
+ */
35
+ export declare const User: ({ Scrypt, session, userTools }: UserConfig) => Effect.Effect<{
36
+ readonly verifyUsernameInput: (username: string) => Effect.Effect<string | true, import("../errors.js").CheckIfUnsafeError | UserError, never>;
37
+ readonly createUserAvatar: (email: string) => Effect.Effect.AsEffect<Effect.Effect<string, UserError, never>>;
38
+ readonly createLocalUser: (name: string, username: string, email: string, password: string) => Effect.Effect<{
39
+ name: string;
40
+ username: string;
41
+ id: string;
42
+ url: string | null;
43
+ email: string | null;
44
+ avatar: string | null;
45
+ password: string | null;
46
+ updatedAt: Date | null;
47
+ createdAt: Date | null;
48
+ emailVerified: boolean;
49
+ notifications: string | null;
50
+ }, import("@withstudiocms/effect/scrypt").ScryptError | UserError, never>;
51
+ readonly createOAuthUser: (data: UserData, oAuthFields: {
52
+ provider: string;
53
+ providerUserId: string;
54
+ }) => Effect.Effect<{
55
+ name: string;
56
+ username: string;
57
+ id: string;
58
+ url: string | null;
59
+ email: string | null;
60
+ avatar: string | null;
61
+ password: string | null;
62
+ updatedAt: Date | null;
63
+ createdAt: Date | null;
64
+ emailVerified: boolean;
65
+ notifications: string | null;
66
+ }, UserError, never>;
67
+ readonly updateUserPassword: (userId: string, password: string) => Effect.Effect<{
68
+ name: string;
69
+ username: string;
70
+ id: string;
71
+ url: string | null;
72
+ email: string | null;
73
+ avatar: string | null;
74
+ password: string | null;
75
+ updatedAt: Date | null;
76
+ createdAt: Date | null;
77
+ emailVerified: boolean;
78
+ notifications: string | null;
79
+ }, import("@withstudiocms/effect/scrypt").ScryptError | UserError, never>;
80
+ readonly getUserPasswordHash: (userId: string) => Effect.Effect<string, UserError, never>;
81
+ readonly getUserFromEmail: (email: string) => Effect.Effect.AsEffect<Effect.Effect<CombinedUserData | null | undefined, UserError, never>>;
82
+ readonly getUserData: (context: APIContext<Record<string, any>, Record<string, string | undefined>> | AstroGlobal<Record<string, any>, import("astro/runtime/server/index.js").AstroComponentFactory, Record<string, string | undefined>>) => Effect.Effect<UserSessionData, import("../errors.js").SessionError | UserError, never>;
83
+ readonly getUserPermissionLevel: (userData: UserSessionData | CombinedUserData | null) => Effect.Effect<UserPermissionLevel, UserError, never>;
84
+ readonly isUserAllowed: (userData: UserSessionData | CombinedUserData | null, requiredPerms: "owner" | "admin" | "editor" | "visitor" | "unknown") => Effect.Effect<boolean, UserError, never>;
85
+ }, import("../errors.js").SessionError | UserError, never>;
@@ -0,0 +1,169 @@
1
+ import { Effect } from "@withstudiocms/effect";
2
+ import { UserError, useSessionError, useUserError, useUserErrorPromise } from "../errors.js";
3
+ import { UserPermissionLevel } from "../types.js";
4
+ import libravatar from "../utils/libravatar.js";
5
+ import {
6
+ getDefaultUserSession,
7
+ getLevel,
8
+ parseRequiredPerms,
9
+ verifyUsernameCharacters,
10
+ verifyUsernameLength,
11
+ verifyUsernameSafe
12
+ } from "../utils/user.js";
13
+ import { Password as _Password } from "./password.js";
14
+ import { Session as _Session } from "./session.js";
15
+ const User = ({ Scrypt, session, userTools }) => Effect.gen(function* () {
16
+ const [Password, Session] = yield* Effect.all([_Password(Scrypt), _Session(session)]);
17
+ if (!userTools) {
18
+ return yield* Effect.fail(new UserError({ cause: "User tools are not available" }));
19
+ }
20
+ const notifier = userTools.notifier;
21
+ const verifyUsernameInput = Effect.fn(
22
+ "@withstudiocms/AuthKit/modules/user.verifyUsernameInput"
23
+ )(function* (username) {
24
+ const [testLength, usernameChars, safeCheck] = yield* Effect.all([
25
+ verifyUsernameLength(username),
26
+ verifyUsernameCharacters(username),
27
+ verifyUsernameSafe(username)
28
+ ]);
29
+ if (testLength) return testLength;
30
+ if (usernameChars) return usernameChars;
31
+ if (safeCheck) return safeCheck;
32
+ return true;
33
+ });
34
+ const createUserAvatar = Effect.fn("@withstudiocms/AuthKit/modules/user.createUserAvatar")(
35
+ (email) => useUserErrorPromise(
36
+ async () => libravatar.getAvatarUrl({ email, https: true, size: 400, default: "retro" })
37
+ )
38
+ );
39
+ const createLocalUser = Effect.fn("@withstudiocms/AuthKit/modules/user.createLocalUser")(
40
+ function* (name, username, email, password) {
41
+ const [passwordHash, avatar, id] = yield* Effect.all([
42
+ Password.hashPassword(password),
43
+ createUserAvatar(email),
44
+ useUserError(() => userTools.idGenerator())
45
+ ]);
46
+ const createdAt = /* @__PURE__ */ new Date();
47
+ const createdUser = yield* useUserErrorPromise(
48
+ () => userTools.createLocalUser({
49
+ id,
50
+ name,
51
+ username,
52
+ email,
53
+ createdAt,
54
+ avatar,
55
+ updatedAt: createdAt,
56
+ password: passwordHash,
57
+ emailVerified: false,
58
+ notifications: null,
59
+ url: null
60
+ })
61
+ );
62
+ if (notifier) {
63
+ yield* useUserErrorPromise(() => notifier.admin("new_user", createdUser.username));
64
+ }
65
+ return createdUser;
66
+ }
67
+ );
68
+ const createOAuthUser = Effect.fn("@withstudiocms/AuthKit/modules/user.createOAuthUser")(
69
+ function* (data, oAuthFields) {
70
+ const createdUser = yield* useUserErrorPromise(() => userTools.createLocalUser(data));
71
+ yield* useUserErrorPromise(
72
+ () => userTools.createOAuthUser({
73
+ userId: createdUser.id,
74
+ ...oAuthFields
75
+ })
76
+ );
77
+ if (notifier) {
78
+ yield* useUserErrorPromise(() => notifier.admin("new_user", createdUser.username));
79
+ }
80
+ return createdUser;
81
+ }
82
+ );
83
+ const updateUserPassword = Effect.fn("@withstudiocms/AuthKit/modules/user.updateUserPassword")(
84
+ function* (userId, password) {
85
+ const newHash = yield* Password.hashPassword(password);
86
+ return yield* useUserErrorPromise(
87
+ () => userTools.updateLocalUser(userId, { password: newHash })
88
+ );
89
+ }
90
+ );
91
+ const getUserPasswordHash = Effect.fn(
92
+ "@withstudiocms/AuthKit/modules/user.getUserPasswordHash"
93
+ )(function* (userId) {
94
+ const user = yield* useUserErrorPromise(() => userTools.getUserById(userId));
95
+ if (!user) return yield* Effect.fail(new UserError({ cause: "User not found" }));
96
+ if (!user.password)
97
+ return yield* Effect.fail(new UserError({ cause: "User has no password" }));
98
+ return user.password;
99
+ });
100
+ const getUserFromEmail = Effect.fn("@withstudiocms/AuthKit/modules/user.getUserFromEmail")(
101
+ (email) => useUserErrorPromise(() => userTools.getUserByEmail(email))
102
+ );
103
+ const getUserData = Effect.fn("@withstudiocms/AuthKit/modules/user.getUserData")(function* (context) {
104
+ const sessionToken = yield* useSessionError(
105
+ () => context.cookies.get(session.cookieName)?.value
106
+ );
107
+ if (!sessionToken) {
108
+ return yield* getDefaultUserSession();
109
+ }
110
+ const { session: ses, user } = yield* Session.validateSessionToken(sessionToken);
111
+ if (!ses || !user) {
112
+ yield* Session.deleteSessionTokenCookie(context);
113
+ return yield* getDefaultUserSession();
114
+ }
115
+ const rankToPermissionLevel = {
116
+ owner: "owner",
117
+ admin: "admin",
118
+ editor: "editor",
119
+ visitor: "visitor"
120
+ };
121
+ const result = yield* useUserErrorPromise(() => userTools.getCurrentPermissions(user.id));
122
+ const permissionLevel = result && rankToPermissionLevel[result.rank] || "unknown";
123
+ const out = {
124
+ isLoggedIn: true,
125
+ user,
126
+ permissionLevel
127
+ };
128
+ return out;
129
+ });
130
+ const getUserPermissionLevel = Effect.fn(
131
+ "@withstudiocms/AuthKit/modules/user.getUserPermissionLevel"
132
+ )(function* (userData) {
133
+ const level = yield* getLevel(userData);
134
+ switch (level) {
135
+ case "owner":
136
+ return UserPermissionLevel.owner;
137
+ case "admin":
138
+ return UserPermissionLevel.admin;
139
+ case "editor":
140
+ return UserPermissionLevel.editor;
141
+ case "visitor":
142
+ return UserPermissionLevel.visitor;
143
+ default:
144
+ return UserPermissionLevel.unknown;
145
+ }
146
+ });
147
+ const isUserAllowed = Effect.fn("@withstudiocms/AuthKit/modules/user.isUserAllowed")(function* (userData, requiredPerms) {
148
+ const [userLevel, neededLevel] = yield* Effect.all([
149
+ getUserPermissionLevel(userData),
150
+ parseRequiredPerms(requiredPerms)
151
+ ]);
152
+ return userLevel >= neededLevel;
153
+ });
154
+ return {
155
+ verifyUsernameInput,
156
+ createUserAvatar,
157
+ createLocalUser,
158
+ createOAuthUser,
159
+ updateUserPassword,
160
+ getUserPasswordHash,
161
+ getUserFromEmail,
162
+ getUserData,
163
+ getUserPermissionLevel,
164
+ isUserAllowed
165
+ };
166
+ });
167
+ export {
168
+ User
169
+ };