Skip to content

Commit

Permalink
user creation
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacharrisholt committed Aug 10, 2024
1 parent 3cf3554 commit bbad61c
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 13 deletions.
56 changes: 52 additions & 4 deletions src/pevensie/auth.gleam
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import argus
import gleam/dict
import gleam/dynamic.{type Decoder}
import gleam/option.{None, Some}
import pevensie/drivers.{
type AuthDriver, type Connected, type Disabled, type Disconnected,
type Encoder,
}
import pevensie/internal/auth
import pevensie/internal/pevensie.{type Pevensie}
import pevensie/internal/user.{type User as InternalUser}
import pevensie/internal/user.{
type User as InternalUser, type UserInsert as UserInsertInternal, UserInsert,
}

pub type User(user_metadata) =
InternalUser(user_metadata)

pub type UserInsert(user_metadata) =
UserInsertInternal(user_metadata)

pub type AuthConfig(driver, user_metadata, connected) =
auth.AuthConfig(driver, user_metadata, connected)

pub fn new_auth_config(
driver driver: AuthDriver(driver, user_metadata),
user_metadata_decoder user_metadata_decoder: Decoder(user_metadata),
user_metadata_encoder user_metadata_encoder: Encoder(user_metadata),
) -> AuthConfig(driver, user_metadata, Disconnected) {
auth.AuthConfig(driver, user_metadata_decoder)
auth.AuthConfig(driver, user_metadata_decoder, user_metadata_encoder)
}

pub fn disabled() -> AuthConfig(Nil, user_metadata, Disabled) {
Expand All @@ -27,7 +37,7 @@ pub fn get_user_by_id(
pevensie: Pevensie(user_metadata, auth_driver, Connected),
id: String,
) -> Result(User(user_metadata), Nil) {
let assert auth.AuthConfig(driver, user_metadata_decoder) =
let assert auth.AuthConfig(driver, user_metadata_decoder, ..) =
pevensie.auth_config

driver.get_user(driver.driver, "id", id, user_metadata_decoder)
Expand All @@ -37,8 +47,46 @@ pub fn get_user_by_email(
pevensie: Pevensie(user_metadata, auth_driver, Connected),
email: String,
) -> Result(User(user_metadata), Nil) {
let assert auth.AuthConfig(driver, user_metadata_decoder) =
let assert auth.AuthConfig(driver, user_metadata_decoder, ..) =
pevensie.auth_config

driver.get_user(driver.driver, "email", email, user_metadata_decoder)
}

fn hash_password(password: String) {
argus.hasher()
|> argus.hash(password, argus.gen_salt())
}

pub fn create_user_with_email(
pevensie: Pevensie(user_metadata, auth_driver, Connected),
email: String,
password: String,
user_metadata: user_metadata,
) -> Result(User(user_metadata), Nil) {
let assert auth.AuthConfig(
driver,
user_metadata_decoder,
user_metadata_encoder,
) = pevensie.auth_config

// TODO: Handle errors
let assert Ok(hashed_password) = hash_password(password)

driver.insert_user(
driver.driver,
UserInsert(
role: None,
email: email,
password_hash: Some(hashed_password.encoded_hash),
email_confirmed_at: None,
phone_number: None,
phone_number_confirmed_at: None,
last_sign_in: None,
app_metadata: dict.new(),
user_metadata: user_metadata,
),
user_metadata_decoder,
user_metadata_encoder,
)
}
16 changes: 15 additions & 1 deletion src/pevensie/drivers.gleam
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import gleam/dynamic.{type Decoder}
import pevensie/internal/user.{type User}
import gleam/json
import pevensie/internal/user.{type User, type UserInsert}

pub opaque type Connected {
Connected
Expand All @@ -13,6 +14,9 @@ pub opaque type Disabled {
Disabled
}

pub type Encoder(a) =
fn(a) -> json.Json

type ConnectFunction(auth_driver) =
fn(auth_driver) -> Result(auth_driver, Nil)

Expand All @@ -23,11 +27,21 @@ type GetUserFunction(auth_driver, user_metadata) =
fn(auth_driver, String, String, Decoder(user_metadata)) ->
Result(User(user_metadata), Nil)

type InsertUserFunction(auth_driver, user_metadata) =
fn(
auth_driver,
UserInsert(user_metadata),
Decoder(user_metadata),
Encoder(user_metadata),
) ->
Result(User(user_metadata), Nil)

pub type AuthDriver(driver, user_metadata) {
AuthDriver(
driver: driver,
connect: ConnectFunction(driver),
disconnect: DisconnectFunction(driver),
get_user: GetUserFunction(driver, user_metadata),
insert_user: InsertUserFunction(driver, user_metadata),
)
}
98 changes: 95 additions & 3 deletions src/pevensie/drivers/postgres.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import gleam/dynamic.{
type DecodeErrors as DynamicDecodeErrors, type Decoder,
DecodeError as DynamicDecodeError,
}
import gleam/io
import gleam/json
import gleam/option.{type Option, None, Some}
import gleam/pgo.{type QueryError as PgoQueryError}
import gleam/result
import pevensie/drivers.{type AuthDriver, AuthDriver}
import pevensie/internal/user.{type User, User}
import pevensie/drivers.{type AuthDriver, type Encoder, AuthDriver}
import pevensie/internal/user.{
type User, type UserInsert, User, app_metadata_to_json,
}

pub type IpVersion {
Ipv4
Expand Down Expand Up @@ -86,7 +89,16 @@ pub fn new_auth_driver(
disconnect: disconnect,
get_user: fn(driver, field, value, decoder) {
get_user(driver, field, value, decoder)
|> result.map_error(fn(_) { Nil })
// TODO: Handle errors
|> result.map_error(fn(_err) { Nil })
},
insert_user: fn(driver, user, user_metadata_decoder, user_metadata_encoder) {
insert_user(driver, user, user_metadata_decoder, user_metadata_encoder)
// TODO: Handle errors
|> result.map_error(fn(err) {
io.debug(err)
Nil
})
},
)
}
Expand Down Expand Up @@ -277,3 +289,83 @@ fn get_user(
_ -> Error(InternalError("Unexpected number of rows returned"))
}
}

fn insert_user(
driver: Postgres,
user: UserInsert(user_metadata),
decoder user_metadata_decoder: Decoder(user_metadata),
encoder user_metadata_encoder: Encoder(user_metadata),
) -> Result(User(user_metadata), GetUserError) {
let assert Postgres(_, Some(conn)) = driver

let sql =
"
insert into pevensie.\"user\" (
role,
email,
password_hash,
email_confirmed_at,
phone_number,
phone_number_confirmed_at,
app_metadata,
user_metadata
) values (
$1,
$2,
$3,
$4::timestamptz,
$5,
$6::timestamptz,
$7::jsonb,
$8::jsonb
)
returning
id::text,
-- Convert timestamp fields to UNIX epoch microseconds
(extract(epoch from created_at) * 1000000)::bigint as created_at,
(extract(epoch from updated_at) * 1000000)::bigint as updated_at,
(extract(epoch from deleted_at) * 1000000)::bigint as deleted_at,
role,
email,
password_hash,
(extract(epoch from email_confirmed_at) * 1000000)::bigint as email_confirmed_at,
phone_number,
(extract(epoch from phone_number_confirmed_at) * 1000000)::bigint as phone_number_confirmed_at,
(extract(epoch from last_sign_in) * 1000000)::bigint as last_sign_in,
app_metadata,
user_metadata,
(extract(epoch from banned_until) * 1000000)::bigint as banned_until
"

let query_result =
pgo.execute(
sql,
conn,
[
pgo.nullable(pgo.text, user.role),
pgo.text(user.email),
pgo.nullable(pgo.text, user.password_hash),
pgo.nullable(
pgo.timestamp,
user.email_confirmed_at |> option.map(birl.to_erlang_datetime),
),
pgo.nullable(pgo.text, user.phone_number),
pgo.nullable(
pgo.timestamp,
user.phone_number_confirmed_at |> option.map(birl.to_erlang_datetime),
),
pgo.text(app_metadata_to_json(user.app_metadata) |> json.to_string),
pgo.text(user_metadata_encoder(user.user_metadata) |> json.to_string),
],
postgres_user_decoder(user_metadata_decoder),
)
// TODO: Handle errors
|> result.map_error(QueryError)

use response <- result.try(query_result)
case response.rows {
[] -> Error(NotFound)
[user] -> Ok(user)
_ -> Error(InternalError("Unexpected number of rows returned"))
}
}
3 changes: 2 additions & 1 deletion src/pevensie/internal/auth.gleam
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import gleam/dynamic.{type Decoder}
import pevensie/drivers.{type AuthDriver}
import pevensie/drivers.{type AuthDriver, type Encoder}

pub type AuthConfig(driver, user_metadata, connected) {
AuthConfig(
driver: AuthDriver(driver, user_metadata),
user_metadata_decoder: Decoder(user_metadata),
user_metadata_encoder: Encoder(user_metadata),
)
AuthDisabled
}
12 changes: 8 additions & 4 deletions src/pevensie/internal/pevensie.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,31 @@ pub fn with_auth(
pub fn connect_auth(
pevensie: Pevensie(user_metadata, auth_driver, Disconnected),
) -> Result(Pevensie(user_metadata, auth_driver, Connected), Nil) {
let assert AuthConfig(driver, user_metadata_decoder) = pevensie.auth_config
let assert AuthConfig(driver, user_metadata_decoder, user_metadata_encoder) =
pevensie.auth_config

driver.connect(driver.driver)
|> result.map(fn(internal_driver) {
Pevensie(AuthConfig(
user_metadata_decoder:,
driver: AuthDriver(..driver, driver: internal_driver),
user_metadata_decoder:,
user_metadata_encoder:,
))
})
}

pub fn disconnect_auth(
pevensie: Pevensie(user_metadata, auth_driver, Connected),
) -> Result(Pevensie(user_metadata, auth_driver, Disconnected), Nil) {
let assert AuthConfig(driver, user_metadata_decoder) = pevensie.auth_config
let assert AuthConfig(driver, user_metadata_decoder, user_metadata_encoder) =
pevensie.auth_config

driver.disconnect(driver.driver)
|> result.map(fn(internal_driver) {
Pevensie(AuthConfig(
user_metadata_decoder:,
driver: AuthDriver(..driver, driver: internal_driver),
user_metadata_decoder:,
user_metadata_encoder:,
))
})
}
20 changes: 20 additions & 0 deletions src/pevensie/internal/user.gleam
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import birl.{type Time}
import gleam/dict.{type Dict}
import gleam/dynamic.{type Dynamic}
import gleam/json
import gleam/option.{type Option}

pub type User(user_metadata) {
Expand All @@ -21,3 +22,22 @@ pub type User(user_metadata) {
banned_until: Option(Time),
)
}

pub type UserInsert(user_metadata) {
UserInsert(
role: Option(String),
email: String,
password_hash: Option(String),
email_confirmed_at: Option(Time),
phone_number: Option(String),
phone_number_confirmed_at: Option(Time),
last_sign_in: Option(Time),
app_metadata: Dict(String, Dynamic),
user_metadata: user_metadata,
)
}

pub fn app_metadata_to_json(_app_metadata: Dict(String, Dynamic)) -> json.Json {
// TODO: Properly type app_metadata
json.object([])
}

0 comments on commit bbad61c

Please sign in to comment.