diff --git a/auth/session.go b/auth/session.go index 93075857..40294e3c 100644 --- a/auth/session.go +++ b/auth/session.go @@ -16,7 +16,9 @@ import ( ) type LoginDto struct { + // Either the email or the username. Login string `json:"login" validate:"required"` + // Password of the account. Password string `json:"password" validate:"required"` } @@ -25,12 +27,13 @@ type LoginDto struct { // @Tags sessions // @Accept json // @Produce json -// @Param device query uuid false "The device the created session will be used on" -// @Param user body LoginDto false "Account informations" -// @Success 201 {object} dbc.Session -// @Failure 400 {object} problem.Problem "Invalid login body" -// @Failure 400 {object} problem.Problem "Invalid password" -// @Failure 404 {object} problem.Problem "Account does not exists" +// @Param device query string false "The device the created session will be used on" +// @Param login body LoginDto false "Account informations" +// @Success 201 {object} dbc.Session +// @Failure 400 {object} problem.Problem "Invalid login body" +// @Failure 403 {object} problem.Problem "Invalid password" +// @Failure 404 {object} problem.Problem "Account does not exists" +// @Failure 422 {object} problem.Problem "User does not have a password (registered via oidc, please login via oidc)" // @Router /sessions [post] func (h *Handler) Login(c echo.Context) error { var req LoginDto @@ -47,7 +50,7 @@ func (h *Handler) Login(c echo.Context) error { return echo.NewHTTPError(http.StatusNotFound, "No account exists with the specified email or username.") } if dbuser.Password == nil { - return echo.NewHTTPError(http.StatusBadRequest, "Can't login with password, this account was created with OIDC.") + return echo.NewHTTPError(http.StatusUnprocessableEntity, "Can't login with password, this account was created with OIDC.") } match, err := argon2id.ComparePasswordAndHash(req.Password, *dbuser.Password) @@ -55,7 +58,7 @@ func (h *Handler) Login(c echo.Context) error { return err } if !match { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid password") + return echo.NewHTTPError(http.StatusForbidden, "Invalid password") } user := MapDbUser(&dbuser) @@ -88,9 +91,20 @@ func (h *Handler) createSession(c echo.Context, user *User) error { return c.JSON(201, session) } +// @Summary Get JWT +// @Description Convert a session token to a short lived JWT. +// @Tags sessions +// @Accept json +// @Produce json +// @Param user body LoginDto false "Account informations" +// @Success 200 {object} dbc.Session +// @Failure 400 {object} problem.Problem "Invalid login body" +// @Failure 400 {object} problem.Problem "Invalid password" +// @Failure 404 {object} problem.Problem "Account does not exists" +// @Router /jwt [get] func (h *Handler) CreateJwt(c echo.Context, user *User) error { claims := maps.Clone(user.Claims) - claims["sub"] = user.ID.String() + claims["sub"] = user.Id.String() claims["iss"] = h.config.Issuer claims["exp"] = &jwt.NumericDate{ Time: time.Now().UTC().Add(time.Hour), diff --git a/auth/sql/queries/sessions.sql b/auth/sql/queries/sessions.sql index 2b6c1d48..5cfa829c 100644 --- a/auth/sql/queries/sessions.sql +++ b/auth/sql/queries/sessions.sql @@ -1,11 +1,11 @@ --- name: GetUserFromSession :one +-- name: GetUserFromToken :one select u.* from users as u left join sessions as s on u.id = s.user_id where - s.id = $1 + s.token = $1 limit 1; -- name: TouchSession :exec @@ -27,7 +27,7 @@ order by last_used; -- name: CreateSession :one -insert into sessions(id, user_id, device) +insert into sessions(token, user_id, device) values ($1, $2, $3) returning *; @@ -38,3 +38,9 @@ where id = $1 returning *; +-- name: DeleteSessionByToken :one +delete from sessions +where token = $1 +returning + *; + diff --git a/auth/users.go b/auth/users.go index 6035372f..fc4512b1 100644 --- a/auth/users.go +++ b/auth/users.go @@ -13,30 +13,43 @@ import ( ) type User struct { - ID uuid.UUID `json:"id"` + // Id of the user. + Id uuid.UUID `json:"id"` + // Username of the user. Can be used as a login. Username string `json:"username"` + // Email of the user. Can be used as a login. Email string `json:"email" format:"email"` + // When was this account created? CreatedDate time.Time `json:"createdDate"` + // When was the last time this account made any authorized request? LastSeen time.Time `json:"lastSeen"` + // List of custom claims JWT created via get /jwt will have Claims jwt.MapClaims `json:"claims"` + // List of other login method available for this user. Access tokens wont be returned here. Oidc map[string]OidcHandle `json:"oidc,omitempty"` } type OidcHandle struct { + // Id of this oidc handle. Id string `json:"id"` + // Username of the user on the external service. Username string `json:"username"` + // Link to the profile of the user on the external service. Null if unknown or irrelevant. ProfileUrl *string `json:"profileUrl" format:"url"` } type RegisterDto struct { + // Username of the new account, can't contain @ signs. Can be used for login. Username string `json:"username" validate:"required,excludes=@"` + // Valid email that could be used for forgotten password requests. Can be used for login. Email string `json:"email" validate:"required,email" format:"email"` + // Password to use. Password string `json:"password" validate:"required"` } func MapDbUser(user *dbc.User) User { return User{ - ID: user.ID, + Id: user.ID, Username: user.Username, Email: user.Email, CreatedDate: user.CreatedDate, @@ -53,7 +66,7 @@ func MapDbUser(user *dbc.User) User { // @Produce json // @Param afterId query string false "used for pagination." Format(uuid) // @Success 200 {object} User[] -// @Failure 400 {object} problem.Problem +// @Failure 400 {object} problem.Problem "Invalid after id" // @Router /users [get] func (h *Handler) ListUsers(c echo.Context) error { ctx := context.Background()