Compare commits
No commits in common. "471ef4fdd897723f1d48dcb000ed9475a5fee72e" and "53b04696471a425a2f3b94eea90fc21187b51487" have entirely different histories.
471ef4fdd8
...
53b0469647
15
Dockerfile
15
Dockerfile
@ -5,10 +5,14 @@ WORKDIR /app
|
||||
|
||||
# Always make sure that swagger docs are updated
|
||||
RUN go install github.com/swaggo/swag/cmd/swag@latest
|
||||
RUN /go/bin/swag init -g cmd/server.go
|
||||
RUN /go/bin/swag i
|
||||
|
||||
#RUN go build .
|
||||
#RUN go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||
# Always build the latest sql queries
|
||||
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
|
||||
RUN /go/bin/sqlc generate
|
||||
|
||||
RUN go build .
|
||||
RUN go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||
|
||||
FROM alpine:latest as app
|
||||
|
||||
@ -17,7 +21,8 @@ RUN apk --no-cache add libc6-compat
|
||||
RUN apk --no-cache add chromium
|
||||
|
||||
RUN mkdir /app && mkdir /app/migrations
|
||||
COPY --from=build /app/server /app
|
||||
COPY ./internal/database/migrations/ /app/migrations
|
||||
COPY --from=build /app/collector /app
|
||||
COPY --from=build /go/bin/goose /app
|
||||
COPY ./database/migrations/ /app/migrations
|
||||
|
||||
CMD [ "/app/collector" ]
|
@ -3,9 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
_ "github.com/glebarez/go-sqlite"
|
||||
"github.com/pressly/goose/v3"
|
||||
@ -19,10 +17,6 @@ import (
|
||||
// @title NewsBot collector
|
||||
// @version 0.1
|
||||
// @BasePath /api
|
||||
// @securityDefinitions.apikey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Type "Bearer" followed by a space and JWT token.
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
@ -36,9 +30,14 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = migrateDatabase(db)
|
||||
err = goose.SetDialect("sqlite3")
|
||||
if err != nil {
|
||||
fmt.Print(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = goose.Up(db, "../internal/database/migrations")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c := cron.NewScheduler(ctx, db)
|
||||
@ -52,39 +51,3 @@ func main() {
|
||||
|
||||
server.Router.Start(":8081")
|
||||
}
|
||||
|
||||
func migrateDatabase(db *sql.DB) error {
|
||||
err := goose.SetDialect("sqlite3")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = goose.Up(db, "../internal/database/migrations")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = os.Stat("./migrations")
|
||||
if err == nil {
|
||||
|
||||
err = goose.Up(db, "../internal/database/migrations")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = os.Stat("../internal/database/migrations")
|
||||
if err == nil {
|
||||
|
||||
err = goose.Up(db, "../internal/database/migrations")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("failed to find the migration files")
|
||||
}
|
||||
|
569
docs/docs.go
569
docs/docs.go
@ -18,11 +18,6 @@ const docTemplate = `{
|
||||
"paths": {
|
||||
"/v1/articles": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -62,11 +57,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/articles/by/sourceid": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -113,11 +103,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/articles/{ID}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -158,11 +143,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/articles/{ID}/details": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -462,13 +442,48 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/sources": {
|
||||
"/v1/queue/discord/webhooks": {
|
||||
"get": {
|
||||
"security": [
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Queue"
|
||||
],
|
||||
"summary": "Returns the top 100 entries from the queue to be processed.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ListDiscordWebHooksQueueResults"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/settings/{key}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Settings"
|
||||
],
|
||||
"summary": "Returns a object based on the Key that was given.",
|
||||
"parameters": [
|
||||
{
|
||||
"Bearer": []
|
||||
"type": "string",
|
||||
"description": "Settings Key value",
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/v1/sources": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -502,11 +517,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/sources/by/source": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -553,11 +563,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/sources/by/sourceAndName": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -605,11 +610,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/sources/new/reddit": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -654,11 +654,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/sources/new/rss": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -703,11 +698,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/sources/new/twitch": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -726,11 +716,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/sources/new/youtube": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -756,11 +741,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/sources/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -799,11 +779,6 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -822,11 +797,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/sources/{id}/disable": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -864,11 +834,6 @@ const docTemplate = `{
|
||||
},
|
||||
"/v1/sources/{id}/enable": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -904,224 +869,165 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/login": {
|
||||
"post": {
|
||||
"/v1/subscriptions": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Logs into the API and returns a bearer token if successful",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "username",
|
||||
"in": "formData"
|
||||
}
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Returns the top 100 entries from the queue to be processed.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.LoginResponse"
|
||||
"$ref": "#/definitions/v1.ListSubscriptions"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"description": "Unable to reach SQL.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ApiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"description": "Failed to process data from SQL.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/refreshToken": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Generates a new token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "body",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.RefreshTokenRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.LoginResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/register": {
|
||||
"post": {
|
||||
"/v1/subscriptions/by/SourceId": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Creates a new user",
|
||||
"summary": "Returns the top 100 entries from the queue to be processed.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "username",
|
||||
"in": "formData"
|
||||
"description": "id",
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"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"
|
||||
"$ref": "#/definitions/v1.ListSubscriptions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/scopes/add": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"/v1/subscriptions/by/discordId": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Adds a new scope to a user account",
|
||||
"summary": "Returns the top 100 entries from the queue to be processed.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "body",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.UpdateScopesRequest"
|
||||
}
|
||||
"type": "string",
|
||||
"description": "id",
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ListSubscriptions"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"description": "Unable to reach SQL or Data problems",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ApiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"description": "Data problems",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/scopes/remove": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"/v1/subscriptions/details": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Adds a new scope to a user account",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "body",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.UpdateScopesRequest"
|
||||
}
|
||||
}
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Returns the top 50 entries with full deatils on the source and output.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ListSubscriptionDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"/v1/subscriptions/discord/webhook/delete": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Removes a Discord WebHook Subscription based on the Subscription ID.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "id",
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
}
|
||||
}
|
||||
"/v1/subscriptions/discord/webhook/new": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Creates a new subscription to link a post from a Source to a DiscordWebHook.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "discordWebHookId",
|
||||
"name": "discordWebHookId",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "sourceId",
|
||||
"name": "sourceId",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1243,34 +1149,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.LoginResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"refreshToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.RefreshTokenRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"refreshToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.SourceDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -1308,30 +1186,211 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.UpdateScopesRequest": {
|
||||
"models.ArticleDetailsDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"scopes"
|
||||
],
|
||||
"properties": {
|
||||
"scopes": {
|
||||
"authorImage": {
|
||||
"type": "string"
|
||||
},
|
||||
"authorName": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"pubdate": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/models.SourceDto"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"username": {
|
||||
"thumbnail": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"video": {
|
||||
"type": "string"
|
||||
},
|
||||
"videoHeight": {
|
||||
"type": "integer"
|
||||
},
|
||||
"videoWidth": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"Bearer": {
|
||||
"description": "Type \"Bearer\" followed by a space and JWT token.",
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
"models.DiscordQueueDetailsDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"article": {
|
||||
"$ref": "#/definitions/models.ArticleDetailsDto"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.DiscordWebHooksDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ID": {
|
||||
"type": "string"
|
||||
},
|
||||
"channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.SourceDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deleted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"site": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.SubscriptionDetailsDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"discordwebhook": {
|
||||
"$ref": "#/definitions/models.DiscordWebHooksDto"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/models.SourceDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.SubscriptionDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"discordwebhookid": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceid": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ApiError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ListDiscordWebHooksQueueResults": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.DiscordQueueDetailsDto"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ListSubscriptionDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.SubscriptionDetailsDto"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ListSubscriptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.SubscriptionDto"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
@ -9,11 +9,6 @@
|
||||
"paths": {
|
||||
"/v1/articles": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -53,11 +48,6 @@
|
||||
},
|
||||
"/v1/articles/by/sourceid": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -104,11 +94,6 @@
|
||||
},
|
||||
"/v1/articles/{ID}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -149,11 +134,6 @@
|
||||
},
|
||||
"/v1/articles/{ID}/details": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -453,13 +433,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/sources": {
|
||||
"/v1/queue/discord/webhooks": {
|
||||
"get": {
|
||||
"security": [
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Queue"
|
||||
],
|
||||
"summary": "Returns the top 100 entries from the queue to be processed.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ListDiscordWebHooksQueueResults"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/settings/{key}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Settings"
|
||||
],
|
||||
"summary": "Returns a object based on the Key that was given.",
|
||||
"parameters": [
|
||||
{
|
||||
"Bearer": []
|
||||
"type": "string",
|
||||
"description": "Settings Key value",
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/v1/sources": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -493,11 +508,6 @@
|
||||
},
|
||||
"/v1/sources/by/source": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -544,11 +554,6 @@
|
||||
},
|
||||
"/v1/sources/by/sourceAndName": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -596,11 +601,6 @@
|
||||
},
|
||||
"/v1/sources/new/reddit": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -645,11 +645,6 @@
|
||||
},
|
||||
"/v1/sources/new/rss": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -694,11 +689,6 @@
|
||||
},
|
||||
"/v1/sources/new/twitch": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -717,11 +707,6 @@
|
||||
},
|
||||
"/v1/sources/new/youtube": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -747,11 +732,6 @@
|
||||
},
|
||||
"/v1/sources/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -790,11 +770,6 @@
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -813,11 +788,6 @@
|
||||
},
|
||||
"/v1/sources/{id}/disable": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -855,11 +825,6 @@
|
||||
},
|
||||
"/v1/sources/{id}/enable": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Source"
|
||||
],
|
||||
@ -895,224 +860,165 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/login": {
|
||||
"post": {
|
||||
"/v1/subscriptions": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Logs into the API and returns a bearer token if successful",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "username",
|
||||
"in": "formData"
|
||||
}
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Returns the top 100 entries from the queue to be processed.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.LoginResponse"
|
||||
"$ref": "#/definitions/v1.ListSubscriptions"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"description": "Unable to reach SQL.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ApiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"description": "Failed to process data from SQL.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/refreshToken": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Generates a new token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "body",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.RefreshTokenRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.LoginResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/register": {
|
||||
"post": {
|
||||
"/v1/subscriptions/by/SourceId": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Creates a new user",
|
||||
"summary": "Returns the top 100 entries from the queue to be processed.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "username",
|
||||
"in": "formData"
|
||||
"description": "id",
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"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"
|
||||
"$ref": "#/definitions/v1.ListSubscriptions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/scopes/add": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"/v1/subscriptions/by/discordId": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Adds a new scope to a user account",
|
||||
"summary": "Returns the top 100 entries from the queue to be processed.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "body",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.UpdateScopesRequest"
|
||||
}
|
||||
"type": "string",
|
||||
"description": "id",
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ListSubscriptions"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"description": "Unable to reach SQL or Data problems",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ApiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"description": "Data problems",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users/scopes/remove": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"/v1/subscriptions/details": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Adds a new scope to a user account",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "body",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.UpdateScopesRequest"
|
||||
}
|
||||
}
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Returns the top 50 entries with full deatils on the source and output.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"$ref": "#/definitions/v1.ListSubscriptionDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
"/v1/subscriptions/discord/webhook/delete": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Removes a Discord WebHook Subscription based on the Subscription ID.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "id",
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BaseResponse"
|
||||
}
|
||||
}
|
||||
"/v1/subscriptions/discord/webhook/new": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Subscription"
|
||||
],
|
||||
"summary": "Creates a new subscription to link a post from a Source to a DiscordWebHook.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "discordWebHookId",
|
||||
"name": "discordWebHookId",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "sourceId",
|
||||
"name": "sourceId",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1234,34 +1140,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.LoginResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"refreshToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.RefreshTokenRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"refreshToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.SourceDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -1299,30 +1177,211 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.UpdateScopesRequest": {
|
||||
"models.ArticleDetailsDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"scopes"
|
||||
],
|
||||
"properties": {
|
||||
"scopes": {
|
||||
"authorImage": {
|
||||
"type": "string"
|
||||
},
|
||||
"authorName": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"pubdate": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/models.SourceDto"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"username": {
|
||||
"thumbnail": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"video": {
|
||||
"type": "string"
|
||||
},
|
||||
"videoHeight": {
|
||||
"type": "integer"
|
||||
},
|
||||
"videoWidth": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"Bearer": {
|
||||
"description": "Type \"Bearer\" followed by a space and JWT token.",
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
"models.DiscordQueueDetailsDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"article": {
|
||||
"$ref": "#/definitions/models.ArticleDetailsDto"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.DiscordWebHooksDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ID": {
|
||||
"type": "string"
|
||||
},
|
||||
"channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"server": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.SourceDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deleted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"site": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.SubscriptionDetailsDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"discordwebhook": {
|
||||
"$ref": "#/definitions/models.DiscordWebHooksDto"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/models.SourceDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.SubscriptionDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"discordwebhookid": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceid": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ApiError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ListDiscordWebHooksQueueResults": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.DiscordQueueDetailsDto"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ListSubscriptionDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.SubscriptionDetailsDto"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ListSubscriptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"payload": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.SubscriptionDto"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -78,24 +78,6 @@ definitions:
|
||||
$ref: '#/definitions/domain.DiscordWebHookDto'
|
||||
type: array
|
||||
type: object
|
||||
domain.LoginResponse:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
refreshToken:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
domain.RefreshTokenRequest:
|
||||
properties:
|
||||
refreshToken:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
domain.SourceDto:
|
||||
properties:
|
||||
enabled:
|
||||
@ -120,16 +102,139 @@ definitions:
|
||||
$ref: '#/definitions/domain.SourceDto'
|
||||
type: array
|
||||
type: object
|
||||
domain.UpdateScopesRequest:
|
||||
models.ArticleDetailsDto:
|
||||
properties:
|
||||
scopes:
|
||||
authorImage:
|
||||
type: string
|
||||
authorName:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
pubdate:
|
||||
type: string
|
||||
source:
|
||||
$ref: '#/definitions/models.SourceDto'
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
username:
|
||||
thumbnail:
|
||||
type: string
|
||||
required:
|
||||
- scopes
|
||||
title:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
video:
|
||||
type: string
|
||||
videoHeight:
|
||||
type: integer
|
||||
videoWidth:
|
||||
type: integer
|
||||
type: object
|
||||
models.DiscordQueueDetailsDto:
|
||||
properties:
|
||||
article:
|
||||
$ref: '#/definitions/models.ArticleDetailsDto'
|
||||
id:
|
||||
type: string
|
||||
type: object
|
||||
models.DiscordWebHooksDto:
|
||||
properties:
|
||||
ID:
|
||||
type: string
|
||||
channel:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
server:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
models.SourceDto:
|
||||
properties:
|
||||
deleted:
|
||||
type: boolean
|
||||
enabled:
|
||||
type: boolean
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
site:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
models.SubscriptionDetailsDto:
|
||||
properties:
|
||||
discordwebhook:
|
||||
$ref: '#/definitions/models.DiscordWebHooksDto'
|
||||
id:
|
||||
type: string
|
||||
source:
|
||||
$ref: '#/definitions/models.SourceDto'
|
||||
type: object
|
||||
models.SubscriptionDto:
|
||||
properties:
|
||||
discordwebhookid:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
sourceid:
|
||||
type: string
|
||||
type: object
|
||||
v1.ApiError:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
status:
|
||||
type: integer
|
||||
type: object
|
||||
v1.ListDiscordWebHooksQueueResults:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
payload:
|
||||
items:
|
||||
$ref: '#/definitions/models.DiscordQueueDetailsDto'
|
||||
type: array
|
||||
status:
|
||||
type: integer
|
||||
type: object
|
||||
v1.ListSubscriptionDetails:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
payload:
|
||||
items:
|
||||
$ref: '#/definitions/models.SubscriptionDetailsDto'
|
||||
type: array
|
||||
status:
|
||||
type: integer
|
||||
type: object
|
||||
v1.ListSubscriptions:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
payload:
|
||||
items:
|
||||
$ref: '#/definitions/models.SubscriptionDto'
|
||||
type: array
|
||||
status:
|
||||
type: integer
|
||||
type: object
|
||||
info:
|
||||
contact: {}
|
||||
@ -158,8 +263,6 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Lists the top 25 records ordering from newest to oldest.
|
||||
tags:
|
||||
- Articles
|
||||
@ -186,8 +289,6 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Returns an article based on defined ID.
|
||||
tags:
|
||||
- Articles
|
||||
@ -214,8 +315,6 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Returns an article and source based on defined ID.
|
||||
tags:
|
||||
- Articles
|
||||
@ -246,8 +345,6 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Finds the articles based on the SourceID provided. Returns the top
|
||||
25.
|
||||
tags:
|
||||
@ -423,6 +520,32 @@ paths:
|
||||
summary: Creates a new record for a discord web hook to post data to.
|
||||
tags:
|
||||
- DiscordWebhook
|
||||
/v1/queue/discord/webhooks:
|
||||
get:
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: ok
|
||||
schema:
|
||||
$ref: '#/definitions/v1.ListDiscordWebHooksQueueResults'
|
||||
summary: Returns the top 100 entries from the queue to be processed.
|
||||
tags:
|
||||
- Queue
|
||||
/v1/settings/{key}:
|
||||
get:
|
||||
parameters:
|
||||
- description: Settings Key value
|
||||
in: path
|
||||
name: key
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses: {}
|
||||
summary: Returns a object based on the Key that was given.
|
||||
tags:
|
||||
- Settings
|
||||
/v1/sources:
|
||||
get:
|
||||
parameters:
|
||||
@ -441,8 +564,6 @@ paths:
|
||||
description: Unable to reach SQL or Data problems
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Lists the top 50 records
|
||||
tags:
|
||||
- Source
|
||||
@ -469,8 +590,6 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Returns a single entity by ID
|
||||
tags:
|
||||
- Source
|
||||
@ -482,8 +601,6 @@ paths:
|
||||
required: true
|
||||
type: string
|
||||
responses: {}
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Marks a source as deleted based on its ID value.
|
||||
tags:
|
||||
- Source
|
||||
@ -508,8 +625,6 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Disables a source from processing.
|
||||
tags:
|
||||
- Source
|
||||
@ -534,8 +649,6 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Enables a source to continue processing.
|
||||
tags:
|
||||
- Source
|
||||
@ -566,8 +679,6 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 'Lists the top 50 records based on the name given. Example: reddit'
|
||||
tags:
|
||||
- Source
|
||||
@ -599,8 +710,6 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Returns a single entity by ID
|
||||
tags:
|
||||
- Source
|
||||
@ -630,8 +739,6 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Creates a new reddit source to monitor.
|
||||
tags:
|
||||
- Source
|
||||
@ -661,8 +768,6 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Creates a new rss source to monitor.
|
||||
tags:
|
||||
- Source
|
||||
@ -675,8 +780,6 @@ paths:
|
||||
required: true
|
||||
type: string
|
||||
responses: {}
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Creates a new twitch source to monitor.
|
||||
tags:
|
||||
- Source
|
||||
@ -694,154 +797,112 @@ paths:
|
||||
required: true
|
||||
type: string
|
||||
responses: {}
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Creates a new youtube source to monitor.
|
||||
tags:
|
||||
- Source
|
||||
/v1/users/login:
|
||||
post:
|
||||
parameters:
|
||||
- in: formData
|
||||
name: password
|
||||
type: string
|
||||
- in: formData
|
||||
name: username
|
||||
type: string
|
||||
/v1/subscriptions:
|
||||
get:
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
description: ok
|
||||
schema:
|
||||
$ref: '#/definitions/domain.LoginResponse'
|
||||
$ref: '#/definitions/v1.ListSubscriptions'
|
||||
"400":
|
||||
description: Bad Request
|
||||
description: Unable to reach SQL.
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
$ref: '#/definitions/v1.ApiError'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
description: Failed to process data from SQL.
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
summary: Logs into the API and returns a bearer token if successful
|
||||
$ref: '#/definitions/v1.ApiError'
|
||||
summary: Returns the top 100 entries from the queue to be processed.
|
||||
tags:
|
||||
- Users
|
||||
/v1/users/refreshToken:
|
||||
post:
|
||||
- Subscription
|
||||
/v1/subscriptions/by/SourceId:
|
||||
get:
|
||||
parameters:
|
||||
- description: body
|
||||
in: body
|
||||
name: request
|
||||
- description: id
|
||||
in: query
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/domain.RefreshTokenRequest'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.LoginResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Generates a new token
|
||||
tags:
|
||||
- Users
|
||||
/v1/users/register:
|
||||
post:
|
||||
parameters:
|
||||
- in: formData
|
||||
name: password
|
||||
type: string
|
||||
- in: formData
|
||||
name: username
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
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'
|
||||
summary: Creates a new user
|
||||
$ref: '#/definitions/v1.ListSubscriptions'
|
||||
summary: Returns the top 100 entries from the queue to be processed.
|
||||
tags:
|
||||
- Users
|
||||
/v1/users/scopes/add:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
- Subscription
|
||||
/v1/subscriptions/by/discordId:
|
||||
get:
|
||||
parameters:
|
||||
- description: body
|
||||
in: body
|
||||
name: request
|
||||
- description: id
|
||||
in: query
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/domain.UpdateScopesRequest'
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
description: ok
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
$ref: '#/definitions/v1.ListSubscriptions'
|
||||
"400":
|
||||
description: Bad Request
|
||||
description: Unable to reach SQL or Data problems
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
$ref: '#/definitions/v1.ApiError'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
description: Data problems
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BaseResponse'
|
||||
summary: Adds a new scope to a user account
|
||||
$ref: '#/definitions/v1.ApiError'
|
||||
summary: Returns the top 100 entries from the queue to be processed.
|
||||
tags:
|
||||
- Users
|
||||
/v1/users/scopes/remove:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: body
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/domain.UpdateScopesRequest'
|
||||
- Subscription
|
||||
/v1/subscriptions/details:
|
||||
get:
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
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'
|
||||
summary: Adds a new scope to a user account
|
||||
$ref: '#/definitions/v1.ListSubscriptionDetails'
|
||||
summary: Returns the top 50 entries with full deatils on the source and output.
|
||||
tags:
|
||||
- Users
|
||||
securityDefinitions:
|
||||
Bearer:
|
||||
description: Type "Bearer" followed by a space and JWT token.
|
||||
in: header
|
||||
name: Authorization
|
||||
type: apiKey
|
||||
- Subscription
|
||||
/v1/subscriptions/discord/webhook/delete:
|
||||
delete:
|
||||
parameters:
|
||||
- description: id
|
||||
in: query
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
responses: {}
|
||||
summary: Removes a Discord WebHook Subscription based on the Subscription ID.
|
||||
tags:
|
||||
- Subscription
|
||||
/v1/subscriptions/discord/webhook/new:
|
||||
post:
|
||||
parameters:
|
||||
- description: discordWebHookId
|
||||
in: query
|
||||
name: discordWebHookId
|
||||
required: true
|
||||
type: string
|
||||
- description: sourceId
|
||||
in: query
|
||||
name: sourceId
|
||||
required: true
|
||||
type: string
|
||||
responses: {}
|
||||
summary: Creates a new subscription to link a post from a Source to a DiscordWebHook.
|
||||
tags:
|
||||
- Subscription
|
||||
swagger: "2.0"
|
||||
|
@ -18,12 +18,23 @@ CREATE TABLE Articles (
|
||||
AuthorImageUrl TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE Table DiscordQueue (
|
||||
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
CreatedAt DATETIME NOT NULL,
|
||||
UpdatedAt DATETIME NOT NULL,
|
||||
DeletedAt DATETIME,
|
||||
ArticleId NUMBER NOT NULL,
|
||||
SourceId NUMBER NOT NULL
|
||||
);
|
||||
|
||||
CREATE Table DiscordWebHooks (
|
||||
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
CreatedAt DATETIME NOT NULL,
|
||||
UpdatedAt DATETIME NOT NULL,
|
||||
DeletedAt DATETIME NOT NULL,
|
||||
UserID INTEGER NOT NULL,
|
||||
--Name TEXT NOT NULL, -- Defines webhook purpose
|
||||
--Key TEXT,
|
||||
Url TEXT NOT NULL, -- Webhook Url
|
||||
Server TEXT NOT NULL, -- Defines the server its bound it. Used for reference
|
||||
Channel TEXT NOT NULL, -- Defines the channel its bound to. Used for reference
|
||||
@ -61,6 +72,18 @@ CREATE Table Sources (
|
||||
Tags TEXT NOT NULL
|
||||
);
|
||||
|
||||
/*
|
||||
CREATE TABLE Subscriptions (
|
||||
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
CreatedAt DATETIME NOT NULL,
|
||||
UpdatedAt DATETIME NOT NULL,
|
||||
DeletedAt DATETIME NOT NULL,
|
||||
DiscordWebHookID NUMBER NOT NULL,
|
||||
SourceID NUMBER NOT NULL,
|
||||
UserID NUMBER NOT NULL
|
||||
);
|
||||
*/
|
||||
|
||||
CREATE TABLE UserSourceSubscriptions (
|
||||
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
CreatedAt DATETIME NOT NULL,
|
||||
@ -84,7 +107,7 @@ CREATE TABLE Users (
|
||||
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
CreatedAt DATETIME NOT NULL,
|
||||
UpdatedAt DATETIME NOT NULL,
|
||||
DeletedAt DATETIME NOT NULL,
|
||||
DeletedAt DATETIME,
|
||||
Name TEXT NOT NULL,
|
||||
Hash TEXT NOT NULL,
|
||||
Scopes TEXT NOT NULL
|
||||
@ -94,7 +117,7 @@ CREATE TABLE RefreshTokens (
|
||||
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
CreatedAt DATETIME NOT NULL,
|
||||
UpdatedAt DATETIME NOT NULL,
|
||||
DeletedAt DATETIME NOT NULL,
|
||||
DeletedAt DATETIME,
|
||||
Username TEXT NOT NULL,
|
||||
Token TEXT NOT NULL
|
||||
);
|
||||
@ -104,12 +127,13 @@ CREATE TABLE RefreshTokens (
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE AlertDiscord;
|
||||
Drop Table Articles;
|
||||
Drop Table DiscordQueue;
|
||||
Drop Table DiscordWebHooks;
|
||||
Drop Table Icons;
|
||||
DROP TABLE RefreshTokens;
|
||||
Drop Table Settings;
|
||||
Drop Table Sources;
|
||||
DROP TABLE Subscriptions;
|
||||
DROP TABLE Users;
|
||||
DROP TABLE UserSourceSubscriptions;
|
||||
DROP TABLE RefreshTokens;
|
||||
-- +goose StatementEnd
|
||||
|
22
internal/domain/interfaces/source.go
Normal file
22
internal/domain/interfaces/source.go
Normal file
@ -0,0 +1,22 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/mmcdole/gofeed"
|
||||
)
|
||||
|
||||
type Sources interface {
|
||||
CheckSource() error
|
||||
PullFeed() (*gofeed.Feed, error)
|
||||
|
||||
GetBrowser() *rod.Browser
|
||||
GetPage(parser *rod.Browser, url string) *rod.Page
|
||||
|
||||
ExtractThumbnail(page *rod.Page) (string, error)
|
||||
ExtractPubDate(page *rod.Page) (string, error)
|
||||
ExtractDescription(page *rod.Page) (string, error)
|
||||
ExtractAuthor(page *rod.Page) (string, error)
|
||||
ExtractAuthorImage(page *rod.Page) (string, error)
|
||||
ExtractTags(page *rod.Page) (string, error)
|
||||
ExtractTitle(page *rod.Page) (string, error)
|
||||
}
|
129
internal/domain/models/dto.go
Normal file
129
internal/domain/models/dto.go
Normal file
@ -0,0 +1,129 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
|
||||
)
|
||||
|
||||
type ArticleDto struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Source uuid.UUID `json:"sourceid"`
|
||||
Tags []string `json:"tags"`
|
||||
Title string `json:"title"`
|
||||
Url string `json:"url"`
|
||||
Pubdate time.Time `json:"pubdate"`
|
||||
Video string `json:"video"`
|
||||
Videoheight int32 `json:"videoHeight"`
|
||||
Videowidth int32 `json:"videoWidth"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Description string `json:"description"`
|
||||
Authorname string `json:"authorName"`
|
||||
Authorimage string `json:"authorImage"`
|
||||
}
|
||||
|
||||
type ArticleDetailsDto struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Source SourceDto `json:"source"`
|
||||
Tags []string `json:"tags"`
|
||||
Title string `json:"title"`
|
||||
Url string `json:"url"`
|
||||
Pubdate time.Time `json:"pubdate"`
|
||||
Video string `json:"video"`
|
||||
Videoheight int32 `json:"videoHeight"`
|
||||
Videowidth int32 `json:"videoWidth"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Description string `json:"description"`
|
||||
Authorname string `json:"authorName"`
|
||||
Authorimage string `json:"authorImage"`
|
||||
}
|
||||
|
||||
type DiscordWebHooksDto struct {
|
||||
ID uuid.UUID `json:"ID"`
|
||||
Url string `json:"url"`
|
||||
Server string `json:"server"`
|
||||
Channel string `json:"channel"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func ConvertToDiscordWebhookDto(i database.Discordwebhook) DiscordWebHooksDto {
|
||||
return DiscordWebHooksDto{
|
||||
ID: i.ID,
|
||||
Url: i.Url,
|
||||
Server: i.Server,
|
||||
Channel: i.Channel,
|
||||
Enabled: i.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
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 database.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,
|
||||
}
|
||||
}
|
||||
|
||||
type DiscordQueueDto struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Articleid uuid.UUID `json:"articleId"`
|
||||
}
|
||||
|
||||
type DiscordQueueDetailsDto struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Article ArticleDetailsDto `json:"article"`
|
||||
}
|
||||
|
||||
type SubscriptionDto struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
DiscordWebhookId uuid.UUID `json:"discordwebhookid"`
|
||||
SourceId uuid.UUID `json:"sourceid"`
|
||||
}
|
||||
|
||||
func ConvertToSubscriptionDto(i database.Subscription) SubscriptionDto {
|
||||
c := SubscriptionDto{
|
||||
ID: i.ID,
|
||||
DiscordWebhookId: i.Discordwebhookid,
|
||||
SourceId: i.Sourceid,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type SubscriptionDetailsDto struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Source SourceDto `json:"source"`
|
||||
DiscordWebHook DiscordWebHooksDto `json:"discordwebhook"`
|
||||
}
|
||||
|
||||
func splitTags(t string) []string {
|
||||
items := strings.Split(t, ", ")
|
||||
return items
|
||||
}
|
@ -1,10 +1,5 @@
|
||||
package domain
|
||||
|
||||
type LoginFormRequest struct {
|
||||
Username string `form:"username"`
|
||||
Password string `form:"password"`
|
||||
}
|
||||
|
||||
type GetSourceBySourceAndNameParamRequest struct {
|
||||
Name string `query:"name"`
|
||||
Source string `query:"source"`
|
||||
@ -16,12 +11,3 @@ type NewSourceParamRequest struct {
|
||||
Tags string `query:"tags"`
|
||||
}
|
||||
|
||||
type RefreshTokenRequest struct {
|
||||
Username string `json:"username"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
||||
|
||||
type UpdateScopesRequest struct {
|
||||
Username string `json:"username"`
|
||||
Scopes []string `json:"scopes" validate:"required"`
|
||||
}
|
@ -5,13 +5,6 @@ type BaseResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
BaseResponse
|
||||
Token string `json:"token"`
|
||||
Type string `json:"type"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
||||
|
||||
type ArticleResponse struct {
|
||||
BaseResponse
|
||||
Payload []ArticleDto `json:"payload"`
|
||||
|
@ -2,13 +2,5 @@ package domain
|
||||
|
||||
const (
|
||||
ScopeAll = "newsbot:all"
|
||||
|
||||
ScopeArticleRead = "newsbot:article:read"
|
||||
ScopeArticleDisable = "newsbot:article:disable"
|
||||
|
||||
ScopeSourceRead = "newsbot:source:read"
|
||||
ScopeSourceCreate = "newsbot:source:create"
|
||||
|
||||
ScopeDiscordWebHookCreate = "newsbot:discordwebhook:create"
|
||||
ScopeDiscordWebhookRead = "newsbot:discordwebhook:read"
|
||||
ScopeRead = "newsbot:read"
|
||||
)
|
||||
|
@ -18,10 +18,7 @@ import (
|
||||
// @Success 200 {object} domain.ArticleResponse
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) listArticles(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeArticleRead)
|
||||
|
||||
resp := domain.ArticleResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
@ -51,9 +48,7 @@ func (s *Handler) listArticles(c echo.Context) error {
|
||||
// @Success 200 {object} domain.ArticleResponse "OK"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) getArticle(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeArticleRead)
|
||||
p := domain.ArticleResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
@ -87,14 +82,14 @@ func (s *Handler) getArticle(c echo.Context) error {
|
||||
// @Success 200 {object} domain.ArticleDetailedResponse "OK"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) getArticleDetails(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeArticleRead)
|
||||
p := domain.ArticleDetailedResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
},
|
||||
Payload: domain.ArticleAndSourceModel{},
|
||||
Payload: domain.ArticleAndSourceModel{
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(c.Param("ID"))
|
||||
@ -128,9 +123,7 @@ func (s *Handler) getArticleDetails(c echo.Context) error {
|
||||
// @Success 200 {object} domain.ArticleResponse "OK"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) ListArticlesBySourceId(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeArticleRead)
|
||||
p := domain.ArticleResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
|
@ -1,257 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrUserNotFound = "requested user does not exist"
|
||||
ErrUsernameAlreadyExists = "the requested username already exists"
|
||||
)
|
||||
|
||||
// @Summary Creates a new user
|
||||
// @Router /v1/users/register [post]
|
||||
// @Param request formData domain.LoginFormRequest true "form"
|
||||
// @Accepts x-www-form-urlencoded
|
||||
// @Produce json
|
||||
// @Tags Users
|
||||
// @Success 200 {object} domain.BaseResponse
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
func (h *Handler) AuthRegister(c echo.Context) error {
|
||||
username := c.FormValue("username")
|
||||
password := c.FormValue("password")
|
||||
|
||||
//username := c.QueryParam("username")
|
||||
exists, err := h.repo.Users.GetUser(c.Request().Context(), username)
|
||||
if err != nil {
|
||||
// if we have an err, validate that if its not user not found.
|
||||
// if the user is not found, we can use that name
|
||||
if err.Error() != repository.ErrUserNotFound {
|
||||
return h.WriteError(c, err, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
if exists.Username == username {
|
||||
return h.InternalServerErrorResponse(c, ErrUsernameAlreadyExists)
|
||||
}
|
||||
|
||||
//password := c.QueryParam("password")
|
||||
err = h.repo.Users.CheckPasswordForRequirements(password)
|
||||
if err != nil {
|
||||
return h.WriteError(c, err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
_, err = h.repo.Users.Create(c.Request().Context(), username, password, domain.ScopeArticleRead)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusCreated, domain.BaseResponse{
|
||||
Message: "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Logs into the API and returns a bearer token if successful
|
||||
// @Router /v1/users/login [post]
|
||||
// @Param request formData domain.LoginFormRequest true "form"
|
||||
// @Accepts x-www-form-urlencoded
|
||||
// @Produce json
|
||||
// @Tags Users
|
||||
// @Success 200 {object} domain.LoginResponse
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
func (h *Handler) AuthLogin(c echo.Context) error {
|
||||
username := c.FormValue("username")
|
||||
password := c.FormValue("password")
|
||||
|
||||
// Check to see if they are trying to login with the admin token
|
||||
if username == "" {
|
||||
return h.createAdminToken(c, password)
|
||||
}
|
||||
|
||||
// check if the user exists
|
||||
user, err := h.repo.Users.GetUser(c.Request().Context(), username)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
// make sure the hash matches
|
||||
err = h.repo.Users.DoesPasswordMatchHash(c.Request().Context(), username, password)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
// TODO think about moving this down some?
|
||||
expiresAt := time.Now().Add(time.Hour * 48)
|
||||
|
||||
jwt, err := h.generateJwtWithExp(username, user.Scopes, h.config.ServerAddress, user.ID, expiresAt)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
refresh, err := h.repo.RefreshTokens.Create(c.Request().Context(), username)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, domain.LoginResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: "OK",
|
||||
},
|
||||
Token: jwt,
|
||||
Type: "Bearer",
|
||||
RefreshToken: refresh,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) createAdminToken(c echo.Context, password string) error {
|
||||
// if the admin token is blank, then the admin wanted this disabled.
|
||||
// this will fail right away and not progress.
|
||||
if h.config.AdminSecret == "" {
|
||||
return h.InternalServerErrorResponse(c, ErrUserNotFound)
|
||||
}
|
||||
|
||||
if h.config.AdminSecret != password {
|
||||
return h.UnauthorizedResponse(c, ErrUserNotFound)
|
||||
}
|
||||
|
||||
token, err := h.generateJwt("admin", domain.ScopeAll, h.config.ServerAddress, -1)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, domain.LoginResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: "OK",
|
||||
},
|
||||
Token: token,
|
||||
Type: "Bearer",
|
||||
})
|
||||
}
|
||||
|
||||
// This will take collect some information about the requested refresh, validate and then return a new jwt token if approved.
|
||||
// Register
|
||||
// @Summary Generates a new token
|
||||
// @Router /v1/users/refreshToken [post]
|
||||
// @Param request body domain.RefreshTokenRequest true "body"
|
||||
// @Tags Users
|
||||
// @Success 200 {object} domain.LoginResponse
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (h *Handler) RefreshJwtToken(c echo.Context) error {
|
||||
// Check the context for the refresh token
|
||||
var request domain.RefreshTokenRequest
|
||||
err := (&echo.DefaultBinder{}).BindBody(c, &request)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
err = h.repo.RefreshTokens.IsRequestValid(c.Request().Context(), request.Username, request.RefreshToken)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
user, err := h.repo.Users.GetUser(c.Request().Context(), request.Username)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
jwt, err := h.generateJwtWithExp(request.Username, user.Scopes, h.config.ServerAddress, user.ID, time.Now().Add(time.Hour*48))
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
newRefreshToken, err := h.repo.RefreshTokens.Create(c.Request().Context(), request.Username)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, domain.LoginResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: "OK",
|
||||
},
|
||||
Token: jwt,
|
||||
Type: "Bearer",
|
||||
RefreshToken: newRefreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Adds a new scope to a user account
|
||||
// @Router /v1/users/scopes/add [post]
|
||||
// @Param request body domain.UpdateScopesRequest true "body"
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.BaseResponse
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
func (h *Handler) AddScopes(c echo.Context) error {
|
||||
token, err := h.getJwtTokenFromContext(c)
|
||||
if err != nil {
|
||||
return h.UnauthorizedResponse(c, err.Error())
|
||||
}
|
||||
|
||||
err = token.IsValid(domain.ScopeAll)
|
||||
if err != nil {
|
||||
return h.UnauthorizedResponse(c, err.Error())
|
||||
}
|
||||
|
||||
request := domain.UpdateScopesRequest{}
|
||||
err = (&echo.DefaultBinder{}).BindBody(c, &request)
|
||||
if err != nil {
|
||||
h.WriteError(c, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
err = h.repo.Users.AddScopes(c.Request().Context(), request.Username, request.Scopes)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, domain.BaseResponse{
|
||||
Message: "OK",
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Adds a new scope to a user account
|
||||
// @Router /v1/users/scopes/remove [post]
|
||||
// @Param request body domain.UpdateScopesRequest true "body"
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.BaseResponse
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
func (h *Handler) RemoveScopes(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)
|
||||
}
|
||||
|
||||
request := domain.UpdateScopesRequest{}
|
||||
err = (&echo.DefaultBinder{}).BindBody(c, &request)
|
||||
if err != nil {
|
||||
h.WriteError(c, err, http.StatusBadRequest)
|
||||
|
||||
}
|
||||
|
||||
err = h.repo.Users.RemoveScopes(c.Request().Context(), request.Username, request.Scopes)
|
||||
if err != nil {
|
||||
return h.InternalServerErrorResponse(c, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, domain.BaseResponse{
|
||||
Message: "OK",
|
||||
})
|
||||
}
|
@ -18,9 +18,7 @@ import (
|
||||
// @Success 200 {object} domain.DiscordWebhookResponse
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) ListDiscordWebHooks(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead)
|
||||
p := domain.DiscordWebhookResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
@ -44,9 +42,7 @@ func (s *Handler) ListDiscordWebHooks(c echo.Context) error {
|
||||
// @Success 200 {object} domain.DiscordWebhookResponse "OK"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) GetDiscordWebHooksById(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead)
|
||||
p := domain.DiscordWebhookResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
@ -78,9 +74,7 @@ func (s *Handler) GetDiscordWebHooksById(c echo.Context) error {
|
||||
// @Success 200 {object} domain.DiscordWebhookResponse "OK"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead)
|
||||
p := domain.DiscordWebhookResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
@ -116,9 +110,13 @@ func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error {
|
||||
// @Success 200 {object} domain.DiscordWebhookResponse "OK"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) NewDiscordWebHook(c echo.Context) error {
|
||||
token := s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate)
|
||||
token, err := s.getJwtToken(c)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusUnauthorized, domain.BaseResponse{
|
||||
Message: ErrJwtMissing,
|
||||
})
|
||||
}
|
||||
|
||||
_url := c.QueryParam("url")
|
||||
_server := c.QueryParam("server")
|
||||
@ -145,7 +143,7 @@ func (s *Handler) NewDiscordWebHook(c echo.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
user, err := s.repo.Users.GetUser(c.Request().Context(), token.UserName)
|
||||
user, err := s.repo.Users.GetByName(token.UserName)
|
||||
if err != nil {
|
||||
s.WriteMessage(c, ErrUserUnknown, http.StatusBadRequest)
|
||||
}
|
||||
@ -183,9 +181,7 @@ func (s *Handler) NewDiscordWebHook(c echo.Context) error {
|
||||
// @Success 200 {object} domain.DiscordWebhookResponse "OK"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) disableDiscordWebHook(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate)
|
||||
id, err := strconv.Atoi(c.Param("ID"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, domain.BaseResponse{
|
||||
@ -194,15 +190,11 @@ func (s *Handler) disableDiscordWebHook(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Check to make sure we can find the record
|
||||
record, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
|
||||
_, err = s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
|
||||
if err != nil {
|
||||
s.WriteError(c, err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if record.UserID != s.GetUserIdFromJwtToken(c) {
|
||||
s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// flip the it
|
||||
updated, err := s.repo.DiscordWebHooks.Disable(c.Request().Context(), int64(id))
|
||||
if err != nil {
|
||||
@ -234,24 +226,18 @@ func (s *Handler) disableDiscordWebHook(c echo.Context) error {
|
||||
// @Param id path int true "id"
|
||||
// @Tags DiscordWebhook
|
||||
// @Router /v1/discord/webhooks/{ID}/enable [post]
|
||||
// @Security Bearer
|
||||
func (s *Handler) enableDiscordWebHook(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate)
|
||||
id, err := strconv.Atoi(c.Param("ID"))
|
||||
if err != nil {
|
||||
s.WriteError(c, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Check to make sure we can find the record
|
||||
record, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
|
||||
_, err = s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
|
||||
if err != nil {
|
||||
s.WriteError(c, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if record.UserID != s.GetUserIdFromJwtToken(c) {
|
||||
s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
updated, err := s.repo.DiscordWebHooks.Enable(c.Request().Context(), int64(id))
|
||||
if err != nil {
|
||||
s.WriteError(c, err, http.StatusInternalServerError)
|
||||
@ -285,22 +271,17 @@ func (s *Handler) enableDiscordWebHook(c echo.Context) error {
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
func (s *Handler) deleteDiscordWebHook(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate)
|
||||
id, err := strconv.Atoi(c.Param("ID"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
// Check to make sure we can find the record
|
||||
record, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
|
||||
_, err = s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if record.UserID != s.GetUserIdFromJwtToken(c) {
|
||||
s.WriteMessage(c, ErrYouDontOwnTheRecord, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Soft delete the record
|
||||
updated, err := s.repo.DiscordWebHooks.SoftDelete(c.Request().Context(), int64(id))
|
||||
if err != nil {
|
||||
|
@ -3,7 +3,7 @@ package v1
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"errors"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
echojwt "github.com/labstack/echo-jwt/v4"
|
||||
@ -12,13 +12,14 @@ import (
|
||||
swagger "github.com/swaggo/echo-swagger"
|
||||
|
||||
_ "git.jamestombleson.com/jtom38/newsbot-api/docs"
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/services"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
Router *echo.Echo
|
||||
//Db *database.Queries
|
||||
Db *database.Queries
|
||||
config services.Configs
|
||||
repo services.RepositoryService
|
||||
}
|
||||
@ -27,13 +28,9 @@ const (
|
||||
ErrParameterIdMissing = "The requested parameter ID was not found."
|
||||
ErrParameterMissing = "The requested parameter was not found found:"
|
||||
ErrUnableToParseId = "Unable to parse the requested ID"
|
||||
|
||||
ErrRecordMissing = "The requested record was not found"
|
||||
ErrFailedToCreateRecord = "The record was not created due to a database problem"
|
||||
ErrFailedToUpdateRecord = "The requested record was not updated due to a database problem"
|
||||
|
||||
ErrUserUnknown = "User is unknown"
|
||||
ErrYouDontOwnTheRecord = "The record requested does not belong to you"
|
||||
|
||||
ResponseMessageSuccess = "Success"
|
||||
)
|
||||
@ -103,13 +100,14 @@ func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Han
|
||||
sources.POST("/:ID/disable", s.disableSource)
|
||||
sources.POST("/:ID/enable", s.enableSource)
|
||||
|
||||
users := v1.Group("/users")
|
||||
users.POST("/login", s.AuthLogin)
|
||||
users.POST("/register", s.AuthRegister)
|
||||
users.Use(echojwt.WithConfig(jwtConfig))
|
||||
users.POST("/scopes/add", s.AddScopes)
|
||||
users.POST("/scopes/remove", s.RemoveScopes)
|
||||
users.POST("/refreshToken", s.RefreshJwtToken)
|
||||
subs := v1.Group("/subscriptions")
|
||||
subs.Use(echojwt.WithConfig(jwtConfig))
|
||||
subs.GET("/", s.ListSubscriptions)
|
||||
subs.GET("/details", s.ListSubscriptionDetails)
|
||||
subs.GET("/by/discordId", s.GetSubscriptionsByDiscordId)
|
||||
subs.GET("/by/sourceId", s.GetSubscriptionsBySourceId)
|
||||
subs.POST("/discord/webhook/new", s.newDiscordWebHookSubscription)
|
||||
subs.DELETE("/discord/webhook/delete", s.DeleteDiscordWebHookSubscription)
|
||||
|
||||
s.Router = router
|
||||
return s
|
||||
@ -136,49 +134,18 @@ func (s *Handler) WriteMessage(c echo.Context, msg string, HttpStatusCode int) e
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Handler) InternalServerErrorResponse(c echo.Context, msg string) error {
|
||||
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{
|
||||
Message: msg,
|
||||
})
|
||||
func (h *Handler) getJwtToken(c echo.Context) (JwtToken, error) {
|
||||
// Make sure that the request came with a jwtToken
|
||||
token, ok := c.Get("user").(*jwt.Token)
|
||||
if !ok {
|
||||
return JwtToken{}, errors.New(ErrJwtMissing)
|
||||
}
|
||||
|
||||
func (s *Handler) UnauthorizedResponse(c echo.Context, msg string) error {
|
||||
return c.JSON(http.StatusUnauthorized, domain.BaseResponse{
|
||||
Message: msg,
|
||||
})
|
||||
// Generate the claims from the token
|
||||
claims, ok := token.Claims.(*JwtToken)
|
||||
if !ok {
|
||||
return JwtToken{}, errors.New(ErrJwtClaimsMissing)
|
||||
}
|
||||
|
||||
// If the token is not valid then an json error will be returned.
|
||||
// If the token has the wrong scope, a json error will be returned.
|
||||
// If the token passes all the checks, it is valid and is returned back to the caller.
|
||||
func (s *Handler) ValidateJwtToken(c echo.Context, requiredScope string) JwtToken {
|
||||
token, err := s.getJwtTokenFromContext(c)
|
||||
if err != nil {
|
||||
s.WriteMessage(c, ErrJwtMissing, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
err = token.hasScope(requiredScope)
|
||||
if err != nil {
|
||||
s.WriteMessage(c, ErrJwtScopeMissing, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
if token.Iss != s.config.ServerAddress {
|
||||
s.WriteMessage(c, ErrJwtInvalidIssuer, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
err = token.hasExpired()
|
||||
if err != nil {
|
||||
s.WriteMessage(c, ErrJwtExpired, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func (s *Handler) GetUserIdFromJwtToken(c echo.Context) int64 {
|
||||
token, err := s.getJwtTokenFromContext(c)
|
||||
if err != nil {
|
||||
s.WriteMessage(c, ErrJwtMissing, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
return token.GetUserId()
|
||||
return *claims, nil
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -15,7 +14,6 @@ const (
|
||||
ErrJwtClaimsMissing = "claims missing on token"
|
||||
ErrJwtExpired = "auth token has expired"
|
||||
ErrJwtScopeMissing = "required scope is missing"
|
||||
ErrJwtInvalidIssuer = "incorrect server issued the token"
|
||||
)
|
||||
|
||||
type JwtToken struct {
|
||||
@ -23,7 +21,6 @@ type JwtToken struct {
|
||||
Iss string `json:"iss"`
|
||||
Authorized bool `json:"authorized"`
|
||||
UserName string `json:"username"`
|
||||
UserId int64 `json:"userId"`
|
||||
Scopes []string `json:"scopes"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
@ -34,13 +31,6 @@ func (j JwtToken) IsValid(scope string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check to see if they have the scope to do anything
|
||||
// if they do, let them pass
|
||||
err = j.hasScope(domain.ScopeAll)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = j.hasScope(scope)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -53,10 +43,6 @@ func (j JwtToken) GetUsername() string {
|
||||
return j.UserName
|
||||
}
|
||||
|
||||
func (j JwtToken) GetUserId() int64 {
|
||||
return j.UserId
|
||||
}
|
||||
|
||||
func (j JwtToken) hasExpired() error {
|
||||
// Check to see if the token has expired
|
||||
hasExpired := j.Exp.Compare(time.Now())
|
||||
@ -66,27 +52,25 @@ func (j JwtToken) hasExpired() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// This will check the users token to make sure they have the correct scope to access the handler.
|
||||
// It will evaluate if you have the admin scope or the required scope for the handler.
|
||||
func (j JwtToken) hasScope(scope string) error {
|
||||
// they have the scope to access everything, so let them pass.
|
||||
userScopes := strings.Join(j.Scopes, "")
|
||||
if strings.Contains(domain.ScopeAll, userScopes) {
|
||||
if strings.Contains(domain.ScopeAll, scope) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(userScopes, scope) {
|
||||
for _, s := range j.Scopes {
|
||||
if strings.Contains(s, scope) {
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
return errors.New(ErrJwtScopeMissing)
|
||||
}
|
||||
|
||||
func (h *Handler) generateJwt(username, scopes, issuer string, userId int64) (string, error) {
|
||||
return h.generateJwtWithExp(username, scopes, issuer, userId, time.Now().Add(10*time.Minute))
|
||||
func (h *Handler) generateJwt(username, issuer string) (string, error) {
|
||||
return h.generateJwtWithExp(username, issuer, time.Now().Add(10*time.Minute))
|
||||
}
|
||||
|
||||
func (h *Handler) generateJwtWithExp(username, userScopes, issuer string, userId int64, expiresAt time.Time) (string, error) {
|
||||
func (h *Handler) generateJwtWithExp(username, issuer string, expiresAt time.Time) (string, error) {
|
||||
secret := []byte(h.config.JwtSecret)
|
||||
|
||||
// Anyone who wants to decrypt the key needs to use the same method
|
||||
@ -96,11 +80,15 @@ func (h *Handler) generateJwtWithExp(username, userScopes, issuer string, userId
|
||||
claims["authorized"] = true
|
||||
claims["username"] = username
|
||||
claims["iss"] = issuer
|
||||
claims["userId"] = userId
|
||||
|
||||
var scopes []string
|
||||
if username == "admin" {
|
||||
scopes = append(scopes, domain.ScopeAll)
|
||||
claims["scopes"] = scopes
|
||||
} else {
|
||||
scopes = append(scopes, domain.ScopeRead)
|
||||
claims["scopes"] = scopes
|
||||
}
|
||||
|
||||
tokenString, err := token.SignedString(secret)
|
||||
if err != nil {
|
||||
@ -109,19 +97,3 @@ func (h *Handler) generateJwtWithExp(username, userScopes, issuer string, userId
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func (h *Handler) getJwtTokenFromContext(c echo.Context) (JwtToken, error) {
|
||||
// Make sure that the request came with a jwtToken
|
||||
token, ok := c.Get("user").(*jwt.Token)
|
||||
if !ok {
|
||||
return JwtToken{}, errors.New(ErrJwtMissing)
|
||||
}
|
||||
|
||||
// Generate the claims from the token
|
||||
claims, ok := token.Claims.(*JwtToken)
|
||||
if !ok {
|
||||
return JwtToken{}, errors.New(ErrJwtClaimsMissing)
|
||||
}
|
||||
|
||||
return *claims, nil
|
||||
}
|
||||
|
39
internal/handler/v1/queue.go
Normal file
39
internal/handler/v1/queue.go
Normal file
@ -0,0 +1,39 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type ListDiscordWebHooksQueueResults struct {
|
||||
ApiStatusModel
|
||||
Payload []models.DiscordQueueDetailsDto `json:"payload"`
|
||||
}
|
||||
|
||||
// GetDiscordQueue
|
||||
// @Summary Returns the top 100 entries from the queue to be processed.
|
||||
// @Produce application/json
|
||||
// @Tags Queue
|
||||
// @Router /v1/queue/discord/webhooks [get]
|
||||
// @Success 200 {object} ListDiscordWebHooksQueueResults "ok"
|
||||
func (s *Handler) ListDiscordWebhookQueue(c echo.Context) error {
|
||||
p := ListDiscordWebHooksQueueResults{
|
||||
ApiStatusModel: ApiStatusModel{
|
||||
Message: "OK",
|
||||
StatusCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
// Get the raw resp from sql
|
||||
//res, err := s.dto.ListDiscordWebhookQueueDetails(c.Request().Context(), 50)
|
||||
//if err != nil {
|
||||
// return c.JSON(http.StatusInternalServerError, domain.BaseResponse{
|
||||
// Message: err.Error(),
|
||||
// })
|
||||
//}
|
||||
|
||||
//p.Payload = res
|
||||
return c.JSON(http.StatusOK, p)
|
||||
}
|
@ -1,13 +1,17 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/services"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
@ -19,9 +23,7 @@ import (
|
||||
// @Router /v1/sources [get]
|
||||
// @Success 200 {object} domain.SourcesResponse "ok"
|
||||
// @Failure 400 {object} domain.BaseResponse "Unable to reach SQL or Data problems"
|
||||
// @Security Bearer
|
||||
func (s *Handler) listSources(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeSourceRead)
|
||||
resp := domain.SourcesResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
@ -53,9 +55,7 @@ func (s *Handler) listSources(c echo.Context) error {
|
||||
// @Success 200 {object} domain.SourcesResponse "ok"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) listSourcesBySource(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeSourceRead)
|
||||
resp := domain.SourcesResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
@ -93,9 +93,7 @@ func (s *Handler) listSourcesBySource(c echo.Context) error {
|
||||
// @Success 200 {object} domain.SourcesResponse "ok"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) getSource(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeSourceRead)
|
||||
resp := domain.SourcesResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
@ -130,9 +128,7 @@ func (s *Handler) getSource(c echo.Context) error {
|
||||
// @Success 200 {object} domain.SourcesResponse "ok"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) GetSourceBySourceAndName(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeSourceRead)
|
||||
resp := domain.SourcesResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
@ -167,10 +163,7 @@ func (s *Handler) GetSourceBySourceAndName(c echo.Context) error {
|
||||
// @Success 200 {object} domain.SourcesResponse "ok"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) newRedditSource(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeSourceCreate)
|
||||
|
||||
resp := domain.SourcesResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
@ -180,13 +173,20 @@ func (s *Handler) newRedditSource(c echo.Context) error {
|
||||
var param domain.NewSourceParamRequest
|
||||
err := c.Bind(¶m)
|
||||
if err != nil {
|
||||
s.WriteError(c, err, http.StatusBadRequest)
|
||||
return c.JSON(http.StatusBadRequest, domain.BaseResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if param.Url == "" {
|
||||
s.WriteMessage(c, "url is missing", http.StatusBadRequest)
|
||||
return c.JSON(http.StatusBadRequest, domain.BaseResponse{
|
||||
Message: "Url is missing a value",
|
||||
})
|
||||
}
|
||||
if !strings.Contains(param.Url, "reddit.com") {
|
||||
s.WriteMessage(c, "invalid url", http.StatusBadRequest)
|
||||
return c.JSON(http.StatusBadRequest, domain.BaseResponse{
|
||||
Message: "Invalid URL given",
|
||||
})
|
||||
}
|
||||
|
||||
tags := fmt.Sprintf("twitch, %v, %s", param.Name, param.Tags)
|
||||
@ -216,67 +216,7 @@ func (s *Handler) newRedditSource(c echo.Context) error {
|
||||
// @Param url query string true "url"
|
||||
// @Tags Source
|
||||
// @Router /v1/sources/new/youtube [post]
|
||||
// @Security Bearer
|
||||
func (s *Handler) newYoutubeSource(c echo.Context) error {
|
||||
// Validate the jwt
|
||||
s.ValidateJwtToken(c, domain.ScopeSourceCreate)
|
||||
|
||||
var param domain.NewSourceParamRequest
|
||||
err := c.Bind(¶m)
|
||||
if err != nil {
|
||||
s.WriteError(c, err, http.StatusBadRequest)
|
||||
}
|
||||
if param.Url == "" {
|
||||
s.WriteMessage(c, "url is missing a value", http.StatusBadRequest)
|
||||
}
|
||||
if !strings.Contains(param.Url, "youtube.com") {
|
||||
s.WriteMessage(c, "invalid url", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
resp := domain.SourcesResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
},
|
||||
}
|
||||
|
||||
item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorYoutube, param.Name)
|
||||
if err == nil {
|
||||
var dto []domain.SourceDto
|
||||
dto = append(dto, services.SourceToDto(item))
|
||||
resp.Payload = dto
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
tags := fmt.Sprintf("twitch, %v", param.Name)
|
||||
rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorYoutube, param.Name, param.Url, tags, true)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if rows != 1 {
|
||||
s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
item, err = s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorYoutube, param.Name)
|
||||
if err == nil {
|
||||
var dto []domain.SourceDto
|
||||
dto = append(dto, services.SourceToDto(item))
|
||||
resp.Payload = dto
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// NewTwitchSource
|
||||
// @Summary Creates a new twitch source to monitor.
|
||||
// @Param name query string true "name"
|
||||
// @Tags Source
|
||||
// @Router /v1/sources/new/twitch [post]
|
||||
// @Security Bearer
|
||||
func (s *Handler) newTwitchSource(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeSourceCreate)
|
||||
|
||||
var param domain.NewSourceParamRequest
|
||||
err := c.Bind(¶m)
|
||||
if err != nil {
|
||||
@ -285,41 +225,100 @@ func (s *Handler) newTwitchSource(c echo.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
resp := domain.SourcesResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
},
|
||||
//query := r.URL.Query()
|
||||
//_name := query["name"][0]
|
||||
//_url := query["url"][0]
|
||||
////_tags := query["tags"][0]
|
||||
|
||||
if param.Url == "" {
|
||||
return c.JSON(http.StatusBadRequest, domain.BaseResponse{
|
||||
Message: "url is missing a value",
|
||||
})
|
||||
}
|
||||
if !strings.Contains(param.Url, "youtube.com") {
|
||||
return c.JSON(http.StatusBadRequest, domain.BaseResponse{
|
||||
Message: "Invalid URL",
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
if _tags == "" {
|
||||
tags = fmt.Sprintf("twitch, %v", _name)
|
||||
} else {
|
||||
}
|
||||
*/
|
||||
tags := fmt.Sprintf("twitch, %v", param.Name)
|
||||
url := fmt.Sprintf("https://twitch.tv/%v", param.Name)
|
||||
|
||||
// Check if the record already exists
|
||||
item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorTwitch, param.Name)
|
||||
if err == nil {
|
||||
var dto []domain.SourceDto
|
||||
dto = append(dto, services.SourceToDto(item))
|
||||
resp.Payload = dto
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
params := database.CreateSourceParams{
|
||||
ID: uuid.New(),
|
||||
Site: "youtube",
|
||||
Name: param.Name,
|
||||
Source: "youtube",
|
||||
Type: "feed",
|
||||
Enabled: true,
|
||||
Url: param.Url,
|
||||
Tags: tags,
|
||||
}
|
||||
err = s.Db.CreateSource(context.Background(), params)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorTwitch, param.Name, url, tags, true)
|
||||
bJson, err := json.Marshal(¶ms)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if rows != 1 {
|
||||
s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError)
|
||||
return c.JSON(http.StatusOK, bJson)
|
||||
}
|
||||
|
||||
item, err = s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorTwitch, param.Name)
|
||||
var dto []domain.SourceDto
|
||||
dto = append(dto, services.SourceToDto(item))
|
||||
resp.Payload = dto
|
||||
// NewTwitchSource
|
||||
// @Summary Creates a new twitch source to monitor.
|
||||
// @Param name query string true "name"
|
||||
// @Tags Source
|
||||
// @Router /v1/sources/new/twitch [post]
|
||||
func (s *Handler) newTwitchSource(c echo.Context) error {
|
||||
var param domain.NewSourceParamRequest
|
||||
err := c.Bind(¶m)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, domain.BaseResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
//query := r.URL.Query()
|
||||
//_name := query["name"][0]
|
||||
|
||||
tags := fmt.Sprintf("twitch, %v", param.Name)
|
||||
_url := fmt.Sprintf("https://twitch.tv/%v", param.Name)
|
||||
|
||||
params := database.CreateSourceParams{
|
||||
ID: uuid.New(),
|
||||
Site: "twitch",
|
||||
Name: param.Name,
|
||||
Source: "twitch",
|
||||
Type: "api",
|
||||
Enabled: true,
|
||||
Url: _url,
|
||||
Tags: tags,
|
||||
}
|
||||
err = s.Db.CreateSource(c.Request().Context(), params)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
bJson, err := json.Marshal(¶ms)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, bJson)
|
||||
}
|
||||
|
||||
// NewRssSource
|
||||
@ -331,10 +330,7 @@ func (s *Handler) newTwitchSource(c echo.Context) error {
|
||||
// @Success 200 {object} domain.SourcesResponse "ok"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) newRssSource(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeSourceCreate)
|
||||
|
||||
resp := domain.SourcesResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: ResponseMessageSuccess,
|
||||
@ -378,52 +374,49 @@ func (s *Handler) newRssSource(c echo.Context) error {
|
||||
|
||||
// DeleteSource
|
||||
// @Summary Marks a source as deleted based on its ID value.
|
||||
// @Param id path int true "id"
|
||||
// @Param id path string true "id"
|
||||
// @Tags Source
|
||||
// @Router /v1/sources/{id} [POST]
|
||||
// @Success 200 {object} domain.SourcesResponse "ok"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) deleteSources(c echo.Context) error {
|
||||
s.ValidateJwtToken(c, domain.ScopeAll)
|
||||
id, err := strconv.Atoi(c.Param("ID"))
|
||||
id := c.Param("ID")
|
||||
uuid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
s.WriteError(c, err, http.StatusBadRequest)
|
||||
return c.JSON(http.StatusBadRequest, domain.BaseResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Check to make sure we can find the record
|
||||
_, err = s.repo.Sources.GetById(c.Request().Context(), int64(id))
|
||||
_, err = s.Db.GetSourceByID(c.Request().Context(), uuid)
|
||||
if err != nil {
|
||||
s.WriteError(c, err, http.StatusInternalServerError)
|
||||
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Delete the record
|
||||
rows, err := s.repo.Sources.SoftDelete(c.Request().Context(), int64(id))
|
||||
err = s.Db.DeleteSource(c.Request().Context(), uuid)
|
||||
if err != nil {
|
||||
s.WriteError(c, err, http.StatusInternalServerError)
|
||||
}
|
||||
if rows != 1 {
|
||||
s.WriteMessage(c, ErrFailedToUpdateRecord, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// pull the record with its updated value
|
||||
item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id))
|
||||
if err != nil {
|
||||
s.WriteError(c, err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
var items []domain.SourceDto
|
||||
items = append(items, services.SourceToDto(item))
|
||||
|
||||
return c.JSON(http.StatusOK, domain.SourcesResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
Message: "OK",
|
||||
},
|
||||
Payload: items,
|
||||
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
p := ApiStatusModel{
|
||||
Message: "OK",
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, b)
|
||||
}
|
||||
|
||||
// DisableSource
|
||||
// @Summary Disables a source from processing.
|
||||
// @Param id path int true "id"
|
||||
@ -432,7 +425,6 @@ func (s *Handler) deleteSources(c echo.Context) error {
|
||||
// @Success 200 {object} domain.SourcesResponse "ok"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) disableSource(c echo.Context) error {
|
||||
resp := domain.SourcesResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
@ -475,7 +467,6 @@ func (s *Handler) disableSource(c echo.Context) error {
|
||||
// @Success 200 {object} domain.SourcesResponse "ok"
|
||||
// @Failure 400 {object} domain.BaseResponse
|
||||
// @Failure 500 {object} domain.BaseResponse
|
||||
// @Security Bearer
|
||||
func (s *Handler) enableSource(c echo.Context) error {
|
||||
resp := domain.SourcesResponse{
|
||||
BaseResponse: domain.BaseResponse{
|
||||
|
225
internal/handler/v1/subscriptions.go
Normal file
225
internal/handler/v1/subscriptions.go
Normal file
@ -0,0 +1,225 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type ListSubscriptions struct {
|
||||
ApiStatusModel
|
||||
Payload []models.SubscriptionDto `json:"payload"`
|
||||
}
|
||||
|
||||
type GetSubscription struct {
|
||||
ApiStatusModel
|
||||
Payload models.SubscriptionDto `json:"payload"`
|
||||
}
|
||||
|
||||
type ListSubscriptionDetails struct {
|
||||
ApiStatusModel
|
||||
Payload []models.SubscriptionDetailsDto `json:"payload"`
|
||||
}
|
||||
|
||||
// GetSubscriptions
|
||||
// @Summary Returns the top 100 entries from the queue to be processed.
|
||||
// @Produce application/json
|
||||
// @Tags Subscription
|
||||
// @Router /v1/subscriptions [get]
|
||||
// @Success 200 {object} ListSubscriptions "ok"
|
||||
// @Failure 400 {object} ApiError "Unable to reach SQL."
|
||||
// @Failure 500 {object} ApiError "Failed to process data from SQL."
|
||||
func (s *Handler) ListSubscriptions(c echo.Context) error {
|
||||
payload := ListSubscriptions{
|
||||
ApiStatusModel: ApiStatusModel{
|
||||
StatusCode: http.StatusOK,
|
||||
Message: "OK",
|
||||
},
|
||||
}
|
||||
|
||||
//res, err := s.dto.ListSubscriptions(c.Request().Context(), 50)
|
||||
//if err != nil {
|
||||
// return s.WriteError(c, err, http.StatusBadRequest)
|
||||
//}
|
||||
//payload.Payload = res
|
||||
return c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
// ListSubscriptionDetails
|
||||
// @Summary Returns the top 50 entries with full deatils on the source and output.
|
||||
// @Produce application/json
|
||||
// @Tags Subscription
|
||||
// @Router /v1/subscriptions/details [get]
|
||||
// @Success 200 {object} ListSubscriptionDetails "ok"
|
||||
func (s *Handler) ListSubscriptionDetails(c echo.Context) error {
|
||||
payload := ListSubscriptionDetails{
|
||||
ApiStatusModel: ApiStatusModel{
|
||||
StatusCode: http.StatusOK,
|
||||
Message: "OK",
|
||||
},
|
||||
}
|
||||
|
||||
//res, err := s.dto.ListSubscriptionDetails(c.Request().Context(), 50)
|
||||
//if err != nil {
|
||||
// return s.WriteError(c, err, http.StatusInternalServerError)
|
||||
//}
|
||||
//payload.Payload = res
|
||||
return c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
// GetSubscriptionsByDiscordId
|
||||
// @Summary Returns the top 100 entries from the queue to be processed.
|
||||
// @Produce application/json
|
||||
// @Param id query string true "id"
|
||||
// @Tags Subscription
|
||||
// @Router /v1/subscriptions/by/discordId [get]
|
||||
// @Success 200 {object} ListSubscriptions "ok"
|
||||
// @Failure 400 {object} ApiError "Unable to reach SQL or Data problems"
|
||||
// @Failure 500 {object} ApiError "Data problems"
|
||||
func (s *Handler) GetSubscriptionsByDiscordId(c echo.Context) error {
|
||||
p := ListSubscriptions{
|
||||
ApiStatusModel: ApiStatusModel{
|
||||
StatusCode: http.StatusOK,
|
||||
Message: "OK",
|
||||
},
|
||||
}
|
||||
|
||||
id := c.QueryParam("id")
|
||||
if id == "" {
|
||||
return s.WriteError(c, errors.New(ErrIdValueMissing), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
//uuid, err := uuid.Parse(id)
|
||||
//if err != nil {
|
||||
// return s.WriteError(c, errors.New(ErrValueNotUuid), http.StatusBadRequest)
|
||||
//}
|
||||
|
||||
//res, err := s.dto.ListSubscriptionsByDiscordWebhookId(context.Background(), uuid)
|
||||
//if err != nil {
|
||||
// return s.WriteError(c, err, http.StatusNoContent)
|
||||
//}
|
||||
//p.Payload = res
|
||||
return c.JSON(http.StatusOK, p)
|
||||
}
|
||||
|
||||
// GetSubscriptionsBySourceId
|
||||
// @Summary Returns the top 100 entries from the queue to be processed.
|
||||
// @Produce application/json
|
||||
// @Param id query string true "id"
|
||||
// @Tags Subscription
|
||||
// @Router /v1/subscriptions/by/SourceId [get]
|
||||
// @Success 200 {object} ListSubscriptions "ok"
|
||||
func (s *Handler) GetSubscriptionsBySourceId(c echo.Context) error {
|
||||
p := ListSubscriptions{
|
||||
ApiStatusModel: ApiStatusModel{
|
||||
StatusCode: http.StatusOK,
|
||||
Message: "OK",
|
||||
},
|
||||
}
|
||||
|
||||
_id := c.QueryParam("id")
|
||||
if _id == "" {
|
||||
return s.WriteError(c, errors.New(ErrIdValueMissing), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
//uuid, err := uuid.Parse(_id)
|
||||
//if err != nil {
|
||||
// return s.WriteError(c, err, http.StatusBadRequest)
|
||||
//}
|
||||
|
||||
//res, err := s.dto.ListSubscriptionsBySourceId(context.Background(), uuid)
|
||||
//if err != nil {
|
||||
// return s.WriteError(c, err, http.StatusNoContent)
|
||||
//}
|
||||
//p.Payload = res
|
||||
return c.JSON(http.StatusOK, p)
|
||||
}
|
||||
|
||||
// NewDiscordWebHookSubscription
|
||||
// @Summary Creates a new subscription to link a post from a Source to a DiscordWebHook.
|
||||
// @Param discordWebHookId query string true "discordWebHookId"
|
||||
// @Param sourceId query string true "sourceId"
|
||||
// @Tags Subscription
|
||||
// @Router /v1/subscriptions/discord/webhook/new [post]
|
||||
func (s *Handler) newDiscordWebHookSubscription(c echo.Context) error {
|
||||
// Extract the values given
|
||||
discordWebHookId := c.QueryParam("discordWebHookId")
|
||||
sourceId := c.QueryParam("sourceId")
|
||||
|
||||
// Check to make we didn't get a null
|
||||
if discordWebHookId == "" {
|
||||
return s.WriteError(c, errors.New("invalid discordWebHooksId given"), http.StatusBadRequest)
|
||||
}
|
||||
if sourceId == "" {
|
||||
return s.WriteError(c, errors.New("invalid sourceID given"), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Validate they are UUID values
|
||||
uHook, err := uuid.Parse(discordWebHookId)
|
||||
if err != nil {
|
||||
return s.WriteError(c, err, http.StatusBadRequest)
|
||||
}
|
||||
uSource, err := uuid.Parse(sourceId)
|
||||
if err != nil {
|
||||
return s.WriteError(c, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Check if the sub already exists
|
||||
_, err = s.Db.QuerySubscriptions(c.Request().Context(), database.QuerySubscriptionsParams{
|
||||
Discordwebhookid: uHook,
|
||||
Sourceid: uSource,
|
||||
})
|
||||
if err == nil {
|
||||
return s.WriteError(c, errors.New("a subscription already exists between these two entities"), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Does not exist, so make it.
|
||||
params := database.CreateSubscriptionParams{
|
||||
ID: uuid.New(),
|
||||
Discordwebhookid: uHook,
|
||||
Sourceid: uSource,
|
||||
}
|
||||
err = s.Db.CreateSubscription(context.Background(), params)
|
||||
if err != nil {
|
||||
return s.WriteError(c, err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
bJson, err := json.Marshal(¶ms)
|
||||
if err != nil {
|
||||
return s.WriteError(c, err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, bJson)
|
||||
}
|
||||
|
||||
// DeleteDiscordWebHookSubscription
|
||||
// @Summary Removes a Discord WebHook Subscription based on the Subscription ID.
|
||||
// @Param id query string true "id"
|
||||
// @Tags Subscription
|
||||
// @Router /v1/subscriptions/discord/webhook/delete [delete]
|
||||
func (s *Handler) DeleteDiscordWebHookSubscription(c echo.Context) error {
|
||||
var ErrMissingSubscriptionID string = "the request was missing a 'Id'"
|
||||
|
||||
id := c.QueryParam("id")
|
||||
if id == "" {
|
||||
return s.WriteError(c, errors.New(ErrMissingSubscriptionID), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
uid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return s.WriteError(c, err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
err = s.Db.DeleteSubscription(context.Background(), uid)
|
||||
if err != nil {
|
||||
return s.WriteError(c, err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, nil)
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -16,9 +15,9 @@ const (
|
||||
)
|
||||
|
||||
type RefreshToken interface {
|
||||
Create(ctx context.Context, username string, token string) (int64, error)
|
||||
GetByUsername(ctx context.Context, name string) (domain.RefreshTokenEntity, error)
|
||||
DeleteById(ctx context.Context, id int64) (int64, error)
|
||||
Create(username string, token string) (int64, error)
|
||||
GetByUsername(name string) (domain.RefreshTokenEntity, error)
|
||||
DeleteById(id int64) (int64, error)
|
||||
}
|
||||
|
||||
type RefreshTokenRepository struct {
|
||||
@ -31,15 +30,15 @@ func NewRefreshTokenRepository(conn *sql.DB) RefreshTokenRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (rt RefreshTokenRepository) Create(ctx context.Context, username string, token string) (int64, error) {
|
||||
func (rt RefreshTokenRepository) Create(username string, token string) (int64, error) {
|
||||
dt := time.Now()
|
||||
builder := sqlbuilder.NewInsertBuilder()
|
||||
builder.InsertInto(refreshTokenTableName)
|
||||
builder.Cols("Username", "Token", "CreatedAt", "UpdatedAt", "DeletedAt")
|
||||
builder.Values(username, token, dt, dt, time.Time{})
|
||||
builder.Cols("Username", "Token", "CreatedAt", "UpdatedAt")
|
||||
builder.Values(username, token, dt, dt)
|
||||
query, args := builder.Build()
|
||||
|
||||
_, err := rt.connection.ExecContext(ctx, query, args...)
|
||||
_, err := rt.connection.Exec(query, args...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -47,14 +46,14 @@ func (rt RefreshTokenRepository) Create(ctx context.Context, username string, to
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (rt RefreshTokenRepository) GetByUsername(ctx context.Context, name string) (domain.RefreshTokenEntity, error) {
|
||||
func (rt RefreshTokenRepository) GetByUsername(name string) (domain.RefreshTokenEntity, error) {
|
||||
builder := sqlbuilder.NewSelectBuilder()
|
||||
builder.Select("*").From(refreshTokenTableName).Where(
|
||||
builder.E("Username", name),
|
||||
)
|
||||
|
||||
query, args := builder.Build()
|
||||
rows, err := rt.connection.QueryContext(ctx, query, args...)
|
||||
rows, err := rt.connection.Query(query, args...)
|
||||
if err != nil {
|
||||
return domain.RefreshTokenEntity{}, err
|
||||
}
|
||||
@ -67,7 +66,7 @@ func (rt RefreshTokenRepository) GetByUsername(ctx context.Context, name string)
|
||||
return data[0], nil
|
||||
}
|
||||
|
||||
func (rt RefreshTokenRepository) DeleteById(ctx context.Context, id int64) (int64, error) {
|
||||
func (rt RefreshTokenRepository) DeleteById(id int64) (int64, error) {
|
||||
builder := sqlbuilder.NewDeleteBuilder()
|
||||
builder.DeleteFrom(refreshTokenTableName)
|
||||
builder.Where(
|
||||
@ -75,7 +74,7 @@ func (rt RefreshTokenRepository) DeleteById(ctx context.Context, id int64) (int6
|
||||
)
|
||||
|
||||
query, args := builder.Build()
|
||||
rows, err := rt.connection.ExecContext(ctx, query, args...)
|
||||
rows, err := rt.connection.Exec(query, args...)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
|
||||
@ -15,7 +14,7 @@ func TestRefreshTokenCreate(t *testing.T) {
|
||||
}
|
||||
|
||||
client := repository.NewRefreshTokenRepository(conn)
|
||||
rows, err := client.Create(context.Background(), "tester", "BadTokenDontUse")
|
||||
rows, err := client.Create("tester", "BadTokenDontUse")
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
@ -34,7 +33,7 @@ func TestRefreshTokenGetByUsername(t *testing.T) {
|
||||
}
|
||||
|
||||
client := repository.NewRefreshTokenRepository(conn)
|
||||
rows, err := client.Create(context.Background(), "tester", "BadTokenDoNotUse")
|
||||
rows, err := client.Create("tester", "BadTokenDoNotUse")
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
@ -45,7 +44,7 @@ func TestRefreshTokenGetByUsername(t *testing.T) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
model, err := client.GetByUsername(context.Background(), "tester")
|
||||
model, err := client.GetByUsername("tester")
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
@ -65,7 +64,7 @@ func TestRefreshTokenDeleteById(t *testing.T) {
|
||||
}
|
||||
|
||||
client := repository.NewRefreshTokenRepository(conn)
|
||||
created, err := client.Create(context.Background(), "tester", "BadTokenDoNotUse")
|
||||
created, err := client.Create("tester", "BadTokenDoNotUse")
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
@ -74,13 +73,13 @@ func TestRefreshTokenDeleteById(t *testing.T) {
|
||||
t.Log("Unexpected number back for rows created")
|
||||
}
|
||||
|
||||
model, err := client.GetByUsername(context.Background(), "tester")
|
||||
model, err := client.GetByUsername("tester")
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
updated, err := client.DeleteById(context.Background(), model.ID)
|
||||
updated, err := client.DeleteById(model.ID)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
|
@ -1,7 +1,6 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -19,12 +18,12 @@ const (
|
||||
)
|
||||
|
||||
type Users interface {
|
||||
GetByName(ctx context.Context, name string) (domain.UserEntity, error)
|
||||
Create(ctx context.Context, name, password, scope string) (int64, error)
|
||||
Update(ctx context.Context, id int, entity domain.UserEntity) error
|
||||
UpdatePassword(ctx context.Context, name, password string) error
|
||||
CheckUserHash(ctx context.Context, name, password string) error
|
||||
UpdateScopes(ctx context.Context, name, scope string) error
|
||||
GetByName(name string) (domain.UserEntity, error)
|
||||
Create(name, password, scope string) (int64, error)
|
||||
Update(id int, entity domain.UserEntity) error
|
||||
UpdatePassword(name, password string) error
|
||||
CheckUserHash(name, password string) error
|
||||
UpdateScopes(name, scope string) error
|
||||
}
|
||||
|
||||
// Creates a new instance of UserRepository with the bound sql
|
||||
@ -38,14 +37,14 @@ type userRepository struct {
|
||||
connection *sql.DB
|
||||
}
|
||||
|
||||
func (ur userRepository) GetByName(ctx context.Context, name string) (domain.UserEntity, error) {
|
||||
func (ur userRepository) GetByName(name string) (domain.UserEntity, error) {
|
||||
builder := sqlbuilder.NewSelectBuilder()
|
||||
builder.Select("*").From("users").Where(
|
||||
builder.E("Name", name),
|
||||
)
|
||||
query, args := builder.Build()
|
||||
|
||||
rows, err := ur.connection.QueryContext(ctx, query, args...)
|
||||
rows, err := ur.connection.Query(query, args...)
|
||||
if err != nil {
|
||||
return domain.UserEntity{}, err
|
||||
}
|
||||
@ -58,7 +57,7 @@ func (ur userRepository) GetByName(ctx context.Context, name string) (domain.Use
|
||||
return data[0], nil
|
||||
}
|
||||
|
||||
func (ur userRepository) Create(ctx context.Context,name, password, scope string) (int64, error) {
|
||||
func (ur userRepository) Create(name, password, scope string) (int64, error) {
|
||||
passwordBytes := []byte(password)
|
||||
hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@ -68,11 +67,11 @@ func (ur userRepository) Create(ctx context.Context,name, password, scope string
|
||||
dt := time.Now()
|
||||
queryBuilder := sqlbuilder.NewInsertBuilder()
|
||||
queryBuilder.InsertInto("users")
|
||||
queryBuilder.Cols("Name", "Hash", "UpdatedAt", "CreatedAt", "DeletedAt", "Scopes")
|
||||
queryBuilder.Values(name, string(hash), dt, dt, time.Time{}, scope)
|
||||
queryBuilder.Cols("Name", "Hash", "UpdatedAt", "CreatedAt", "Scopes")
|
||||
queryBuilder.Values(name, string(hash), dt, dt, scope)
|
||||
query, args := queryBuilder.Build()
|
||||
|
||||
_, err = ur.connection.ExecContext(ctx, query, args...)
|
||||
_, err = ur.connection.Exec(query, args...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -80,12 +79,12 @@ func (ur userRepository) Create(ctx context.Context,name, password, scope string
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (ur userRepository) Update(ctx context.Context, id int, entity domain.UserEntity) error {
|
||||
func (ur userRepository) Update(id int, entity domain.UserEntity) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (ur userRepository) UpdatePassword(ctx context.Context, name, password string) error {
|
||||
_, err := ur.GetByName(ctx, name)
|
||||
func (ur userRepository) UpdatePassword(name, password string) error {
|
||||
_, err := ur.GetByName(name)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@ -98,8 +97,8 @@ func (ur userRepository) UpdatePassword(ctx context.Context, name, password stri
|
||||
|
||||
// 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
|
||||
func (ur userRepository) CheckUserHash(ctx context.Context,name, password string) error {
|
||||
record, err := ur.GetByName(ctx, name)
|
||||
func (ur userRepository) CheckUserHash(name, password string) error {
|
||||
record, err := ur.GetByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -112,7 +111,7 @@ func (ur userRepository) CheckUserHash(ctx context.Context,name, password string
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ur userRepository) UpdateScopes(ctx context.Context,name, scope string) error {
|
||||
func (ur userRepository) UpdateScopes(name, scope string) error {
|
||||
builder := sqlbuilder.NewUpdateBuilder()
|
||||
builder.Update("users")
|
||||
builder.Set(
|
||||
@ -123,7 +122,7 @@ func (ur userRepository) UpdateScopes(ctx context.Context,name, scope string) er
|
||||
)
|
||||
query, args := builder.Build()
|
||||
|
||||
_, err := ur.connection.ExecContext(ctx, query, args...)
|
||||
_, err := ur.connection.Exec(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log"
|
||||
"testing"
|
||||
@ -21,7 +20,7 @@ func TestCanCreateNewUser(t *testing.T) {
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewUserRepository(db)
|
||||
updated, err := repo.Create(context.Background(), "testing", "NotSecure", "placeholder")
|
||||
updated, err := repo.Create("testing", "NotSecure", "placeholder")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
t.FailNow()
|
||||
@ -38,7 +37,7 @@ func TestCanFindUserInTable(t *testing.T) {
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewUserRepository(db)
|
||||
updated, err := repo.Create(context.Background(), "testing", "NotSecure", "placeholder")
|
||||
updated, err := repo.Create("testing", "NotSecure", "placeholder")
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
t.FailNow()
|
||||
@ -49,7 +48,7 @@ func TestCanFindUserInTable(t *testing.T) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
user, err := repo.GetByName(context.Background(), "testing")
|
||||
user, err := repo.GetByName("testing")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
t.FailNow()
|
||||
@ -66,7 +65,7 @@ func TestCheckUserHash(t *testing.T) {
|
||||
defer db.Close()
|
||||
|
||||
repo := repository.NewUserRepository(db)
|
||||
repo.CheckUserHash(context.Background(), "testing", "NotSecure")
|
||||
repo.CheckUserHash("testing", "NotSecure")
|
||||
}
|
||||
|
||||
func setupInMemoryDb() (*sql.DB, error) {
|
||||
|
@ -1,87 +0,0 @@
|
||||
package respositoryservices
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrUnexpectedAmountOfRowsUpdated = "got a unexpected of rows updated"
|
||||
)
|
||||
|
||||
type RefreshToken interface {
|
||||
Create(ctx context.Context, username string) (string, error)
|
||||
GetByName(ctx context.Context, name string) (domain.RefreshTokenEntity, error)
|
||||
Delete(ctx context.Context, id int64) (int64, error)
|
||||
IsRequestValid(ctx context.Context, username, refreshToken string) error
|
||||
}
|
||||
|
||||
// A new jwt token can be made if the user has the correct refresh token for the user.
|
||||
// It will also require the old JWT token so the expire time is pulled and part of the validation
|
||||
type RefreshTokenService struct {
|
||||
table repository.RefreshTokenRepository
|
||||
}
|
||||
|
||||
func NewRefreshTokenService(conn *sql.DB) RefreshTokenService {
|
||||
return RefreshTokenService{
|
||||
table: repository.NewRefreshTokenRepository(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (rt RefreshTokenService) Create(ctx context.Context, username string) (string, error) {
|
||||
//if a refresh token already exists for a user, reuse
|
||||
existingToken, err := rt.GetByName(ctx, username)
|
||||
if err == nil {
|
||||
rowsRemoved, err := rt.Delete(ctx, existingToken.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if rowsRemoved != 1 {
|
||||
return "", errors.New(ErrUnexpectedAmountOfRowsUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
token, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
rows, err := rt.table.Create(ctx, username, token.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if rows != 1 {
|
||||
return "", errors.New("expected one row but got none")
|
||||
}
|
||||
return token.String(), nil
|
||||
}
|
||||
|
||||
// Find the saved refresh token for a user and return it if it exists
|
||||
func (rt RefreshTokenService) GetByName(ctx context.Context, name string) (domain.RefreshTokenEntity, error) {
|
||||
return rt.table.GetByUsername(ctx, name)
|
||||
}
|
||||
|
||||
// This will request that a object is removed from the database
|
||||
func (rt RefreshTokenService) Delete(ctx context.Context, id int64) (int64, error) {
|
||||
return rt.table.DeleteById(ctx, id)
|
||||
}
|
||||
|
||||
func (rt RefreshTokenService) IsRequestValid(ctx context.Context, username, refreshToken string) error {
|
||||
token, err := rt.GetByName(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if token.Token != refreshToken {
|
||||
return errors.New("the refresh token given does not match")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
package respositoryservices
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrPasswordNotLongEnough = "password needs to be 8 character or longer"
|
||||
ErrPasswordMissingSpecialCharacter = "password needs to contain one of the following: !, @, #"
|
||||
ErrInvalidPassword = "invalid password"
|
||||
)
|
||||
|
||||
type UserServices interface {
|
||||
DoesUserExist(ctx context.Context, username string) error
|
||||
DoesPasswordMatchHash(ctx context.Context, username, password string) error
|
||||
GetUser(ctx context.Context, username string) (domain.UserEntity, error)
|
||||
AddScopes(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) (domain.UserEntity, error)
|
||||
CheckPasswordForRequirements(password string) error
|
||||
}
|
||||
|
||||
// This will handle operations that are user related, but one layer higher then the repository
|
||||
type UserService struct {
|
||||
repo repository.Users
|
||||
}
|
||||
|
||||
// This is a layer on top of the Users Repository.
|
||||
// Use this over directly talking to the table when ever possible.
|
||||
func NewUserService(conn *sql.DB) UserService {
|
||||
return UserService{
|
||||
repo: repository.NewUserRepository(conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (us UserService) DoesUserExist(ctx context.Context, username string) error {
|
||||
_, err := us.repo.GetByName(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us UserService) DoesPasswordMatchHash(ctx context.Context, username, password string) error {
|
||||
model, err := us.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(model.Hash), []byte(password))
|
||||
if err != nil {
|
||||
return errors.New(ErrInvalidPassword)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us UserService) GetUser(ctx context.Context, username string) (domain.UserEntity, error) {
|
||||
return us.repo.GetByName(ctx, username)
|
||||
}
|
||||
|
||||
func (us UserService) AddScopes(ctx context.Context, username string, scopes []string) error {
|
||||
usr, err := us.repo.GetByName(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if usr.Username != username {
|
||||
return errors.New(repository.ErrUserNotFound)
|
||||
}
|
||||
|
||||
currentScopes := strings.Split(usr.Scopes, ",")
|
||||
|
||||
// check the current scopes
|
||||
for _, item := range scopes {
|
||||
if !strings.Contains(usr.Scopes, item) {
|
||||
currentScopes = append(currentScopes, item)
|
||||
}
|
||||
}
|
||||
return us.repo.UpdateScopes(ctx, username, strings.Join(currentScopes, ","))
|
||||
}
|
||||
|
||||
func (us UserService) RemoveScopes(ctx context.Context, username string, scopes []string) error {
|
||||
usr, err := us.repo.GetByName(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if usr.Username != username {
|
||||
return errors.New(repository.ErrUserNotFound)
|
||||
}
|
||||
|
||||
var newScopes []string
|
||||
|
||||
// check all the scopes that are currently assigned
|
||||
for _, item := range strings.Split(usr.Scopes, ",") {
|
||||
|
||||
// check the scopes given, if one matches skip it
|
||||
if us.doesScopeExist(scopes, item) {
|
||||
continue
|
||||
}
|
||||
|
||||
// did not match, add it
|
||||
newScopes = append(newScopes, item)
|
||||
}
|
||||
|
||||
return us.repo.UpdateScopes(ctx, username, strings.Join(newScopes, ","))
|
||||
}
|
||||
|
||||
func (us UserService) doesScopeExist(scopes []string, target string) bool {
|
||||
for _, item := range scopes {
|
||||
if item == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (us UserService) Create(ctx context.Context, name, password, scope string) (domain.UserEntity, error) {
|
||||
err := us.CheckPasswordForRequirements(password)
|
||||
if err != nil {
|
||||
return domain.UserEntity{}, err
|
||||
}
|
||||
|
||||
us.repo.Create(ctx, name, password, domain.ScopeArticleRead)
|
||||
return domain.UserEntity{}, nil
|
||||
}
|
||||
|
||||
func (us UserService) CheckPasswordForRequirements(password string) error {
|
||||
err := us.checkPasswordLength(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = us.checkPasswordForSpecialCharacters(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us UserService) checkPasswordLength(password string) error {
|
||||
if len(password) < 8 {
|
||||
return errors.New(ErrPasswordNotLongEnough)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us UserService) checkPasswordForSpecialCharacters(password string) error {
|
||||
var chars []string
|
||||
chars = append(chars, "!")
|
||||
chars = append(chars, "@")
|
||||
chars = append(chars, "#")
|
||||
|
||||
for _, char := range chars {
|
||||
if strings.Contains(password, char) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New(ErrPasswordMissingSpecialCharacter)
|
||||
}
|
@ -35,7 +35,6 @@ const (
|
||||
type Configs struct {
|
||||
ServerAddress string
|
||||
JwtSecret string
|
||||
AdminSecret string
|
||||
|
||||
RedditEnabled bool
|
||||
RedditPullTop bool
|
||||
@ -67,7 +66,6 @@ func GetEnvConfig() Configs {
|
||||
return Configs{
|
||||
ServerAddress: os.Getenv(ServerAddress),
|
||||
JwtSecret: os.Getenv("JwtSecret"),
|
||||
AdminSecret: os.Getenv("AdminSecret"),
|
||||
|
||||
RedditEnabled: processBoolConfig(os.Getenv(FEATURE_ENABLE_REDDIT_BACKEND)),
|
||||
RedditPullTop: processBoolConfig(os.Getenv(REDDIT_PULL_TOP)),
|
||||
|
@ -4,16 +4,15 @@ import (
|
||||
"database/sql"
|
||||
|
||||
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
|
||||
repositoryservice "git.jamestombleson.com/jtom38/newsbot-api/internal/respositoryServices"
|
||||
)
|
||||
|
||||
type RepositoryService struct {
|
||||
AlertDiscord repository.AlertDiscordRepo
|
||||
Articles repository.ArticlesRepo
|
||||
DiscordWebHooks repository.DiscordWebHookRepo
|
||||
RefreshTokens repositoryservice.RefreshToken
|
||||
RefreshTokens repository.RefreshToken
|
||||
Sources repository.Sources
|
||||
Users repositoryservice.UserServices
|
||||
Users repository.Users
|
||||
UserSourceSubscriptions repository.UserSourceRepo
|
||||
}
|
||||
|
||||
@ -22,9 +21,9 @@ func NewRepositoryService(conn *sql.DB) RepositoryService {
|
||||
AlertDiscord: repository.NewAlertDiscordRepository(conn),
|
||||
Articles: repository.NewArticleRepository(conn),
|
||||
DiscordWebHooks: repository.NewDiscordWebHookRepository(conn),
|
||||
RefreshTokens: repositoryservice.NewRefreshTokenService(conn),
|
||||
RefreshTokens: repository.NewRefreshTokenRepository(conn),
|
||||
Sources: repository.NewSourceRepository(conn),
|
||||
Users: repositoryservice.NewUserService(conn),
|
||||
Users: repository.NewUserRepository(conn),
|
||||
UserSourceSubscriptions: repository.NewUserSourceRepository(conn),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user