sqlc removed and adding sessionToken to jwt

This commit is contained in:
James Tombleson 2024-05-26 07:52:29 -07:00
parent 4e9a17209f
commit 47058dd866
20 changed files with 250 additions and 1783 deletions

View File

@ -996,6 +996,45 @@ const docTemplate = `{
} }
} }
}, },
"/v1/users/refresh/sessionToken": {
"post": {
"security": [
{
"Bearer": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Revokes the current session token and replaces it with a new one.",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.BaseResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.BaseResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.BaseResponse"
}
}
}
}
},
"/v1/users/refreshToken": { "/v1/users/refreshToken": {
"post": { "post": {
"security": [ "security": [
@ -1311,6 +1350,9 @@ const docTemplate = `{
"refreshToken": { "refreshToken": {
"type": "string" "type": "string"
}, },
"sessionToken": {
"type": "string"
},
"token": { "token": {
"type": "string" "type": "string"
}, },

View File

@ -987,6 +987,45 @@
} }
} }
}, },
"/v1/users/refresh/sessionToken": {
"post": {
"security": [
{
"Bearer": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Revokes the current session token and replaces it with a new one.",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.BaseResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.BaseResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.BaseResponse"
}
}
}
}
},
"/v1/users/refreshToken": { "/v1/users/refreshToken": {
"post": { "post": {
"security": [ "security": [
@ -1302,6 +1341,9 @@
"refreshToken": { "refreshToken": {
"type": "string" "type": "string"
}, },
"sessionToken": {
"type": "string"
},
"token": { "token": {
"type": "string" "type": "string"
}, },

View File

@ -84,6 +84,8 @@ definitions:
type: string type: string
refreshToken: refreshToken:
type: string type: string
sessionToken:
type: string
token: token:
type: string type: string
type: type:
@ -750,6 +752,30 @@ paths:
summary: Logs into the API and returns a bearer token if successful summary: Logs into the API and returns a bearer token if successful
tags: tags:
- Users - Users
/v1/users/refresh/sessionToken:
post:
consumes:
- application/json
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.BaseResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.BaseResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.BaseResponse'
security:
- Bearer: []
summary: Revokes the current session token and replaces it with a new one.
tags:
- Users
/v1/users/refreshToken: /v1/users/refreshToken:
post: post:
parameters: parameters:

View File

@ -10,6 +10,7 @@ type LoginResponse struct {
Token string `json:"token"` Token string `json:"token"`
Type string `json:"type"` Type string `json:"type"`
RefreshToken string `json:"refreshToken"` RefreshToken string `json:"refreshToken"`
SessionToken string `json:"sessionToken"`
} }
type ArticleResponse struct { type ArticleResponse struct {

View File

@ -1,31 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.16.0
package database
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@ -1,45 +0,0 @@
package database
import (
"strings"
"github.com/google/uuid"
)
type SourceDto struct {
ID uuid.UUID `json:"id"`
Site string `json:"site"`
Name string `json:"name"`
Source string `json:"source"`
Type string `json:"type"`
Value string `json:"value"`
Enabled bool `json:"enabled"`
Url string `json:"url"`
Tags []string `json:"tags"`
Deleted bool `json:"deleted"`
}
func ConvertToSourceDto(i Source) SourceDto {
var deleted bool
if !i.Deleted.Valid {
deleted = true
}
return SourceDto{
ID: i.ID,
Site: i.Site,
Name: i.Name,
Source: i.Source,
Type: i.Type,
Value: i.Value.String,
Enabled: i.Enabled,
Url: i.Url,
Tags: splitTags(i.Tags),
Deleted: deleted,
}
}
func splitTags(t string) []string {
items := strings.Split(t, ", ")
return items
}

View File

@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE Users ADD SessionToken TEXT;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE Users DROP SessionToken;
-- +goose StatementEnd

View File

@ -1,73 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.16.0
package database
import (
"database/sql"
"time"
"github.com/google/uuid"
)
type Article struct {
ID uuid.UUID
Sourceid uuid.UUID
Tags string
Title string
Url string
Pubdate time.Time
Video sql.NullString
Videoheight int32
Videowidth int32
Thumbnail string
Description string
Authorname sql.NullString
Authorimage sql.NullString
}
type Discordqueue struct {
ID uuid.UUID
Articleid uuid.UUID
}
type Discordwebhook struct {
ID uuid.UUID
Url string
Server string
Channel string
Enabled bool
}
type Icon struct {
ID uuid.UUID
Filename string
Site string
}
type Setting struct {
ID uuid.UUID
Key string
Value string
Options sql.NullString
}
type Source struct {
ID uuid.UUID
Site string
Name string
Source string
Type string
Value sql.NullString
Enabled bool
Url string
Tags string
Deleted sql.NullBool
}
type Subscription struct {
ID uuid.UUID
Discordwebhookid uuid.UUID
Sourceid uuid.UUID
}

File diff suppressed because it is too large Load Diff

View File

@ -1,215 +0,0 @@
/* Articles */
-- name: GetArticleByID :one
Select * from Articles
WHERE ID = $1 LIMIT 1;
-- name: GetArticleByUrl :one
Select * from Articles
Where Url = $1 LIMIT 1;
-- name: ListArticles :many
Select * From articles
Order By PubDate DESC
offset $2
fetch next $1 rows only;
-- name: ListArticlesByDate :many
Select * From articles
ORDER BY pubdate desc
Limit $1;
-- name: GetArticlesBySource :many
select * from articles
INNER join sources on articles.sourceid=Sources.ID
where site = $1;
-- name: ListNewArticlesBySourceId :many
SELECT * FROM articles
Where sourceid = $1
ORDER BY pubdate desc
offset $3
fetch next $2 rows only;
-- name: ListOldestArticlesBySourceId :many
SELECT * FROM articles
Where sourceid = $1
ORDER BY pubdate asc
offset $3
fetch next $2 rows only;
-- name: ListArticlesBySourceId :many
Select * From articles
Where sourceid = $1
Limit 50;
-- name: GetArticlesBySourceName :many
select
articles.ID, articles.SourceId, articles.Tags, articles.Title, articles.Url, articles.PubDate, articles.Video, articles.VideoHeight, articles.VideoWidth, articles.Thumbnail, articles.Description, articles.AuthorName, articles.AuthorImage, sources.source, sources.name
From articles
Left Join sources
On articles.sourceid = sources.id
Where name = $1;
-- name: ListArticlesByPage :many
select * from articles
order by pubdate desc
offset $2
fetch next $1 rows only;
-- name: CreateArticle :exec
INSERT INTO Articles
(ID, SourceId, Tags, Title, Url, PubDate, Video, VideoHeight, VideoWidth, Thumbnail, Description, AuthorName, AuthorImage)
Values
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13);
/* DiscordQueue */
-- name: CreateDiscordQueue :exec
Insert into DiscordQueue
(ID, ArticleId)
Values
($1, $2);
-- name: GetDiscordQueueByID :one
Select * from DiscordQueue
Where ID = $1 LIMIT 1;
-- name: DeleteDiscordQueueItem :exec
Delete From DiscordQueue Where ID = $1;
-- name: ListDiscordQueueItems :many
Select * from DiscordQueue LIMIT $1;
/* DiscordWebHooks */
-- name: CreateDiscordWebHook :exec
Insert Into DiscordWebHooks
(ID, Url, Server, Channel, Enabled)
Values
($1, $2, $3, $4, $5);
-- name: GetDiscordWebHooksByID :one
Select * from DiscordWebHooks
Where ID = $1 LIMIT 1;
-- name: ListDiscordWebHooksByServer :many
Select * From DiscordWebHooks
Where Server = $1;
-- name: GetDiscordWebHooksByServerAndChannel :many
SELECT * FROM DiscordWebHooks
WHERE Server = $1 and Channel = $2;
-- name: GetDiscordWebHookByUrl :one
Select * From DiscordWebHooks Where url = $1;
-- name: ListDiscordWebhooks :many
Select * From discordwebhooks LIMIT $1;
-- name: DeleteDiscordWebHooks :exec
Delete From discordwebhooks Where ID = $1;
-- name: DisableDiscordWebHook :exec
Update discordwebhooks Set Enabled = FALSE where ID = $1;
-- name: EnableDiscordWebHook :exec
Update discordwebhooks Set Enabled = TRUE where ID = $1;
/* Icons */
-- name: CreateIcon :exec
INSERT INTO Icons
(ID, FileName, Site)
VALUES
($1,$2,$3);
-- name: GetIconByID :one
Select * FROM Icons
Where ID = $1 Limit 1;
-- name: GetIconBySite :one
Select * FROM Icons
Where Site = $1 Limit 1;
-- name: DeleteIcon :exec
Delete From Icons where ID = $1;
/* Settings */
-- name: CreateSettings :one
Insert Into settings
(ID, Key, Value, OPTIONS)
Values
($1,$2,$3,$4)
RETURNING *;
-- name: GetSettingByID :one
Select * From settings
Where ID = $1 Limit 1;
-- name: GetSettingByKey :one
Select * From settings Where
Key = $1 Limit 1;
-- name: GetSettingByValue :one
Select * From settings Where
Value = $1 Limit 1;
-- name: DeleteSetting :exec
Delete From settings Where ID = $1;
/* Sources */
-- name: CreateSource :exec
Insert Into Sources
(ID, Site, Name, Source, Type, Value, Enabled, Url, Tags)
Values
($1,$2,$3,$4,$5,$6,$7,$8,$9);
-- name: GetSourceByID :one
Select * From Sources where ID = $1 Limit 1;
-- name: GetSourceByName :one
Select * from Sources where name = $1 Limit 1;
-- name: GetSourceByNameAndSource :one
Select * from Sources WHERE name = $1 and source = $2;
-- name: ListSources :many
Select * From Sources Limit $1;
-- name: ListSourcesBySource :many
Select * From Sources where Source = $1;
-- name: DeleteSource :exec
UPDATE Sources Set Disabled = TRUE where id = $1;
-- name: DisableSource :exec
Update Sources Set Enabled = FALSE where ID = $1;
-- name: EnableSource :exec
Update Sources Set Enabled = TRUE where ID = $1;
/* Subscriptions */
-- name: CreateSubscription :exec
Insert Into subscriptions (ID, DiscordWebHookId, SourceId) Values ($1, $2, $3);
-- name: ListSubscriptions :many
Select * From subscriptions Limit $1;
-- name: ListSubscriptionsBySourceId :many
Select * From subscriptions where sourceid = $1;
-- name: QuerySubscriptions :one
Select * From subscriptions Where discordwebhookid = $1 and sourceid = $2 Limit 1;
-- name: GetSubscriptionsBySourceID :many
Select * From subscriptions Where sourceid = $1;
-- name: GetSubscriptionsByDiscordWebHookId :many
Select * from subscriptions Where discordwebhookid = $1;
-- name: DeleteSubscription :exec
Delete From subscriptions Where id = $1;

View File

@ -1,61 +0,0 @@
CREATE TABLE Articles (
ID uuid PRIMARY KEY,
SourceId uuid NOT null,
Tags TEXT NOT NULL,
Title TEXT NOT NULL,
Url TEXT NOT NULL,
PubDate timestamp NOT NULL,
Video TEXT,
VideoHeight int NOT NULL,
VideoWidth int NOT NULL,
Thumbnail TEXT NOT NULL,
Description TEXT NOT NULL,
AuthorName TEXT,
AuthorImage TEXT
);
CREATE Table DiscordQueue (
ID uuid PRIMARY KEY,
ArticleId uuid NOT NULL
);
CREATE Table DiscordWebHooks (
ID uuid PRIMARY KEY,
Url TEXT NOT NULL, -- Webhook Url
Server TEXT NOT NULL, -- Defines the server its bound it. Used for refrence
Channel TEXT NOT NULL, -- Defines the channel its bound to. Used for refrence
Enabled BOOLEAN NOT NULL
);
CREATE Table Icons (
ID uuid PRIMARY Key,
FileName TEXT NOT NULL,
Site TEXT NOT NULL
);
Create Table Settings (
ID uuid PRIMARY Key,
Key TEXT NOT NULL,
Value TEXT NOT NULL,
Options TEXT
);
Create Table Sources (
ID uuid PRIMARY Key,
Site TEXT NOT NULL, -- Vanity name
Name TEXT NOT NULL, -- Defines the name of the source. IE: dadjokes
Source TEXT NOT NULL, -- Defines the service that will use this reocrd. IE reddit or youtube
Type TEXT NOT NULL, -- Defines what kind of feed this is. feed, user, tag
Value TEXT,
Enabled BOOLEAN NOT NULL,
Url TEXT NOT NULL,
Tags TEXT NOT NULL,
Deleted BOOLEAN
);
/* This table is used to track what the Web Hook wants to have sent by Source */;
Create TABLE Subscriptions (
ID uuid Primary Key,
DiscordWebHookID uuid Not Null,
SourceID uuid Not Null
);

View File

@ -128,6 +128,7 @@ type UserEntity struct {
Username string Username string
Hash string Hash string
Scopes string Scopes string
SessionToken string
} }
type RefreshTokenEntity struct { type RefreshTokenEntity struct {

View File

@ -83,11 +83,11 @@ func (j JwtToken) hasScope(scope string) error {
return errors.New(ErrJwtScopeMissing) return errors.New(ErrJwtScopeMissing)
} }
func (h *Handler) generateJwt(username, issuer string, userScopes []string, userId int64) (string, error) { func (h *Handler) generateJwt(username, issuer, sessionToken string, userScopes []string, userId int64) (string, error) {
return h.generateJwtWithExp(username, issuer, userScopes, userId, time.Now().Add(10*time.Minute)) return h.generateJwtWithExp(username, issuer, sessionToken, userScopes, userId, time.Now().Add(10*time.Minute))
} }
func (h *Handler) generateJwtWithExp(username, issuer string, userScopes []string, userId int64, expiresAt time.Time) (string, error) { func (h *Handler) generateJwtWithExp(username, issuer, sessionToken string, userScopes []string, userId int64, expiresAt time.Time) (string, error) {
secret := []byte(h.config.JwtSecret) secret := []byte(h.config.JwtSecret)
// Anyone who wants to decrypt the key needs to use the same method // Anyone who wants to decrypt the key needs to use the same method
@ -98,6 +98,7 @@ func (h *Handler) generateJwtWithExp(username, issuer string, userScopes []strin
claims["username"] = username claims["username"] = username
claims["iss"] = issuer claims["iss"] = issuer
claims["userId"] = userId claims["userId"] = userId
claims["sessionToken"] = sessionToken
var scopes []string var scopes []string
scopes = append(scopes, userScopes...) scopes = append(scopes, userScopes...)

View File

@ -8,6 +8,7 @@ import (
"git.jamestombleson.com/jtom38/newsbot-api/domain" "git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository" "git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
"github.com/google/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@ -48,7 +49,7 @@ func (h *Handler) AuthRegister(c echo.Context) error {
return h.WriteError(c, err, http.StatusInternalServerError) return h.WriteError(c, err, http.StatusInternalServerError)
} }
_, err = h.repo.Users.Create(c.Request().Context(), username, password, domain.ScopeArticleRead) _, err = h.repo.Users.Create(c.Request().Context(), username, password, "", domain.ScopeArticleRead)
if err != nil { if err != nil {
return h.InternalServerErrorResponse(c, err.Error()) return h.InternalServerErrorResponse(c, err.Error())
} }
@ -91,8 +92,12 @@ func (h *Handler) AuthLogin(c echo.Context) error {
// TODO think about moving this down some? // TODO think about moving this down some?
expiresAt := time.Now().Add(time.Hour * 48) expiresAt := time.Now().Add(time.Hour * 48)
userScopes := strings.Split(user.Scopes, ",") userScopes := strings.Split(user.Scopes, ",")
sessionToken, err := uuid.NewV7()
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
jwt, err := h.generateJwtWithExp(username, h.config.ServerAddress, userScopes, user.ID, expiresAt) jwt, err := h.generateJwtWithExp(username, h.config.ServerAddress, sessionToken.String(), userScopes, user.ID, expiresAt)
if err != nil { if err != nil {
return h.InternalServerErrorResponse(c, err.Error()) return h.InternalServerErrorResponse(c, err.Error())
} }
@ -109,6 +114,7 @@ func (h *Handler) AuthLogin(c echo.Context) error {
Token: jwt, Token: jwt,
Type: "Bearer", Type: "Bearer",
RefreshToken: refresh, RefreshToken: refresh,
SessionToken: sessionToken.String(),
}) })
} }
@ -124,8 +130,12 @@ func (h *Handler) createAdminToken(c echo.Context, password string) error {
} }
var userScopes []string var userScopes []string
userScopes = append(userScopes, domain.ScopeAll) userScopes = append(userScopes, domain.ScopeAll)
sessionToken, err := uuid.NewV7()
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
token, err := h.generateJwt("admin", h.config.ServerAddress, userScopes, -1) token, err := h.generateJwt("admin", h.config.ServerAddress, sessionToken.String(), userScopes, -1)
if err != nil { if err != nil {
return h.InternalServerErrorResponse(c, err.Error()) return h.InternalServerErrorResponse(c, err.Error())
} }
@ -173,7 +183,7 @@ func (h *Handler) RefreshJwtToken(c echo.Context) error {
} }
userScopes := strings.Split(user.Scopes, ",") userScopes := strings.Split(user.Scopes, ",")
jwt, err := h.generateJwtWithExp(request.Username, h.config.ServerAddress, userScopes, user.ID, time.Now().Add(time.Hour*48)) jwt, err := h.generateJwtWithExp(request.Username, h.config.ServerAddress, "", userScopes, user.ID, time.Now().Add(time.Hour*48))
if err != nil { if err != nil {
return h.InternalServerErrorResponse(c, err.Error()) return h.InternalServerErrorResponse(c, err.Error())
} }
@ -250,7 +260,6 @@ func (h *Handler) RemoveScopes(c echo.Context) error {
err = (&echo.DefaultBinder{}).BindBody(c, &request) err = (&echo.DefaultBinder{}).BindBody(c, &request)
if err != nil { if err != nil {
h.WriteError(c, err, http.StatusBadRequest) h.WriteError(c, err, http.StatusBadRequest)
} }
err = h.repo.Users.RemoveScopes(c.Request().Context(), request.Username, request.Scopes) err = h.repo.Users.RemoveScopes(c.Request().Context(), request.Username, request.Scopes)
@ -262,3 +271,26 @@ func (h *Handler) RemoveScopes(c echo.Context) error {
Message: "OK", Message: "OK",
}) })
} }
// @Summary Revokes the current session token and replaces it with a new one.
// @Router /v1/users/refresh/sessionToken [post]
// @Tags Users
// @Accept json
// @Produce json
// @Success 200 {object} domain.BaseResponse
// @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse
// @Security Bearer
func (h *Handler) LogoutEverywhere(c echo.Context) error {
token, err := h.getJwtTokenFromContext(c)
if err != nil {
return h.WriteError(c, err, http.StatusUnauthorized)
}
err = token.IsValid(domain.ScopeAll)
if err != nil {
return h.WriteError(c, err, http.StatusUnauthorized)
}
return c.String(http.StatusInternalServerError, "Not Implemented")
}

View File

@ -14,17 +14,18 @@ import (
) )
const ( const (
TableName string = "users" usersTableName string = "users"
ErrUserNotFound string = "requested user was not found" ErrUserNotFound string = "requested user was not found"
) )
type Users interface { type Users interface {
GetByName(ctx context.Context, name string) (entity.UserEntity, error) GetByName(ctx context.Context, name string) (entity.UserEntity, error)
Create(ctx context.Context, name, password, scope string) (int64, error) Create(ctx context.Context, name, password, sessionTOken, scope string) (int64, error)
Update(ctx context.Context, id int, entity entity.UserEntity) error Update(ctx context.Context, id int, entity entity.UserEntity) error
UpdatePassword(ctx context.Context, name, password string) error UpdatePassword(ctx context.Context, name, password string) error
CheckUserHash(ctx context.Context, name, password string) error CheckUserHash(ctx context.Context, name, password string) error
UpdateScopes(ctx context.Context, name, scope string) error UpdateScopes(ctx context.Context, name, scope string) error
UpdateSessionToken(ctx context.Context, name, sessionToken string) (int64, error)
} }
// Creates a new instance of UserRepository with the bound sql // Creates a new instance of UserRepository with the bound sql
@ -58,7 +59,7 @@ func (ur userRepository) GetByName(ctx context.Context, name string) (entity.Use
return data[0], nil return data[0], nil
} }
func (ur userRepository) Create(ctx context.Context, name, password, scope string) (int64, error) { func (ur userRepository) Create(ctx context.Context, name, password, sessionToken, scope string) (int64, error) {
passwordBytes := []byte(password) passwordBytes := []byte(password)
hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
if err != nil { if err != nil {
@ -68,8 +69,8 @@ func (ur userRepository) Create(ctx context.Context, name, password, scope strin
dt := time.Now() dt := time.Now()
queryBuilder := sqlbuilder.NewInsertBuilder() queryBuilder := sqlbuilder.NewInsertBuilder()
queryBuilder.InsertInto("users") queryBuilder.InsertInto("users")
queryBuilder.Cols("Name", "Hash", "UpdatedAt", "CreatedAt", "DeletedAt", "Scopes") queryBuilder.Cols("Name", "Hash", "UpdatedAt", "CreatedAt", "DeletedAt", "Scopes", "SessionToken")
queryBuilder.Values(name, string(hash), dt, dt, time.Time{}, scope) queryBuilder.Values(name, string(hash), dt, dt, time.Time{}, scope, sessionToken)
query, args := queryBuilder.Build() query, args := queryBuilder.Build()
_, err = ur.connection.ExecContext(ctx, query, args...) _, err = ur.connection.ExecContext(ctx, query, args...)
@ -91,11 +92,35 @@ func (ur userRepository) UpdatePassword(ctx context.Context, name, password stri
} }
queryBuilder := sqlbuilder.NewUpdateBuilder() queryBuilder := sqlbuilder.NewUpdateBuilder()
queryBuilder.Update(TableName) queryBuilder.Update(usersTableName)
//queryBuilder.Set //queryBuilder.Set
return nil return nil
} }
func (ur userRepository) UpdateSessionToken(ctx context.Context, name, sessionToken string) (int64, error) {
_, err := ur.GetByName(ctx, name)
if err != nil {
return 0, err
}
q := sqlbuilder.NewUpdateBuilder()
q.Update(usersTableName)
q.Set(
q.Equal("SessionToken", sessionToken),
)
q.Where(
q.Equal("Name", name),
)
query, args := q.Build()
rowsUpdates, err := ur.connection.ExecContext(ctx, query, args...)
if err != nil {
return 0, err
}
return rowsUpdates.RowsAffected()
}
// If the hash matches what we have in the database, an error will not be returned. // If the hash matches what we have in the database, an error will not be returned.
// If the user does not exist or the hash does not match, an error will be returned // If the user does not exist or the hash does not match, an error will be returned
func (ur userRepository) CheckUserHash(ctx context.Context, name, password string) error { func (ur userRepository) CheckUserHash(ctx context.Context, name, password string) error {
@ -141,7 +166,8 @@ func (ur userRepository) processRows(rows *sql.Rows) []entity.UserEntity {
var updatedAt time.Time var updatedAt time.Time
var deletedAt sql.NullTime var deletedAt sql.NullTime
var scopes string var scopes string
err := rows.Scan(&id, &createdAt, &updatedAt, &deletedAt, &username, &hash, &scopes) var sessionToken string
err := rows.Scan(&id, &createdAt, &updatedAt, &deletedAt, &username, &hash, &scopes, &sessionToken)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
@ -153,6 +179,7 @@ func (ur userRepository) processRows(rows *sql.Rows) []entity.UserEntity {
Hash: hash, Hash: hash,
Scopes: scopes, Scopes: scopes,
CreatedAt: createdAt, CreatedAt: createdAt,
SessionToken: sessionToken,
} }
if deletedAt.Valid { if deletedAt.Valid {
item.DeletedAt = deletedAt.Time item.DeletedAt = deletedAt.Time

View File

@ -21,7 +21,7 @@ func TestCanCreateNewUser(t *testing.T) {
defer db.Close() defer db.Close()
repo := repository.NewUserRepository(db) repo := repository.NewUserRepository(db)
updated, err := repo.Create(context.Background(), "testing", "NotSecure", "placeholder") updated, err := repo.Create(context.Background(), "testing", "NotSecure", "sessionToken", "placeholder")
if err != nil { if err != nil {
log.Println(err) log.Println(err)
t.FailNow() t.FailNow()
@ -38,7 +38,7 @@ func TestCanFindUserInTable(t *testing.T) {
defer db.Close() defer db.Close()
repo := repository.NewUserRepository(db) repo := repository.NewUserRepository(db)
updated, err := repo.Create(context.Background(), "testing", "NotSecure", "placeholder") updated, err := repo.Create(context.Background(), "testing", "NotSecure", "sessionToken", "placeholder")
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()

View File

@ -4,11 +4,13 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"strings" "strings"
"git.jamestombleson.com/jtom38/newsbot-api/domain" "git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository" "git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@ -25,7 +27,8 @@ type UserServices interface {
GetUser(ctx context.Context, username string) (entity.UserEntity, error) GetUser(ctx context.Context, username string) (entity.UserEntity, error)
AddScopes(ctx context.Context, username string, scopes []string) error AddScopes(ctx context.Context, username string, scopes []string) error
RemoveScopes(ctx context.Context, username string, scopes []string) error RemoveScopes(ctx context.Context, username string, scopes []string) error
Create(ctx context.Context, name, password, scope string) (entity.UserEntity, error) Create(ctx context.Context, name, password, sessionToken, scope string) (entity.UserEntity, error)
NewSessionToken(ctx context.Context, name string) error
CheckPasswordForRequirements(password string) error CheckPasswordForRequirements(password string) error
} }
@ -125,16 +128,34 @@ func (us UserService) doesScopeExist(scopes []string, target string) bool {
return false return false
} }
func (us UserService) Create(ctx context.Context, name, password, scope string) (entity.UserEntity, error) { func (us UserService) Create(ctx context.Context, name, password, sessionToken, scope string) (entity.UserEntity, error) {
err := us.CheckPasswordForRequirements(password) err := us.CheckPasswordForRequirements(password)
if err != nil { if err != nil {
return entity.UserEntity{}, err return entity.UserEntity{}, err
} }
us.repo.Create(ctx, name, password, domain.ScopeArticleRead) us.repo.Create(ctx, name, password, sessionToken, domain.ScopeArticleRead)
return entity.UserEntity{}, nil return entity.UserEntity{}, nil
} }
func (us UserService) NewSessionToken(ctx context.Context, name string) error {
token, err := uuid.NewV7()
if err != nil {
return err
}
rows, err := us.repo.UpdateSessionToken(ctx, name, token.String())
if err != nil {
return err
}
if rows != 1 {
return fmt.Errorf("UserService.NewSessionToken %w", err)
}
return nil
}
func (us UserService) CheckPasswordForRequirements(password string) error { func (us UserService) CheckPasswordForRequirements(password string) error {
err := us.checkPasswordLength(password) err := us.checkPasswordLength(password)
if err != nil { if err != nil {

View File

@ -7,12 +7,12 @@ import (
_ "github.com/lib/pq" _ "github.com/lib/pq"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" //"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services" "git.jamestombleson.com/jtom38/newsbot-api/internal/services"
) )
type Cron struct { type Cron struct {
Db *database.Queries //Db *database.Queries
ctx context.Context ctx context.Context
timer *cron.Cron timer *cron.Cron
repo services.RepositoryService repo services.RepositoryService

View File

@ -8,7 +8,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
//"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
) )
type discordField struct { type discordField struct {
@ -63,11 +64,11 @@ const (
type Discord struct { type Discord struct {
Subscriptions []string Subscriptions []string
article database.Article article entity.ArticleEntity
Message *DiscordMessage Message *DiscordMessage
} }
func NewDiscordWebHookMessage(Article database.Article) Discord { func NewDiscordWebHookMessage(Article entity.ArticleEntity) Discord {
return Discord{ return Discord{
article: Article, article: Article,
} }

View File

@ -5,22 +5,19 @@ import (
"strings" "strings"
"testing" "testing"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" //"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/output" "git.jamestombleson.com/jtom38/newsbot-api/internal/services/output"
"github.com/google/uuid"
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
var ( var (
article database.Article = database.Article{ article entity.ArticleEntity = entity.ArticleEntity{
ID: uuid.New(), ID: 999,
Sourceid: uuid.New(), SourceID: 1,
Tags: "unit, testing", Tags: "unit, testing",
Title: "Demo", Title: "Demo",
Url: "https://github.com/jtom38/newsbot.collector.api", Url: "https://github.com/jtom38/newsbot.collector.api",
//Pubdate: time.Now(),
Videoheight: 0,
Videowidth: 0,
Description: "Hello World", Description: "Hello World",
} }
blank string = "" blank string = ""