Skip to content

Commit

Permalink
sessions!
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacharrisholt committed Sep 4, 2024
1 parent c6af58d commit 5f35963
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 23 deletions.
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ gleam_pgo = ">= 0.13.0 and < 1.0.0"
birl = ">= 1.7.1 and < 2.0.0"
decode = ">= 0.2.0 and < 1.0.0"
gleam_json = ">= 1.0.1 and < 2.0.0"
youid = ">= 1.2.0 and < 2.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
3 changes: 3 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ packages = [
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
{ name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
{ name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
{ name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
{ name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
{ name = "gleam_javascript", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "483631D3001FCE8EB12ADEAD5E1B808440038E96F93DA7A32D326C82F480C0B2" },
{ name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
Expand All @@ -31,6 +32,7 @@ packages = [
{ name = "startest", version = "0.5.0", build_tools = ["gleam"], requirements = ["argv", "bigben", "birl", "exception", "gleam_community_ansi", "gleam_erlang", "gleam_javascript", "gleam_stdlib", "glint", "simplifile", "tom"], otp_app = "startest", source = "hex", outer_checksum = "7A4BE0A1674D1DEB421B0BEB1E90B1A9C4AB21D954CF1754C0E28EA6B5DBE785" },
{ name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
{ name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
{ name = "youid", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_stdlib"], otp_app = "youid", source = "hex", outer_checksum = "EF0F693004E221155EE5909C6D3C945DD14F7117DBA882887CF5F45BE399B8CA" },
]

[requirements]
Expand All @@ -42,3 +44,4 @@ gleam_json = { version = ">= 1.0.1 and < 2.0.0" }
gleam_pgo = { version = ">= 0.13.0 and < 1.0.0" }
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
youid = { version = ">= 1.2.0 and < 2.0.0" }
138 changes: 136 additions & 2 deletions src/pevensie/auth.gleam
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import argus
import birl.{type Time}
import birl/duration
import gleam/dict
import gleam/dynamic.{type Decoder}
import gleam/json
import gleam/option.{type Option, None, Some}
import gleam/result
import pevensie/cache
import pevensie/drivers.{
type AuthDriver, type Connected, type Disabled, type Disconnected,
type Encoder,
}
import pevensie/internal/auth
import pevensie/internal/encoder.{type Encoder}
import pevensie/internal/pevensie.{type Pevensie}
import pevensie/internal/user.{
type User as InternalUser, type UserInsert as UserInsertInternal,
type UserUpdate as UserUpdateInternal, Set, UserInsert, UserUpdate,
default_user_update,
}
import youid/uuid

pub type User(user_metadata) =
InternalUser(user_metadata)
Expand Down Expand Up @@ -76,7 +81,7 @@ fn hash_password(password: String) {
|> argus.hash(password, argus.gen_salt())
}

pub fn verify_email_and_password(
pub fn get_user_by_email_and_password(
pevensie: Pevensie(
user_metadata,
auth_driver,
Expand Down Expand Up @@ -167,3 +172,132 @@ pub fn set_user_role(
user_metadata_encoder,
)
}

pub type Session {
Session(id: String, user_id: String, created_at: Time, expires_at: Time)
}

fn encode_session(session: Session) -> json.Json {
json.object([
#("id", json.string(session.id)),
#("user_id", json.string(session.user_id)),
#("created_at", json.string(birl.to_iso8601(session.created_at))),
#("expires_at", json.string(birl.to_iso8601(session.expires_at))),
])
}

fn session_decoder() -> Decoder(Session) {
let time_decoder = fn(time) {
use time <- result.try(dynamic.string(time))
birl.parse(time)
|> result.replace_error([])
}

dynamic.decode4(
Session,
dynamic.field("id", dynamic.string),
dynamic.field("user_id", dynamic.string),
dynamic.field("created_at", time_decoder),
dynamic.field("expires_at", time_decoder),
)
}

const session_resource_type = "pevensie:session"

pub fn create_session(
pevensie: Pevensie(
user_metadata,
auth_driver,
Connected,
cache_driver,
Connected,
),
user_id: String,
ttl_seconds: Option(Int),
) -> Result(Session, Nil) {
// Check if the user exists
use user <- result.try(get_user_by_id(pevensie, user_id))

let ttl_seconds = option.unwrap(ttl_seconds, 24 * 60 * 60)
let now = birl.now()
let session =
Session(
id: uuid.v7_string(),
user_id: user.id,
created_at: now,
expires_at: birl.add(now, duration.seconds(ttl_seconds)),
)

use _ <- result.try(cache.store(
pevensie,
session_resource_type,
session.id,
session
|> encode_session
|> json.to_string,
Some(ttl_seconds),
))

Ok(session)
}

pub fn get_session(
pevensie: Pevensie(
user_metadata,
auth_driver,
Connected,
cache_driver,
Connected,
),
session_id: String,
) -> Result(Option(Session), Nil) {
use session <- result.try(cache.get(
pevensie,
session_resource_type,
session_id,
))
case session {
None -> Ok(None)
Some(session_string) -> {
use decoded_session <- result.try(
json.decode(session_string, session_decoder())
|> result.replace_error(Nil),
)
Ok(Some(decoded_session))
}
}
}

pub fn delete_session(
pevensie: Pevensie(
user_metadata,
auth_driver,
Connected,
cache_driver,
Connected,
),
session_id: String,
) -> Result(Nil, Nil) {
use _ <- result.try(cache.delete(pevensie, session_resource_type, session_id))
Ok(Nil)
}

pub fn log_in_user(
pevensie: Pevensie(
user_metadata,
auth_driver,
Connected,
cache_driver,
Connected,
),
email: String,
password: String,
) -> Result(#(Session, User(user_metadata)), Nil) {
use user <- result.try(get_user_by_email_and_password(
pevensie,
email,
password,
))
create_session(pevensie, user.id, Some(24 * 60 * 60))
|> result.map(fn(session) { #(session, user) })
}
5 changes: 1 addition & 4 deletions src/pevensie/drivers.gleam
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import gleam/dynamic.{type Decoder}
import gleam/json
import gleam/option.{type Option}
import pevensie/internal/encoder.{type Encoder}
import pevensie/internal/user.{type User, type UserInsert, type UserUpdate}

pub type Connected
Expand All @@ -9,9 +9,6 @@ pub type Disconnected

pub type Disabled

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

/// A function that connects the auth driver. This may
/// set up any connections or perform any other setup
/// required to make the driver ready to use.
Expand Down
31 changes: 16 additions & 15 deletions src/pevensie/drivers/postgres.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import gleam/pgo.{type QueryError as PgoQueryError}
import gleam/result
import gleam/string
import pevensie/drivers.{
type AuthDriver, type CacheDriver, type Encoder, AuthDriver, CacheDriver,
type AuthDriver, type CacheDriver, AuthDriver, CacheDriver,
}
import pevensie/internal/encoder.{type Encoder}
import pevensie/internal/user.{
type UpdateField, type User, type UserInsert, type UserUpdate, Ignore, Set,
User, app_metadata_to_json,
Expand Down Expand Up @@ -266,20 +267,20 @@ fn postgres_user_decoder(
}

Ok(User(
id,
created_at,
updated_at,
deleted_at,
role,
email,
password_hash,
email_confirmed_at,
phone_number,
phone_number_confirmed_at,
last_sign_in,
app_metadata,
user_metadata,
banned_until,
id:,
created_at:,
updated_at:,
deleted_at:,
role:,
email:,
password_hash:,
email_confirmed_at:,
phone_number:,
phone_number_confirmed_at:,
last_sign_in:,
app_metadata:,
user_metadata:,
banned_until:,
))
})
|> decode.field(0, decode.string)
Expand Down
3 changes: 2 additions & 1 deletion src/pevensie/internal/auth.gleam
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import gleam/dynamic.{type Decoder}
import pevensie/drivers.{type AuthDriver, type Encoder}
import pevensie/drivers.{type AuthDriver}
import pevensie/internal/encoder.{type Encoder}

pub type AuthConfig(driver, user_metadata, connected) {
AuthConfig(
Expand Down
4 changes: 4 additions & 0 deletions src/pevensie/internal/encoder.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import gleam/json

pub type Encoder(a) =
fn(a) -> json.Json
52 changes: 51 additions & 1 deletion src/pevensie/internal/user.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import gleam/dict.{type Dict}
import gleam/dynamic.{type Dynamic}
import gleam/json
import gleam/option.{type Option}
import pevensie/internal/encoder.{type Encoder}

pub type User(user_metadata) {
User(
Expand All @@ -14,7 +15,7 @@ pub type User(user_metadata) {
email: String,
password_hash: Option(String),
email_confirmed_at: Option(Time),
phome_number: Option(String),
phone_number: Option(String),
phone_number_confirmed_at: Option(Time),
last_sign_in: Option(Time),
app_metadata: Dict(String, Dynamic),
Expand Down Expand Up @@ -74,3 +75,52 @@ pub fn app_metadata_to_json(_app_metadata: Dict(String, Dynamic)) -> json.Json {
// TODO: Properly type app_metadata
json.object([])
}

pub fn user_encoder(
user: User(user_metadata),
user_metadata_encoder: Encoder(user_metadata),
) -> json.Json {
json.object([
#("id", json.string(user.id)),
#("created_at", json.string(birl.to_iso8601(user.created_at))),
#("updated_at", json.string(birl.to_iso8601(user.updated_at))),
#(
"deleted_at",
json.nullable(user.deleted_at |> option.map(birl.to_iso8601), json.string),
),
#("role", json.nullable(user.role, json.string)),
#("email", json.string(user.email)),
#("password_hash", json.nullable(user.password_hash, json.string)),
#(
"email_confirmed_at",
json.nullable(
user.email_confirmed_at |> option.map(birl.to_iso8601),
json.string,
),
),
#("phone_number", json.nullable(user.phone_number, json.string)),
#(
"phone_number_confirmed_at",
json.nullable(
user.phone_number_confirmed_at |> option.map(birl.to_iso8601),
json.string,
),
),
#(
"last_sign_in",
json.nullable(
user.last_sign_in |> option.map(birl.to_iso8601),
json.string,
),
),
#("app_metadata", app_metadata_to_json(user.app_metadata)),
#("user_metadata", user_metadata_encoder(user.user_metadata)),
#(
"banned_until",
json.nullable(
user.banned_until |> option.map(birl.to_iso8601),
json.string,
),
),
])
}

0 comments on commit 5f35963

Please sign in to comment.