Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Marco Realacci 2022-12-08 11:51:01 +01:00
commit e38f5d08ab
794 changed files with 610205 additions and 1507 deletions

1
.vscode/launch.json vendored
View file

@ -9,6 +9,7 @@
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"buildFlags": "-tags webui",
"program": "./cmd/webapi" "program": "./cmd/webapi"
} }
] ]

View file

@ -26,7 +26,7 @@ type WebAPIConfiguration struct {
} }
Debug bool Debug bool
DB struct { DB struct {
Filename string `conf:"default:/tmp/decaf.db"` Filename string `conf:"default:./wasaphoto.db"`
} }
Data struct { Data struct {
Path string `conf:"default:/tmp/wasaphoto"` Path string `conf:"default:/tmp/wasaphoto"`

View file

@ -122,11 +122,11 @@ func run() error {
} }
router := apirouter.Handler() router := apirouter.Handler()
//router, err = registerWebUI(router) router, err = registerWebUI(router)
//if err != nil { if err != nil {
// logger.WithError(err).Error("error registering web UI handler") logger.WithError(err).Error("error registering web UI handler")
// return fmt.Errorf("registering web UI handler: %w", err) return fmt.Errorf("registering web UI handler: %w", err)
//} }
// Apply CORS policy // Apply CORS policy
router = applyCORSHandler(router) router = applyCORSHandler(router)

View file

@ -4,13 +4,14 @@ package main
import ( import (
"fmt" "fmt"
//"github.com/notherealmarco/WASAPhoto/webui" "github.com/notherealmarco/WASAPhoto/webui"
"io/fs" "io/fs"
"net/http" "net/http"
"strings" "strings"
) )
func registerWebUI(hdl http.Handler) (http.Handler, error) { func registerWebUI(hdl http.Handler) (http.Handler, error) {
fmt.Printf("Registering WebUI...")
distDirectory, err := fs.Sub(webui.Dist, "dist") distDirectory, err := fs.Sub(webui.Dist, "dist")
if err != nil { if err != nil {
return nil, fmt.Errorf("error embedding WebUI dist/ directory: %w", err) return nil, fmt.Errorf("error embedding WebUI dist/ directory: %w", err)

BIN
cmd/webapi/wasaphoto.db Normal file

Binary file not shown.

View file

@ -4,7 +4,7 @@ info:
description: |- description: |-
Keep in touch with your friends by sharing photos of special moments, thanks to WASAPhoto! You can Keep in touch with your friends by sharing photos of special moments, thanks to WASAPhoto! You can
upload your photos directly from your PC, and they will be visible to everyone following you. upload your photos directly from your PC, and they will be visible to everyone following you.
version: "2.0.0" version: "3.0"
tags: tags:
- name: login - name: login
description: Login API description: Login API
@ -37,7 +37,7 @@ paths:
requestBody: requestBody:
$ref: "#/components/requestBodies/userDetails" $ref: "#/components/requestBodies/userDetails"
responses: responses:
'200': #is 201 in the original one '201':
description: User log-in action successful. description: User log-in action successful.
content: content:
application/json: application/json:
@ -51,7 +51,7 @@ paths:
tags: ["username"] tags: ["username"]
summary: Updates the username summary: Updates the username
description: Changes the username of the user with the given one. description: Changes the username of the user with the given one.
operationId: setMyUsername operationId: setMyUserName
parameters: parameters:
- name: user_id - name: user_id
in: path in: path
@ -73,7 +73,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Username already taken" status: "Username already taken"
'404': '404':
description: The user does not exist. description: The user does not exist.
content: content:
@ -81,7 +81,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "User not found" status: "User not found"
/users/{user_id}/followers: /users/{user_id}/followers:
get: get:
@ -107,6 +107,7 @@ paths:
type: array type: array
description: The list of followers. description: The list of followers.
minItems: 0 minItems: 0
maxItems: 100
items: items:
$ref: "#/components/schemas/uid_name" $ref: "#/components/schemas/uid_name"
example: example:
@ -121,7 +122,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "User not found" status: "User not found"
/users/{user_id}/following: /users/{user_id}/following:
get: get:
tags: ["followers"] tags: ["followers"]
@ -145,6 +146,7 @@ paths:
schema: schema:
type: array type: array
minItems: 0 minItems: 0
maxItems: 100
description: The list of users that the user is following. description: The list of users that the user is following.
items: items:
$ref: "#/components/schemas/uid_name" $ref: "#/components/schemas/uid_name"
@ -160,7 +162,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "User not found" status: "User not found"
/users/{user_id}/followers/{follower_uid}: /users/{user_id}/followers/{follower_uid}:
parameters: parameters:
@ -184,8 +186,14 @@ paths:
security: security:
- BearerAuth: [] - BearerAuth: []
responses: responses:
'204': '201':
description: Follow user action successful. description: Follow user action successful.
content:
application/json:
schema:
$ref: "#/components/schemas/generic_response"
example:
status: "Success"
'403': '403':
description: The user has no permission perform this action. description: The user has no permission perform this action.
content: content:
@ -193,7 +201,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Forbidden" status: "Forbidden"
'404': '404':
description: The resource does not exist. description: The resource does not exist.
content: content:
@ -201,15 +209,15 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "User not found" status: "Resource not found"
'400': '400': # todo: not sure if this is the right error code
description: Trying to follow a user that does not exist. description: Trying to follow a user that does not exist.
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "User not found" status: "User not found"
delete: delete:
tags: ["followers"] tags: ["followers"]
summary: Unfollows a user summary: Unfollows a user
@ -227,7 +235,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "User not found" status: "User not found"
/users/{user_id}/bans/{ban_uid}: /users/{user_id}/bans/{ban_uid}:
parameters: parameters:
@ -251,8 +259,16 @@ paths:
security: security:
- BearerAuth: [] - BearerAuth: []
responses: responses:
'204': '201':
description: Ban user action successful. description: Ban user action successful.
content:
application/json:
schema:
$ref: "#/components/schemas/generic_response"
example:
status: "Success"
'204':
description: The user is already banned.
'403': '403':
description: The user has no permission to perform this action. description: The user has no permission to perform this action.
content: content:
@ -260,7 +276,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Forbidden" status: "Forbidden"
'404': '404':
description: The user does not exist. description: The user does not exist.
content: content:
@ -268,7 +284,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "User not found" status: "User not found"
delete: delete:
tags: ["bans"] tags: ["bans"]
@ -287,7 +303,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Forbidden" status: "Forbidden"
'404': '404':
description: The user is not banned by the user. description: The user is not banned by the user.
content: content:
@ -295,7 +311,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "User not found" status: "User not found"
/users/{user_id}/photos/{photo_id}/likes: /users/{user_id}/photos/{photo_id}/likes:
get: get:
@ -318,6 +334,18 @@ paths:
$ref: "#/components/schemas/photo_id" $ref: "#/components/schemas/photo_id"
required: true required: true
description: The ID of the photo. description: The ID of the photo.
- name: limit
in: query
schema:
$ref: "#/components/schemas/limit"
description: The number of elements to show.
required: false
- name: start_index
in: query
schema:
$ref: "#/components/schemas/start_index"
description: The starting offset.
required: false
responses: responses:
'200': '200':
description: Returns the user list description: Returns the user list
@ -326,6 +354,7 @@ paths:
schema: schema:
type: array type: array
minItems: 0 minItems: 0
maxItems: 100
description: An array of users liking the photo. description: An array of users liking the photo.
items: items:
$ref: "#/components/schemas/uid_name" $ref: "#/components/schemas/uid_name"
@ -341,7 +370,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Resource not found" status: "Resource not found"
/users/{user_id}/photos/{photo_id}/likes/{liker_uid}: /users/{user_id}/photos/{photo_id}/likes/{liker_uid}:
parameters: parameters:
@ -371,8 +400,16 @@ paths:
security: security:
- BearerAuth: [] - BearerAuth: []
responses: responses:
'204': '201':
description: Like photo action successful. description: Like photo action successful.
content:
application/json:
schema:
$ref: "#/components/schemas/generic_response"
example:
status: "Success"
'204':
description: The user already likes the photo.
'403': '403':
description: The user has no permission to perform this action. description: The user has no permission to perform this action.
content: content:
@ -380,7 +417,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Forbidden" status: "Forbidden"
'404': '404':
description: Resource not found (or the author of the photo has banned the authorized user). description: Resource not found (or the author of the photo has banned the authorized user).
content: content:
@ -388,7 +425,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Resource not found" status: "Resource not found"
'400': '400':
description: Bad URI (maybe liker_uid is invalid). description: Bad URI (maybe liker_uid is invalid).
content: content:
@ -396,7 +433,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Bad photo_id" status: "Bad photo_id"
delete: delete:
tags: ["likes"] tags: ["likes"]
summary: Unlikes a photo summary: Unlikes a photo
@ -414,7 +451,75 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Resource not found" status: "Resource not found"
/users:
get:
tags: ["search"]
summary: Search users by username
description: Search users whose username contains the given string.
operationId: searchUsers
security:
- BearerAuth: []
parameters:
- name: query
in: query
schema:
$ref: "#/components/schemas/name"
required: true
description: The username to search.
- name: limit
in: query
schema:
$ref: "#/components/schemas/limit"
description: The number of elements to show.
required: false
- name: start_index
in: query
schema:
$ref: "#/components/schemas/start_index"
description: The starting offset.
required: false
responses:
'200':
description: Returns the user list
content:
application/json:
schema:
type: array
minItems: 0
maxItems: 100
description: An array of users whose username contains the given string.
items:
$ref: "#/components/schemas/uid_name"
example:
- user_id: "123e4567-e89b-12d3-a456-426655440000"
username: "Maria"
- user_id: "123e4567-e89b-12d3-a456-426655440001"
username: "Filippo"
'404':
description: No user found.
content:
application/json:
schema:
$ref: "#/components/schemas/generic_response"
example:
status: "Resource not found"
'400':
description: Some parameters are malformed.
content:
application/json:
schema:
$ref: "#/components/schemas/generic_response"
example:
status: "Invalid start_index or limit value"
'401':
description: The user is not logged in.
content:
application/json:
schema:
$ref: "#/components/schemas/generic_response"
example:
status: "Unauthorized"
/users/{user_id}: /users/{user_id}:
get: get:
@ -442,13 +547,8 @@ paths:
username: "Maria" username: "Maria"
followers: 25 followers: 25
following: 32 following: 32
photos: followed: true
- photo_id: 2341 photos: 50
upload_time: "2020-11-20T12:00:00Z"
likes: 2
- photo_id: 2342
upload_time: "2022-10-23T12:01:00Z"
likes: 5
'404': '404':
description: User not found (or the authorized user is banned). description: User not found (or the authorized user is banned).
content: content:
@ -456,16 +556,9 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Resource not found" status: "Resource not found"
/users/{user_id}/photos: /users/{user_id}/photos:
post:
tags: ["photos"]
summary: Uploads a photo
description: Uploads a photo in the gallery of the authorized user.
operationId: uploadPhoto
security:
- BearerAuth: []
parameters: parameters:
- name: user_id - name: user_id
in: path in: path
@ -473,6 +566,58 @@ paths:
$ref: "#/components/schemas/uid" $ref: "#/components/schemas/uid"
required: true required: true
description: The user ID of the user who uploads the photo. description: The user ID of the user who uploads the photo.
get:
tags: ["photos"]
summary: Returns user photos
description: Returns the list of photos uploaded by a user.
operationId: getUserPhotos
security:
- BearerAuth: []
parameters:
- name: limit
in: query
schema:
$ref: "#/components/schemas/limit"
description: The number of elements to show.
required: false
- name: start_index
in: query
schema:
$ref: "#/components/schemas/start_index"
description: The starting offset.
required: false
responses:
'200':
description: Returns the user photos list
content:
application/json:
schema:
$ref: "#/components/schemas/user_photo_stream"
'404':
description: User not found (or the authorized user is banned).
content:
application/json:
schema:
$ref: "#/components/schemas/generic_response"
example:
status: "Resource not found"
'400':
description: Some parameters are malformed.
content:
application/json:
schema:
$ref: "#/components/schemas/generic_response"
example:
status: "Invalid start_index or limit value"
post:
tags: ["photos"]
summary: Uploads a photo
description: Uploads a photo in the gallery of the authorized user.
operationId: uploadPhoto
security:
- BearerAuth: []
requestBody: requestBody:
content: content:
image/jpeg: image/jpeg:
@ -480,6 +625,7 @@ paths:
description: The photo to upload. description: The photo to upload.
type: string type: string
format: binary format: binary
pattern: "(?s).*" # todo: review. Btw this means "any string"
responses: responses:
'201': '201':
description: Upload photo action successful. description: Upload photo action successful.
@ -496,7 +642,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Resource not found" status: "Resource not found"
/users/{user_id}/photos/{photo_id}: /users/{user_id}/photos/{photo_id}:
parameters: parameters:
@ -525,7 +671,8 @@ paths:
content: content:
image/jpeg: image/jpeg:
schema: schema:
#todo it wants lenght minLength: 1
maxLength: 10485760 # Image should not be bigger than 10 MiB
description: The requested photo in binary format. description: The requested photo in binary format.
type: string type: string
format: binary format: binary
@ -536,7 +683,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Resource not found" status: "Resource not found"
delete: delete:
tags: ["photos"] tags: ["photos"]
summary: Deletes a photo summary: Deletes a photo
@ -554,7 +701,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Unauthorized" status: "Unauthorized"
'404': '404':
description: User or photo not found. description: User or photo not found.
content: content:
@ -562,7 +709,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Resource not found" status: "Resource not found"
/users/{user_id}/photos/{photo_id}/comments: /users/{user_id}/photos/{photo_id}/comments:
parameters: parameters:
@ -583,6 +730,19 @@ paths:
summary: Gets photo comments summary: Gets photo comments
description: Gets the list of comments of a photo description: Gets the list of comments of a photo
operationId: getPhotoComments operationId: getPhotoComments
parameters:
- name: limit
in: query
schema:
$ref: "#/components/schemas/limit"
description: The number of elements to show.
required: false
- name: start_index
in: query
schema:
$ref: "#/components/schemas/start_index"
description: The starting offset.
required: false
security: security:
- BearerAuth: [] - BearerAuth: []
responses: responses:
@ -610,7 +770,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Resource not found" status: "Resource not found"
post: post:
tags: ["comments"] tags: ["comments"]
summary: Comments a photo summary: Comments a photo
@ -627,8 +787,14 @@ paths:
example: example:
comment: "Lovely!" comment: "Lovely!"
responses: responses:
'204': '201':
description: Comment photo action successful. #todo maybe 201 description: Comment photo action successful.
content:
application/json:
schema:
$ref: "#/components/schemas/generic_response"
example:
status: "Created"
'404': '404':
description: The user or the photo does not exists (or the author of the photo has banned the authorized user). description: The user or the photo does not exists (or the author of the photo has banned the authorized user).
content: content:
@ -636,14 +802,14 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Resource not found" status: "Resource not found"
/users/{user_id}/photos/{photo_id}/comments/{comment_id}: /users/{user_id}/photos/{photo_id}/comments/{comment_id}:
delete: delete:
tags: ["comments"] tags: ["comments"]
summary: Deletes a comment summary: Deletes a comment
description: Deletes a photo in the gallery of the authorized user. description: Deletes a photo in the gallery of the authorized user.
operationId: deleteComment operationId: uncommentPhoto
security: security:
- BearerAuth: [] - BearerAuth: []
parameters: parameters:
@ -675,7 +841,7 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Unauthorized" status: "Unauthorized"
'404': '404':
description: The comment does not exists. description: The comment does not exists.
content: content:
@ -683,9 +849,9 @@ paths:
schema: schema:
$ref: "#/components/schemas/generic_response" $ref: "#/components/schemas/generic_response"
example: example:
error: "Resource not found" status: "Resource not found"
/stream: # todo review path /stream:
get: get:
tags: ["stream"] tags: ["stream"]
summary: Returns user stream summary: Returns user stream
@ -697,15 +863,13 @@ paths:
- name: limit - name: limit
in: query in: query
schema: schema:
type: integer $ref: "#/components/schemas/limit"
default: 25
description: The number of elements to show. description: The number of elements to show.
required: false required: false
- name: start_index - name: start_index
in: query in: query
schema: schema:
type: integer $ref: "#/components/schemas/start_index"
default: 0
description: The starting offset. description: The starting offset.
required: false required: false
responses: responses:
@ -717,12 +881,12 @@ paths:
$ref: "#/components/schemas/photo_stream" $ref: "#/components/schemas/photo_stream"
example: example:
- user_id: "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" - user_id: "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
- username: "Federicus" username: "Federicus"
- photo_id: 1520 photo_id: 157
- upload_time: "2020-11-20T12:00:00Z" upload_time: "2020-11-20T12:00:00Z"
- likes: 93 likes: 93
comments: 13
liked: true
components: components:
securitySchemes: securitySchemes:
@ -731,6 +895,14 @@ components:
scheme: bearer scheme: bearer
schemas: schemas:
start_index:
type: integer
description: The starting offset.
default: 0
limit:
type: integer
description: The number of elements to show.
default: 15
uid_name: uid_name:
type: object type: object
properties: properties:
@ -775,11 +947,17 @@ components:
upload_time: upload_time:
type: string type: string
format: date-time format: date-time
description: Upload time and date. description: Upload time and date in RFC3339 format.
minLength: 20
maxLength: 25
likes: likes:
type: integer type: integer
example: 90 example: 90
description: Number of likes. description: Number of likes.
comments_n:
type: integer
example: 7
description: Number of comments on the photo.
followers_n: followers_n:
type: integer type: integer
example: 420 example: 420
@ -788,6 +966,18 @@ components:
type: integer type: integer
example: 69 example: 69
description: Number of following users. description: Number of following users.
like_status:
type: boolean
example: true
description: Whether the user liked the photo.
follow_status:
type: boolean
example: false
description: Whether the user follows the other user.
photos_n:
type: integer
example: 90
description: Number of photos.
user_profile: user_profile:
type: object type: object
description: The profile of the user. description: The profile of the user.
@ -799,10 +989,13 @@ components:
following: following:
$ref: "#/components/schemas/following_n" $ref: "#/components/schemas/following_n"
photos: photos:
$ref: "#/components/schemas/user_photo_stream" $ref: "#/components/schemas/photos_n"
followed:
$ref: "#/components/schemas/follow_status"
user_photo_stream: user_photo_stream:
type: array type: array
minItems: 0 minItems: 0
maxItems: 100
description: An array of photos, upload time and likes. description: An array of photos, upload time and likes.
items: items:
$ref: "#/components/schemas/user_photo_stream_item" $ref: "#/components/schemas/user_photo_stream_item"
@ -815,9 +1008,14 @@ components:
$ref: "#/components/schemas/upload_time" $ref: "#/components/schemas/upload_time"
likes: likes:
$ref: "#/components/schemas/likes" $ref: "#/components/schemas/likes"
comments:
$ref: "#/components/schemas/comments_n"
liked:
$ref: "#/components/schemas/like_status"
photo_stream: photo_stream:
type: array type: array
minItems: 0 minItems: 0
maxItems: 100
description: An array of photos, author, upload time and likes. description: An array of photos, author, upload time and likes.
items: items:
$ref: "#/components/schemas/photo_stream_item" $ref: "#/components/schemas/photo_stream_item"
@ -834,9 +1032,14 @@ components:
$ref: "#/components/schemas/upload_time" $ref: "#/components/schemas/upload_time"
likes: likes:
$ref: "#/components/schemas/likes" $ref: "#/components/schemas/likes"
comments:
$ref: "#/components/schemas/comments_n"
liked:
$ref: "#/components/schemas/like_status"
comments: comments:
type: array type: array
minItems: 0 minItems: 0
maxItems: 100
description: An array of comments. description: An array of comments.
items: items:
$ref : "#/components/schemas/comment_item" $ref : "#/components/schemas/comment_item"
@ -863,8 +1066,8 @@ components:
$ref: "#/components/schemas/comment" $ref: "#/components/schemas/comment"
comment: comment:
minLength: 5 minLength: 5
maxLength: 100 #todo think about it maxLength: 255
pattern: ".*" #everything except newlines pattern: ".*" #everything except newlines ^[*]{5, 255}$
type: string type: string
example: "What a lovely picture! 😊" example: "What a lovely picture! 😊"
description: The comment's text description: The comment's text
@ -875,6 +1078,7 @@ components:
properties: properties:
status: status:
type: string type: string
pattern: ".*"
description: The status of the request. description: The status of the request.
example: "Success" example: "Success"

0
open-npm.sh Normal file → Executable file
View file

View file

@ -11,14 +11,18 @@ func (rt *_router) Handler() http.Handler {
rt.router.PUT("/users/:user_id/username", rt.wrap(rt.UpdateUsername)) rt.router.PUT("/users/:user_id/username", rt.wrap(rt.UpdateUsername))
rt.router.GET("/users", rt.wrap(rt.GetSearchUsers))
rt.router.GET("/users/:user_id/followers", rt.wrap(rt.GetFollowersFollowing)) rt.router.GET("/users/:user_id/followers", rt.wrap(rt.GetFollowersFollowing))
rt.router.PUT("/users/:user_id/followers/:follower_uid", rt.wrap(rt.PutFollow)) rt.router.PUT("/users/:user_id/followers/:follower_uid", rt.wrap(rt.PutFollow))
rt.router.DELETE("/users/:user_id/followers/:follower_uid", rt.wrap(rt.DeleteFollow)) rt.router.DELETE("/users/:user_id/followers/:follower_uid", rt.wrap(rt.DeleteFollow))
rt.router.GET("/users/:user_id/following", rt.wrap(rt.GetFollowersFollowing)) rt.router.GET("/users/:user_id/following", rt.wrap(rt.GetFollowersFollowing))
rt.router.GET("/users/:user_id/bans", rt.wrap(rt.GetUserBans))
rt.router.PUT("/users/:user_id/bans/:ban_uid", rt.wrap(rt.PutBan)) rt.router.PUT("/users/:user_id/bans/:ban_uid", rt.wrap(rt.PutBan))
rt.router.DELETE("/users/:user_id/bans/:ban_uid", rt.wrap(rt.DeleteBan)) rt.router.DELETE("/users/:user_id/bans/:ban_uid", rt.wrap(rt.DeleteBan))
rt.router.GET("/users/:user_id/photos", rt.wrap(rt.GetUserPhotos))
rt.router.POST("/users/:user_id/photos", rt.wrap(rt.PostPhoto)) rt.router.POST("/users/:user_id/photos", rt.wrap(rt.PostPhoto))
rt.router.GET("/users/:user_id/photos/:photo_id", rt.wrap(rt.GetPhoto)) rt.router.GET("/users/:user_id/photos/:photo_id", rt.wrap(rt.GetPhoto))
rt.router.DELETE("/users/:user_id/photos/:photo_id", rt.wrap(rt.DeletePhoto)) rt.router.DELETE("/users/:user_id/photos/:photo_id", rt.wrap(rt.DeletePhoto))
@ -31,8 +35,9 @@ func (rt *_router) Handler() http.Handler {
rt.router.POST("/users/:user_id/photos/:photo_id/comments", rt.wrap(rt.PostComment)) rt.router.POST("/users/:user_id/photos/:photo_id/comments", rt.wrap(rt.PostComment))
rt.router.DELETE("/users/:user_id/photos/:photo_id/comments/:comment_id", rt.wrap(rt.DeleteComment)) rt.router.DELETE("/users/:user_id/photos/:photo_id/comments/:comment_id", rt.wrap(rt.DeleteComment))
rt.router.GET("/", rt.getHelloWorld) rt.router.GET("/users/:user_id", rt.wrap(rt.GetUserProfile))
rt.router.GET("/context", rt.wrap(rt.getContextReply))
rt.router.GET("/stream", rt.wrap(rt.GetUserStream))
// Special routes // Special routes
rt.router.GET("/liveness", rt.liveness) rt.router.GET("/liveness", rt.liveness)

View file

@ -14,7 +14,7 @@ func BuildAuth(header string) (reqcontext.Authorization, error) {
auth, err := BuildBearer(header) auth, err := BuildBearer(header)
if err != nil { if err != nil {
if err.Error() == "invalid authorization header" { if err.Error() == "invalid authorization header" {
return nil, errors.New("method not supported") // todo: better error description return nil, errors.New("authentication method not supported")
} }
return nil, err return nil, err
} }
@ -37,8 +37,25 @@ func SendAuthorizationError(f func(db database.AppDatabase, uid string) (reqcont
} }
// requested user is not found -> 404 as the resource is not found // requested user is not found -> 404 as the resource is not found
if auth == reqcontext.USER_NOT_FOUND { if auth == reqcontext.USER_NOT_FOUND {
helpers.SendStatus(notFoundStatus, w, "Resource not found", l) helpers.SendStatus(notFoundStatus, w, "User not found", l)
return false return false
} }
return true return true
} }
func SendErrorIfNotLoggedIn(f func(db database.AppDatabase) (reqcontext.AuthStatus, error), db database.AppDatabase, w http.ResponseWriter, l logrus.FieldLogger) bool {
auth, err := f(db)
if err != nil {
helpers.SendInternalError(err, "Authorization error", w, l)
return false
}
if auth == reqcontext.UNAUTHORIZED {
helpers.SendStatus(http.StatusUnauthorized, w, "Unauthorized", l)
return false
}
return true
}

View file

@ -1,6 +1,7 @@
package api package api
import ( import (
"encoding/json"
"net/http" "net/http"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
@ -10,6 +11,46 @@ import (
"github.com/notherealmarco/WASAPhoto/service/database" "github.com/notherealmarco/WASAPhoto/service/database"
) )
func (rt *_router) GetUserBans(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
// Get user id
uid := ps.ByName("user_id")
if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, rt.baseLogger, http.StatusNotFound) {
// A user should not be able to see other users' bans
return
}
// Get limits, or use default values
start_index, limit, err := helpers.GetLimits(r.URL.Query())
if err != nil {
// Send error if the limits are specified but invalid
helpers.SendBadRequest(w, "Invalid start_index or limit value", rt.baseLogger)
return
}
// Get bans
// We don't need to check if the user exists, because the authorization middleware already did that
bans, err := rt.db.GetUserBans(uid, start_index, limit)
if err != nil {
helpers.SendInternalError(err, "Database error: GetUserBans", w, rt.baseLogger)
return
}
// Return ban list
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) // is it needed?
err = json.NewEncoder(w).Encode(bans) // write the response
if err != nil {
helpers.SendInternalError(err, "Error encoding json", w, rt.baseLogger)
return
}
}
func (rt *_router) PutBan(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { func (rt *_router) PutBan(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
uid := ps.ByName("user_id") uid := ps.ByName("user_id")

View file

@ -3,6 +3,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"regexp"
"strconv" "strconv"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
@ -33,8 +34,16 @@ func (rt *_router) GetComments(w http.ResponseWriter, r *http.Request, ps httpro
return return
} }
// get limits, or use defaults
start_index, limit, err := helpers.GetLimits(r.URL.Query())
if err != nil {
helpers.SendBadRequest(w, "Invalid start_index or limit", rt.baseLogger)
return
}
// get the user's comments // get the user's comments
success, comments, err := rt.db.GetComments(uid, photo_id) success, comments, err := rt.db.GetComments(uid, photo_id, ctx.Auth.GetUserID(), start_index, limit)
if err != nil { if err != nil {
helpers.SendInternalError(err, "Database error: GetComments", w, rt.baseLogger) helpers.SendInternalError(err, "Database error: GetComments", w, rt.baseLogger)
@ -73,6 +82,22 @@ func (rt *_router) PostComment(w http.ResponseWriter, r *http.Request, ps httpro
// check if the user is authorized to post a comment // check if the user is authorized to post a comment
if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, request_body.UID, rt.db, w, rt.baseLogger, http.StatusBadRequest) { if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, request_body.UID, rt.db, w, rt.baseLogger, http.StatusBadRequest) {
// It returns 400 Bad Request if the user_id field in the request body is missing or an invalid user_id
// It returns 401 if the user is not logged in
// It returns 403 if the user is not authorized to post a comment as the requested user
return
}
// check if the comment is valid (should not contain newlines and at be between 5 and 255 characters)
stat, err := regexp.Match(`^[*]{5, 255}$`, []byte(request_body.Comment))
if err != nil {
helpers.SendInternalError(err, "Error matching regex", w, rt.baseLogger)
return
}
if !stat {
helpers.SendBadRequest(w, "Invalid comment", rt.baseLogger)
return return
} }

View file

@ -15,6 +15,10 @@ import (
func (rt *_router) GetFollowersFollowing(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { func (rt *_router) GetFollowersFollowing(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
if !authorization.SendErrorIfNotLoggedIn(ctx.Auth.Authorized, rt.db, w, rt.baseLogger) {
return
}
uid := ps.ByName("user_id") uid := ps.ByName("user_id")
if !helpers.VerifyUserOrNotFound(rt.db, uid, w, rt.baseLogger) { if !helpers.VerifyUserOrNotFound(rt.db, uid, w, rt.baseLogger) {
@ -25,13 +29,21 @@ func (rt *_router) GetFollowersFollowing(w http.ResponseWriter, r *http.Request,
var err error var err error
var status database.QueryResult var status database.QueryResult
// Get limits, or use default values
start_index, limit, err := helpers.GetLimits(r.URL.Query())
if err != nil {
helpers.SendBadRequest(w, "Invalid start_index or limit", rt.baseLogger)
return
}
// Check if client is asking for followers or following // Check if client is asking for followers or following
if strings.HasSuffix(r.URL.Path, "/followers") { if strings.HasSuffix(r.URL.Path, "/followers") {
// Get the followers from the database // Get the followers from the database
status, users, err = rt.db.GetUserFollowers(uid) status, users, err = rt.db.GetUserFollowers(uid, ctx.Auth.GetUserID(), start_index, limit)
} else { } else {
// Get the following users from the database // Get the following users from the database
status, users, err = rt.db.GetUserFollowing(uid) status, users, err = rt.db.GetUserFollowing(uid, ctx.Auth.GetUserID(), start_index, limit)
} }
// Send a 500 response if there was an error // Send a 500 response if there was an error

View file

@ -1,14 +0,0 @@
package api
import (
"github.com/notherealmarco/WASAPhoto/service/api/reqcontext"
"github.com/julienschmidt/httprouter"
"net/http"
)
// getContextReply is an example of HTTP endpoint that returns "Hello World!" as a plain text. The signature of this
// handler accepts a reqcontext.RequestContext (see httpRouterHandler).
func (rt *_router) getContextReply(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
w.Header().Set("content-type", "text/plain")
_, _ = w.Write([]byte("Hello World!"))
}

View file

@ -1,12 +0,0 @@
package api
import (
"github.com/julienschmidt/httprouter"
"net/http"
)
// getHelloWorld is an example of HTTP endpoint that returns "Hello world!" as a plain text
func (rt *_router) getHelloWorld(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
w.Header().Set("content-type", "text/plain")
_, _ = w.Write([]byte("Hello World!"))
}

48
service/api/get-stream.go Normal file
View file

@ -0,0 +1,48 @@
package api
import (
"encoding/json"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/notherealmarco/WASAPhoto/service/api/authorization"
"github.com/notherealmarco/WASAPhoto/service/api/helpers"
"github.com/notherealmarco/WASAPhoto/service/api/reqcontext"
)
func (rt *_router) GetUserStream(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
// We must know who is requesting the stream
if !authorization.SendErrorIfNotLoggedIn(ctx.Auth.Authorized, rt.db, w, rt.baseLogger) {
return
}
// Get user id, probably this should be changed as it's not really REST
uid := ctx.Auth.GetUserID()
// Get start index and limit, or their default values
start_index, limit, err := helpers.GetLimits(r.URL.Query())
if err != nil {
helpers.SendBadRequest(w, "Invalid start_index or limit value", rt.baseLogger)
return
}
// Get the stream
stream, err := rt.db.GetUserStream(uid, start_index, limit)
if err != nil {
helpers.SendInternalError(err, "Database error: GetUserProfile", w, rt.baseLogger)
return
}
// Return the stream in json format
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(stream)
if err != nil {
helpers.SendInternalError(err, "Error encoding json", w, rt.baseLogger)
return
}
}

View file

@ -0,0 +1,83 @@
package api
import (
"encoding/json"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/notherealmarco/WASAPhoto/service/api/authorization"
"github.com/notherealmarco/WASAPhoto/service/api/helpers"
"github.com/notherealmarco/WASAPhoto/service/api/reqcontext"
"github.com/notherealmarco/WASAPhoto/service/database"
)
func (rt *_router) GetUserProfile(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
// Get user id
uid := ps.ByName("user_id")
if !authorization.SendErrorIfNotLoggedIn(ctx.Auth.Authorized, rt.db, w, rt.baseLogger) ||
!helpers.SendNotFoundIfBanned(rt.db, ctx.Auth.GetUserID(), uid, w, rt.baseLogger) {
return
}
// Get user profile
status, profile, err := rt.db.GetUserProfile(uid, ctx.Auth.GetUserID())
if err != nil {
helpers.SendInternalError(err, "Database error: GetUserProfile", w, rt.baseLogger)
return
}
if status == database.ERR_NOT_FOUND {
helpers.SendNotFound(w, "User not found", rt.baseLogger)
return
}
// Return user profile
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(profile)
if err != nil {
helpers.SendInternalError(err, "Error encoding json", w, rt.baseLogger)
return
}
}
func (rt *_router) GetUserPhotos(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
// Get user id
uid := ps.ByName("user_id")
if !authorization.SendErrorIfNotLoggedIn(ctx.Auth.Authorized, rt.db, w, rt.baseLogger) ||
!helpers.SendNotFoundIfBanned(rt.db, ctx.Auth.GetUserID(), uid, w, rt.baseLogger) {
return
}
// Get limits, or use default values
start_index, limit, err := helpers.GetLimits(r.URL.Query())
if err != nil {
helpers.SendBadRequest(w, "Invalid start_index or limit value", rt.baseLogger)
return
}
// Get user photos
photos, err := rt.db.GetUserPhotos(uid, ctx.Auth.GetUserID(), start_index, limit)
if err != nil {
helpers.SendInternalError(err, "Database error: GetUserPhotos", w, rt.baseLogger)
return
}
// Return user photos
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(photos)
if err != nil {
helpers.SendInternalError(err, "Error encoding json", w, rt.baseLogger)
return
}
}

View file

@ -41,7 +41,6 @@ func SendStatus(httpStatus int, w http.ResponseWriter, description string, l log
err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description})
if err != nil { if err != nil {
l.WithError(err).Error("Error encoding json") l.WithError(err).Error("Error encoding json")
//todo: empty response?
} }
} }
@ -50,7 +49,6 @@ func SendNotFound(w http.ResponseWriter, description string, l logrus.FieldLogge
err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description})
if err != nil { if err != nil {
l.WithError(err).Error("Error encoding json") l.WithError(err).Error("Error encoding json")
//todo: empty response?
} }
} }
@ -59,7 +57,6 @@ func SendBadRequest(w http.ResponseWriter, description string, l logrus.FieldLog
err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description})
if err != nil { if err != nil {
l.WithError(err).Error("Error encoding json") l.WithError(err).Error("Error encoding json")
//todo: empty response?
} }
} }
@ -69,7 +66,6 @@ func SendBadRequestError(err error, description string, w http.ResponseWriter, l
err = json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) err = json.NewEncoder(w).Encode(structures.GenericResponse{Status: description})
if err != nil { if err != nil {
l.WithError(err).Error("Error encoding json") l.WithError(err).Error("Error encoding json")
//todo: empty response?
} }
} }
@ -79,7 +75,6 @@ func SendInternalError(err error, description string, w http.ResponseWriter, l l
err = json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) err = json.NewEncoder(w).Encode(structures.GenericResponse{Status: description})
if err != nil { if err != nil {
l.WithError(err).Error("Error encoding json") l.WithError(err).Error("Error encoding json")
//todo: empty response?
} }
} }
@ -89,3 +84,16 @@ func RollbackOrLogError(tx database.DBTransaction, l logrus.FieldLogger) {
l.WithError(err).Error("Error rolling back transaction") l.WithError(err).Error("Error rolling back transaction")
} }
} }
func SendNotFoundIfBanned(db database.AppDatabase, uid string, banner string, w http.ResponseWriter, l logrus.FieldLogger) bool {
banned, err := db.IsBanned(uid, banner)
if err != nil {
SendInternalError(err, "Database error: IsBanned", w, l)
return false
}
if banned {
SendNotFound(w, "User not found", l)
return false
}
return true
}

View file

@ -0,0 +1,33 @@
package helpers
import (
"net/url"
"strconv"
)
const (
DEFAULT_LIMIT = 15 // don't know if should be moved to config
DEFAULT_OFFSET = 0
)
func GetLimits(query url.Values) (int, int, error) {
limit := DEFAULT_LIMIT
start_index := DEFAULT_OFFSET
var err error
if query.Get("limit") != "" {
limit, err = strconv.Atoi(query.Get("limit"))
}
if query.Get("start_index") != "" {
start_index, err = strconv.Atoi(query.Get("start_index"))
}
if err != nil {
return 0, 0, err
}
return start_index, limit, nil
}

View file

@ -14,6 +14,10 @@ import (
func (rt *_router) GetLikes(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { func (rt *_router) GetLikes(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
if !authorization.SendErrorIfNotLoggedIn(ctx.Auth.Authorized, rt.db, w, rt.baseLogger) {
return
}
// get the user id from the url // get the user id from the url
uid := ps.ByName("user_id") uid := ps.ByName("user_id")
photo_id, err := strconv.ParseInt(ps.ByName("photo_id"), 10, 64) photo_id, err := strconv.ParseInt(ps.ByName("photo_id"), 10, 64)
@ -23,13 +27,21 @@ func (rt *_router) GetLikes(w http.ResponseWriter, r *http.Request, ps httproute
return return
} }
// get limits, or use defaults
start_index, limit, err := helpers.GetLimits(r.URL.Query())
if err != nil {
helpers.SendBadRequest(w, "Invalid start_index or limit", rt.baseLogger)
return
}
// send 404 if the user does not exist // send 404 if the user does not exist
if !helpers.VerifyUserOrNotFound(rt.db, uid, w, rt.baseLogger) { if !helpers.VerifyUserOrNotFound(rt.db, uid, w, rt.baseLogger) {
return return
} }
// get the user's likes // get the user's likes
success, likes, err := rt.db.GetPhotoLikes(uid, photo_id) success, likes, err := rt.db.GetPhotoLikes(uid, photo_id, ctx.Auth.GetUserID(), start_index, limit)
if err != nil { if err != nil {
helpers.SendInternalError(err, "Database error: GetLikes", w, rt.baseLogger) helpers.SendInternalError(err, "Database error: GetLikes", w, rt.baseLogger)

View file

@ -15,7 +15,7 @@ import (
func (rt *_router) PostPhoto(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { func (rt *_router) PostPhoto(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
defer r.Body.Close() // defer r.Body.Close()
uid := ps.ByName("user_id") uid := ps.ByName("user_id")
@ -70,14 +70,35 @@ func (rt *_router) PostPhoto(w http.ResponseWriter, r *http.Request, ps httprout
func (rt *_router) GetPhoto(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { func (rt *_router) GetPhoto(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
uid := ps.ByName("user_id") if !authorization.SendErrorIfNotLoggedIn(ctx.Auth.Authorized, rt.db, w, rt.baseLogger) {
photo_id := ps.ByName("photo_id") // We want the user to be authenticated
if !helpers.VerifyUserOrNotFound(rt.db, uid, w, rt.baseLogger) {
return return
} }
path := rt.dataPath + "/photos/" + uid + "/" + photo_id + ".jpg" uid := ps.ByName("user_id")
photo_id_str := ps.ByName("photo_id")
photo_id, err := strconv.ParseInt(photo_id_str, 10, 64)
if err != nil {
helpers.SendBadRequest(w, "Invalid photo id", rt.baseLogger)
return
}
// This is also checking if the requesting user is banned by the author of the photo
exists, err := rt.db.PhotoExists(uid, photo_id, ctx.Auth.GetUserID())
if err != nil {
helpers.SendInternalError(err, "Database error: PhotoExists", w, rt.baseLogger)
return
}
if !exists {
helpers.SendNotFound(w, "Resource not found", rt.baseLogger)
return
}
path := rt.dataPath + "/photos/" + uid + "/" + photo_id_str + ".jpg"
file, err := os.Open(path) file, err := os.Open(path)
@ -88,7 +109,12 @@ func (rt *_router) GetPhoto(w http.ResponseWriter, r *http.Request, ps httproute
defer file.Close() defer file.Close()
io.Copy(w, file) _, err = io.Copy(w, file)
if err != nil {
helpers.SendInternalError(err, "Error writing response", w, rt.baseLogger)
return
}
} }
func (rt *_router) DeletePhoto(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { func (rt *_router) DeletePhoto(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {

View file

@ -23,14 +23,13 @@ type _respbody struct {
func (rt *_router) PostSession(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { func (rt *_router) PostSession(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
var request _reqbody var request _reqbody
err := json.NewDecoder(r.Body).Decode(&request) //todo: capire se serve close err := json.NewDecoder(r.Body).Decode(&request)
var uid string var uid string
if err == nil { // test if user exists if err == nil { // test if user exists
uid, err = rt.db.GetUserID(request.Name) uid, err = rt.db.GetUserID(request.Name)
} }
if db_errors.EmptySet(err) { // user does not exist if db_errors.EmptySet(err) { // user does not exist
err = nil
uid, err = rt.db.CreateUser(request.Name) uid, err = rt.db.CreateUser(request.Name)
} }
if err != nil { // handle any other error if err != nil { // handle any other error

View file

@ -2,11 +2,13 @@ package api
import ( import (
"net/http" "net/http"
"regexp"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/notherealmarco/WASAPhoto/service/api/authorization" "github.com/notherealmarco/WASAPhoto/service/api/authorization"
"github.com/notherealmarco/WASAPhoto/service/api/helpers" "github.com/notherealmarco/WASAPhoto/service/api/helpers"
"github.com/notherealmarco/WASAPhoto/service/api/reqcontext" "github.com/notherealmarco/WASAPhoto/service/api/reqcontext"
"github.com/notherealmarco/WASAPhoto/service/database"
"github.com/notherealmarco/WASAPhoto/service/structures" "github.com/notherealmarco/WASAPhoto/service/structures"
) )
@ -21,19 +23,29 @@ func (rt *_router) UpdateUsername(w http.ResponseWriter, r *http.Request, ps htt
return return
} }
//err := json.NewDecoder(r.Body).Decode(&req) //todo: capire se serve close stat, err := regexp.Match(`^[a-zA-Z0-9_]{3,16}$`, []byte(req.Name))
//if err != nil {
// w.WriteHeader(http.StatusBadRequest) // todo: move to DecodeOrBadRequest helper
// return
//}
err := rt.db.UpdateUsername(uid, req.Name)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) // todo: is not ok, maybe let's use a helper helpers.SendInternalError(err, "Error while matching username", w, rt.baseLogger)
return return
} }
w.WriteHeader(http.StatusNoContent) // todo: change to 204 also in API spec if !stat { //todo: sta regex non me piace
helpers.SendBadRequest(w, "Username must be between 3 and 16 characters long and can only contain letters, numbers and underscores", rt.baseLogger)
return
}
status, err := rt.db.UpdateUsername(uid, req.Name)
if status == database.ERR_EXISTS {
helpers.SendStatus(http.StatusConflict, w, "Username already exists", rt.baseLogger)
return
}
if err != nil {
helpers.SendInternalError(err, "Database error: UpdateUsername", w, rt.baseLogger)
return
}
w.WriteHeader(http.StatusNoContent)
} }

View file

@ -0,0 +1,19 @@
package reqcontext
import "github.com/notherealmarco/WASAPhoto/service/database"
type AuthStatus int
const (
AUTHORIZED = 0
UNAUTHORIZED = 1
FORBIDDEN = 2
USER_NOT_FOUND = 3
)
type Authorization interface {
GetType() string
GetUserID() string
Authorized(db database.AppDatabase) (AuthStatus, error)
UserAuthorized(db database.AppDatabase, uid string) (AuthStatus, error)
}

View file

@ -8,19 +8,9 @@ package reqcontext
import ( import (
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/notherealmarco/WASAPhoto/service/database"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type AuthStatus int
const (
AUTHORIZED = 0
UNAUTHORIZED = 1
FORBIDDEN = 2
USER_NOT_FOUND = 3
) // todo: here?
// RequestContext is the context of the request, for request-dependent parameters // RequestContext is the context of the request, for request-dependent parameters
type RequestContext struct { type RequestContext struct {
// ReqUUID is the request unique ID // ReqUUID is the request unique ID
@ -31,10 +21,3 @@ type RequestContext struct {
Auth Authorization Auth Authorization
} }
type Authorization interface {
GetType() string
GetUserID() string
Authorized(db database.AppDatabase) (AuthStatus, error)
UserAuthorized(db database.AppDatabase, uid string) (AuthStatus, error)
}

53
service/api/search.go Normal file
View file

@ -0,0 +1,53 @@
package api
import (
"encoding/json"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/notherealmarco/WASAPhoto/service/api/authorization"
"github.com/notherealmarco/WASAPhoto/service/api/helpers"
"github.com/notherealmarco/WASAPhoto/service/api/reqcontext"
)
func (rt *_router) GetSearchUsers(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
// We require user to be authenticated
if !authorization.SendErrorIfNotLoggedIn(ctx.Auth.Authorized, rt.db, w, rt.baseLogger) {
return
}
// Get search query
query := r.URL.Query().Get("query")
if query == "" {
helpers.SendBadRequest(w, "Missing query parameter", rt.baseLogger)
return
}
// Get start index and limit, or their default values
start_index, limit, err := helpers.GetLimits(r.URL.Query())
if err != nil {
helpers.SendBadRequest(w, "Invalid start_index or limit value", rt.baseLogger)
return
}
// Get search results
results, err := rt.db.SearchByName(query, ctx.Auth.GetUserID(), start_index, limit)
if err != nil {
helpers.SendInternalError(err, "Database error: SearchByName", w, rt.baseLogger)
return
}
// Return the results in json format
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(results)
if err != nil {
helpers.SendInternalError(err, "Error encoding json", w, rt.baseLogger)
return
}
}

View file

@ -42,28 +42,36 @@ import (
type AppDatabase interface { type AppDatabase interface {
CreateUser(name string) (string, error) CreateUser(name string) (string, error)
UserExists(uid string) (bool, error) UserExists(uid string) (bool, error)
UserExistsNotBanned(uid string, requesting_uid string) (bool, error)
GetUserID(name string) (string, error) GetUserID(name string) (string, error)
UpdateUsername(uid, name string) error SearchByName(name string, requesting_uid string, start_index int, limit int) (*[]structures.UIDName, error)
GetUserFollowers(uid string) (QueryResult, *[]structures.UIDName, error) // todo: maybe use a pointer to a slice? UpdateUsername(uid string, name string) (QueryResult, error)
GetUserFollowing(uid string) (QueryResult, *[]structures.UIDName, error)
GetUserFollowers(uid string, requesting_uid string, start_index int, limit int) (QueryResult, *[]structures.UIDName, error)
GetUserFollowing(uid string, requesting_uid string, start_index int, offset int) (QueryResult, *[]structures.UIDName, error)
FollowUser(uid string, follow string) (QueryResult, error) FollowUser(uid string, follow string) (QueryResult, error)
UnfollowUser(uid string, unfollow string) (QueryResult, error) UnfollowUser(uid string, unfollow string) (QueryResult, error)
BanUser(uid string, ban string) (QueryResult, error) BanUser(uid string, ban string) (QueryResult, error)
UnbanUser(uid string, unban string) (QueryResult, error) UnbanUser(uid string, unban string) (QueryResult, error)
IsBanned(uid string, banner string) (bool, error)
GetUserBans(uid string, start_index int, limit int) (*[]structures.UIDName, error)
PostPhoto(uid string) (DBTransaction, int64, error) PostPhoto(uid string) (DBTransaction, int64, error)
DeletePhoto(uid string, photo int64) (bool, error) DeletePhoto(uid string, photo int64) (bool, error)
PhotoExists(uid string, photo int64, requesting_uid string) (bool, error)
GetPhotoLikes(uid string, photo int64) (QueryResult, *[]structures.UIDName, error) GetPhotoLikes(uid string, photo int64, requesting_uid string, start_index int, offset int) (QueryResult, *[]structures.UIDName, error)
LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error)
UnlikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) UnlikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error)
GetUserProfile(uid string) (*UserProfile, error) GetUserProfile(uid string, requesting_uid string) (QueryResult, *structures.UserProfile, error)
GetUserPhotos(uid string, requesting_uid string, start_index int, limit int) (*[]structures.UserPhoto, error)
GetUserStream(uid string, start_index int, limit int) (*[]structures.Photo, error)
GetComments(uid string, photo_id int64) (QueryResult, *[]structures.Comment, error) GetComments(uid string, photo_id int64, requesting_uid string, start_index int, offset int) (QueryResult, *[]structures.Comment, error)
PostComment(uid string, photo_id int64, comment_user string, comment string) (QueryResult, error) PostComment(uid string, photo_id int64, comment_user string, comment string) (QueryResult, error)
DeleteComment(uid string, photo_id int64, comment_id int64) (QueryResult, error) DeleteComment(uid string, photo_id int64, comment_id int64) (QueryResult, error)
GetCommentOwner(uid string, photo_id int64, comment_id int64) (QueryResult, string, error) GetCommentOwner(uid string, photo_id int64, comment_id int64) (QueryResult, string, error)

View file

@ -12,7 +12,10 @@ func (db *appdbimpl) PostComment(uid string, photo_id int64, comment_user string
// Check if the photo exists, as API specification requires // Check if the photo exists, as API specification requires
// photos to be identified also by the user who posted them. // photos to be identified also by the user who posted them.
// But our DB implementation only requires the photo id. // But our DB implementation only requires the photo id.
exists, err := db.photoExists(uid, photo_id) //
// This also checks if the author has banned the user who is posting the comment
// as he should not be able to post comments on his photos
exists, err := db.PhotoExists(uid, photo_id, comment_user)
if err != nil || !exists { if err != nil || !exists {
return ERR_NOT_FOUND, err return ERR_NOT_FOUND, err
} }
@ -20,8 +23,9 @@ func (db *appdbimpl) PostComment(uid string, photo_id int64, comment_user string
_, err = db.c.Exec(`PRAGMA foreign_keys = ON; _, err = db.c.Exec(`PRAGMA foreign_keys = ON;
INSERT INTO "comments" ("user", "photo", "comment", "date") VALUES (?, ?, ?, ?)`, comment_user, photo_id, comment, time.Now().Format(time.RFC3339)) INSERT INTO "comments" ("user", "photo", "comment", "date") VALUES (?, ?, ?, ?)`, comment_user, photo_id, comment, time.Now().Format(time.RFC3339))
// todo: we don't actually need it, it's already done before
if db_errors.ForeignKeyViolation(err) { if db_errors.ForeignKeyViolation(err) {
// trying to post a comment on a photo that does not exist
// (actually this should never happen, as we checked if the photo exists before)
return ERR_NOT_FOUND, nil return ERR_NOT_FOUND, nil
} }
@ -33,7 +37,7 @@ func (db *appdbimpl) PostComment(uid string, photo_id int64, comment_user string
func (db *appdbimpl) GetCommentOwner(uid string, photo_id int64, comment_id int64) (QueryResult, string, error) { func (db *appdbimpl) GetCommentOwner(uid string, photo_id int64, comment_id int64) (QueryResult, string, error) {
// Check if the photo exists, as it exist but have no comments // Check if the photo exists, as it may exist but have no comments
exists, err := db.photoExists(uid, photo_id) exists, err := db.photoExists(uid, photo_id)
if err != nil || !exists { if err != nil || !exists {
return ERR_NOT_FOUND, "", err return ERR_NOT_FOUND, "", err
@ -81,32 +85,47 @@ func (db *appdbimpl) DeleteComment(uid string, photo_id int64, comment_id int64)
return SUCCESS, nil return SUCCESS, nil
} }
func (db *appdbimpl) GetComments(uid string, photo_id int64) (QueryResult, *[]structures.Comment, error) { func (db *appdbimpl) GetComments(uid string, photo_id int64, requesting_uid string, start_index int, limit int) (QueryResult, *[]structures.Comment, error) {
// Check if the photo exists, as it exist but have no comments // Check if the photo exists, as it exist but have no comments
exists, err := db.photoExists(uid, photo_id) // this also checks if the author has banned the requesting user
exists, err := db.PhotoExists(uid, photo_id, requesting_uid)
if err != nil || !exists { if err != nil || !exists {
return ERR_NOT_FOUND, nil, err return ERR_NOT_FOUND, nil, err
} }
rows, err := db.c.Query(`SELECT "id", "user", "comment", "date" FROM "comments" WHERE "photo" = ?`, photo_id) rows, err := db.c.Query(`SELECT "c"."id", "c"."user", "c"."comment", "c"."date", "u"."name"
FROM "comments" AS "c", "users" AS "u"
WHERE "c"."photo" = ?
AND "c"."user" NOT IN (
SELECT "bans"."user" FROM "bans"
WHERE "bans"."user" = "c"."user"
AND "bans"."ban" = ?
)
AND "u"."uid" = "c"."user"
LIMIT ?
OFFSET ?`, photo_id, requesting_uid, limit, start_index)
if err != nil { if err != nil {
return ERR_INTERNAL, nil, err return ERR_INTERNAL, nil, err
} }
defer rows.Close()
comments := make([]structures.Comment, 0) comments := make([]structures.Comment, 0)
defer rows.Close()
for rows.Next() { for rows.Next() {
var c structures.Comment var c structures.Comment
err = rows.Scan(&c.CommentID, &c.UID, &c.Comment, &c.Date) err = rows.Scan(&c.CommentID, &c.UID, &c.Comment, &c.Date, &c.Name)
if err != nil { if err != nil {
return ERR_INTERNAL, nil, err return ERR_INTERNAL, nil, err
} }
comments = append(comments, c) comments = append(comments, c)
} }
// We check if the iteration ended prematurely
if err = rows.Err(); err != nil {
return ERR_INTERNAL, nil, err
}
return SUCCESS, &comments, nil return SUCCESS, &comments, nil
} }

View file

@ -0,0 +1,118 @@
package database
import (
"github.com/notherealmarco/WASAPhoto/service/database/db_errors"
"github.com/notherealmarco/WASAPhoto/service/structures"
)
// Get the list of users who liked a photo
func (db *appdbimpl) GetPhotoLikes(uid string, photo int64, requesting_uid string, start_index int, limit int) (QueryResult, *[]structures.UIDName, error) {
// Check if the photo exists, as it could exist but have no likes.
//
// This also checks if the author has banned the requesting user
// as he should not be able to see anything related to his photos
exists, err := db.PhotoExists(uid, photo, requesting_uid)
if err != nil {
return ERR_INTERNAL, nil, err
}
if !exists {
return ERR_NOT_FOUND, nil, nil
}
rows, err := db.c.Query(`SELECT "users"."uid", "users"."name" FROM "likes", "users"
WHERE "likes"."photo_id" = ?
AND "likes"."user" NOT IN (
SELECT "bans"."user" FROM "bans"
WHERE "bans"."user" = "likes"."user"
AND "bans"."ban" = ?
)
AND "likes"."user" = "users"."uid"
LIMIT ?
OFFSET ?`, photo, requesting_uid, limit, start_index)
if err != nil {
return ERR_INTERNAL, nil, err
}
likes := make([]structures.UIDName, 0)
defer rows.Close()
for rows.Next() {
var uid string
var name string
err = rows.Scan(&uid, &name)
if err != nil {
return ERR_INTERNAL, nil, err
}
likes = append(likes, structures.UIDName{UID: uid, Name: name})
}
// We check if the iteration ended prematurely
if err = rows.Err(); err != nil {
return ERR_INTERNAL, nil, err
}
return SUCCESS, &likes, nil
}
// Like a photo
func (db *appdbimpl) LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) {
// Check if the photo exists, as API specification requires
// photos to be identified also by the user who posted them.
// But our DB implementation only requires the photo id.
//
// This also checks if the author of the photo has banned the requesting user
// as he should not be able to like his photos
exists, err := db.PhotoExists(uid, photo, liker_uid)
if err != nil || !exists {
return ERR_NOT_FOUND, err
}
_, err = db.c.Exec(`PRAGMA foreign_keys = ON;
INSERT INTO "likes" ("user", "photo_id") VALUES (?, ?)`, liker_uid, photo)
// The photo exists, but the user already liked it
if db_errors.UniqueViolation(err) {
return ERR_EXISTS, nil
}
if db_errors.ForeignKeyViolation(err) {
return ERR_NOT_FOUND, nil
}
if err != nil {
return ERR_INTERNAL, err
}
return SUCCESS, nil
}
// Unlike a photo
func (db *appdbimpl) UnlikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) {
// Check if the photo exists, as API specification requires
// photos to be identified also by the user who posted them.
// But our DB implementation only requires the photo id.
exists, err := db.photoExists(uid, photo)
if err != nil || !exists {
return ERR_NOT_FOUND, err
}
res, err := db.c.Exec(`DELETE FROM "likes" WHERE "user" = ? AND "photo_id" = ?`, liker_uid, photo)
if err != nil {
return ERR_INTERNAL, err
}
rows, err := res.RowsAffected()
if err != nil {
return ERR_INTERNAL, err
}
if rows == 0 {
return ERR_NOT_FOUND, nil
}
return SUCCESS, nil
}

View file

@ -1,48 +1,10 @@
package database package database
import ( import (
"database/sql" "fmt"
"time" "time"
"github.com/notherealmarco/WASAPhoto/service/database/db_errors"
"github.com/notherealmarco/WASAPhoto/service/structures"
) )
type Photo struct {
ID int64
Likes int64
Comments int64
}
type UserProfile struct {
UID string
Name string
Following int64
Followers int64
Photos []Photo
}
type QueryResult int // todo: move to a separate file
const (
SUCCESS = 0
ERR_NOT_FOUND = 1
ERR_EXISTS = 2
ERR_INTERNAL = 3
)
type dbtransaction struct {
c *sql.Tx
}
func (tx *dbtransaction) Commit() error {
return tx.c.Commit()
}
func (tx *dbtransaction) Rollback() error {
return tx.c.Rollback()
}
// Post a new photo // Post a new photo
func (db *appdbimpl) PostPhoto(uid string) (DBTransaction, int64, error) { func (db *appdbimpl) PostPhoto(uid string) (DBTransaction, int64, error) {
tx, err := db.c.Begin() tx, err := db.c.Begin()
@ -53,13 +15,23 @@ func (db *appdbimpl) PostPhoto(uid string) (DBTransaction, int64, error) {
res, err := tx.Exec(`INSERT INTO "photos" ("user", "date") VALUES (?, ?)`, uid, time.Now().Format(time.RFC3339)) res, err := tx.Exec(`INSERT INTO "photos" ("user", "date") VALUES (?, ?)`, uid, time.Now().Format(time.RFC3339))
if err != nil { if err != nil {
tx.Rollback() // error ? err_rb := tx.Rollback()
// If rollback fails, we return the original error plus the rollback error
if err_rb != nil {
err = fmt.Errorf("%w; %w", err, err_rb)
}
return nil, 0, err return nil, 0, err
} }
id, err := res.LastInsertId() id, err := res.LastInsertId()
if err != nil { if err != nil {
tx.Rollback() // error ? err_rb := tx.Rollback()
// If rollback fails, we return the original error plus the rollback error
if err_rb != nil {
err = fmt.Errorf("%w; %w", err, err_rb)
}
return nil, 0, err return nil, 0, err
} }
@ -81,52 +53,6 @@ func (db *appdbimpl) DeletePhoto(uid string, photo int64) (bool, error) {
return rows > 0, nil return rows > 0, nil
} }
// Get user profile, including username, followers, following, and photos
func (db *appdbimpl) GetUserProfile(uid string) (*UserProfile, error) {
// Get user info
var name string
err := db.c.QueryRow(`SELECT "name" FROM "users" WHERE "uid" = ?`, uid).Scan(&name)
if err != nil {
return nil, err
}
// Get followers
var followers int64
err = db.c.QueryRow(`SELECT COUNT(*) FROM "follows" WHERE "followed" = ?`, uid).Scan(&followers)
// Get following users
var following int64
err = db.c.QueryRow(`SELECT COUNT(*) FROM "follows" WHERE "follower" = ?`, uid).Scan(&following)
// Get photos
rows, err := db.c.Query(`SELECT "photos"."id", "photos"."date",
COUNT("likes"."user") AS "likes",
COUNT("comments"."user") AS "comments"
FROM "photos", "likes", "comments"
WHERE "likes"."photo_id" = "photos"."id"
AND "comments"."photo" = "photos"."id"
AND "user" = ?`, uid)
if err != nil {
return nil, err
}
photos := make([]Photo, 0)
for rows.Next() {
var id int64
var date string
var likes int64
var comments int64
err = rows.Scan(&id, &date, &likes, &comments)
if err != nil {
return nil, err
}
photo_data := Photo{id, likes, comments}
photos = append(photos, photo_data)
}
return &UserProfile{uid, name, followers, following, photos}, nil
}
// Check if a given photo owned by a given user exists // Check if a given photo owned by a given user exists
func (db *appdbimpl) photoExists(uid string, photo int64) (bool, error) { func (db *appdbimpl) photoExists(uid string, photo int64) (bool, error) {
@ -138,94 +64,19 @@ func (db *appdbimpl) photoExists(uid string, photo int64) (bool, error) {
return cnt > 0, nil return cnt > 0, nil
} }
// Get the list of users who liked a photo func (db *appdbimpl) PhotoExists(uid string, photo int64, requesting_uid string) (bool, error) {
func (db *appdbimpl) GetPhotoLikes(uid string, photo int64) (QueryResult, *[]structures.UIDName, error) {
// Check if the photo exists, as it could exist but have no likes var cnt int64
exists, err := db.photoExists(uid, photo) err := db.c.QueryRow(`SELECT COUNT(*) FROM "photos"
WHERE "id" = ?
AND "user" = ?
AND "user" NOT IN (
SELECT "bans"."user" FROM "bans"
WHERE "bans"."user" = "photos"."user"
AND "bans"."ban" = ?
)`, photo, uid, requesting_uid).Scan(&cnt)
if err != nil { if err != nil {
return ERR_INTERNAL, nil, err return false, err
} }
return cnt > 0, nil
if !exists {
return ERR_NOT_FOUND, nil, nil
}
rows, err := db.c.Query(`SELECT "users"."uid", "users"."name" FROM "likes", "users"
WHERE "likes"."photo_id" = ?
AND "likes"."user" = "users"."uid"`, photo)
if err != nil {
return ERR_INTERNAL, nil, err
}
likes := make([]structures.UIDName, 0)
for rows.Next() {
var uid string
var name string
err = rows.Scan(&uid, &name)
if err != nil {
return ERR_INTERNAL, nil, err
}
likes = append(likes, structures.UIDName{UID: uid, Name: name})
}
return SUCCESS, &likes, nil
}
// Like a photo
func (db *appdbimpl) LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) {
// Check if the photo exists, as API specification requires
// photos to be identified also by the user who posted them.
// But our DB implementation only requires the photo id.
exists, err := db.photoExists(uid, photo)
if err != nil || !exists {
return ERR_NOT_FOUND, err
}
_, err = db.c.Exec(`PRAGMA foreign_keys = ON;
INSERT INTO "likes" ("user", "photo_id") VALUES (?, ?)`, liker_uid, photo)
// The photo exists, but the user already liked it
if db_errors.UniqueViolation(err) {
return ERR_EXISTS, nil
}
if db_errors.ForeignKeyViolation(err) {
return ERR_NOT_FOUND, nil
}
if err != nil {
return ERR_INTERNAL, err
}
return SUCCESS, nil
}
// Unlike a photo
func (db *appdbimpl) UnlikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) {
// Check if the photo exists, as API specification requires
// photos to be identified also by the user who posted them.
// But our DB implementation only requires the photo id.
exists, err := db.photoExists(uid, photo)
if err != nil || !exists {
return ERR_NOT_FOUND, err
}
res, err := db.c.Exec(`DELETE FROM "likes" WHERE "user" = ? AND "photo_id" = ?`, liker_uid, photo)
if err != nil {
return ERR_INTERNAL, err
}
rows, err := res.RowsAffected()
if err != nil {
return ERR_INTERNAL, err
}
if rows == 0 {
return ERR_NOT_FOUND, nil
}
return SUCCESS, nil
} }

View file

@ -0,0 +1,114 @@
package database
import (
"github.com/notherealmarco/WASAPhoto/service/database/db_errors"
"github.com/notherealmarco/WASAPhoto/service/structures"
)
//this should be changed, but we need to change OpenAPI first
// Get user profile, including username, followers, following, and photos
func (db *appdbimpl) GetUserProfile(uid string, requesting_uid string) (QueryResult, *structures.UserProfile, error) {
// Get user info
var name string
err := db.c.QueryRow(`SELECT "name" FROM "users" WHERE "uid" = ?`, uid).Scan(&name)
if db_errors.EmptySet(err) {
// Query returned no rows, the user does not exist
return ERR_NOT_FOUND, nil, nil
} else if err != nil {
return ERR_INTERNAL, nil, err
}
// Get followers
var followers int64
err = db.c.QueryRow(`SELECT COUNT(*) FROM "follows" WHERE "followed" = ?`, uid).Scan(&followers)
if err != nil {
// Return the error
return ERR_INTERNAL, nil, err
}
// Get following users
var following int64
err = db.c.QueryRow(`SELECT COUNT(*) FROM "follows" WHERE "follower" = ?`, uid).Scan(&following)
if err != nil {
// Return the error
return ERR_INTERNAL, nil, err
}
var photos int64
err = db.c.QueryRow(`SELECT COUNT(*) FROM "photos" WHERE "photos"."user" = ?`, uid).Scan(&photos)
if err != nil {
return ERR_INTERNAL, nil, err
}
// Get follow status
var follow_status bool
err = db.c.QueryRow(`SELECT EXISTS (SELECT * FROM "follows" WHERE "follower" = ? AND "followed" = ?)`, requesting_uid, uid).Scan(&follow_status)
if err != nil {
return ERR_INTERNAL, nil, err
}
return SUCCESS, &structures.UserProfile{
UID: uid,
Name: name,
Following: following,
Followers: followers,
Followed: follow_status,
Photos: photos,
}, nil
}
func (db *appdbimpl) GetUserPhotos(uid string, requesting_uid string, start_index int, limit int) (*[]structures.UserPhoto, error) {
// Get photos
rows, err := db.c.Query(`SELECT "p"."id", "p"."date",
(
SELECT COUNT(*) AS "likes" FROM "likes" AS "l"
WHERE "l"."photo_id" = "p"."id"
),
(
SELECT COUNT(*) AS "comments" FROM "comments" AS "c"
WHERE "c"."photo" = "p"."id"
),
EXISTS (
SELECT * FROM "likes" AS "l"
WHERE "l"."photo_id" = "p"."id"
AND "l"."user" = ?
)
FROM "photos" AS "p"
WHERE "p"."user" = ?
LIMIT ?
OFFSET ?`, requesting_uid, uid, limit, start_index)
if err != nil {
// Return the error
return nil, err
}
photos := make([]structures.UserPhoto, 0)
defer rows.Close()
for rows.Next() {
// If there is a next row, we create an instance of Photo and add it to the slice
var photo structures.UserPhoto
err = rows.Scan(&photo.ID, &photo.Date, &photo.Likes, &photo.Comments, &photo.Liked)
if err != nil {
// Return the error
return nil, err
}
photos = append(photos, photo)
}
// We check if the iteration ended prematurely
if err = rows.Err(); err != nil {
return nil, err
}
return &photos, nil
}

View file

@ -0,0 +1,11 @@
package database
type QueryResult int
// Constants used to represent the result of queries
const (
SUCCESS = 0
ERR_NOT_FOUND = 1
ERR_EXISTS = 2
ERR_INTERNAL = 3
)

View file

@ -0,0 +1,55 @@
package database
import (
"github.com/notherealmarco/WASAPhoto/service/structures"
)
// Get user stream
func (db *appdbimpl) GetUserStream(uid string, start_index int, limit int) (*[]structures.Photo, error) {
// Get photos from the database
rows, err := db.c.Query(`SELECT "p"."user", "p"."id", "p"."date",
(
SELECT COUNT(*) AS "likes" FROM "likes" AS "l"
WHERE "l"."photo_id" = "p"."id"
),
(
SELECT COUNT(*) AS "comments" FROM "comments" AS "c"
WHERE "c"."photo" = "p"."id"
)
FROM "photos" AS "p"
WHERE "p"."user" IN (
SELECT "followed" FROM "follows" WHERE "follower" = ?
)
AND "p"."user" NOT IN (
SELECT "user" FROM "bans" WHERE "ban" = ?
)
ORDER BY "p"."date" DESC
LIMIT ?
OFFSET ?`, uid, uid, limit, start_index)
if err != nil {
// Return the error
return nil, err
}
photos := make([]structures.Photo, 0)
defer rows.Close()
for rows.Next() {
// If there is a next row, we create an instance of Photo and add it to the slice
var photo structures.Photo
err = rows.Scan(&photo.UID, &photo.ID, &photo.Date, &photo.Likes, &photo.Comments)
if err != nil {
// Return the error
return nil, err
}
photos = append(photos, photo)
}
// We check if the iteration ended prematurely
if err = rows.Err(); err != nil {
return nil, err
}
return &photos, nil
}

View file

@ -0,0 +1,15 @@
package database
import "database/sql"
type dbtransaction struct {
c *sql.Tx
}
func (tx *dbtransaction) Commit() error {
return tx.c.Commit()
}
func (tx *dbtransaction) Rollback() error {
return tx.c.Rollback()
}

View file

@ -8,20 +8,34 @@ import (
"github.com/notherealmarco/WASAPhoto/service/structures" "github.com/notherealmarco/WASAPhoto/service/structures"
) )
//Check if user exists and if exists return the user id by username
//todo
// Check if user exists // Check if user exists
func (db *appdbimpl) UserExists(uid string) (bool, error) { func (db *appdbimpl) UserExists(uid string) (bool, error) {
var name string
err := db.c.QueryRow(`SELECT "name" FROM "users" WHERE "uid" = ?`, uid).Scan(&name)
if db_errors.EmptySet(err) { var cnt int
return false, nil err := db.c.QueryRow(`SELECT COUNT(*) FROM "users" WHERE "uid" = ?`, uid).Scan(&cnt)
} else if err != nil {
if err != nil {
return false, err return false, err
} }
return true, nil return cnt > 0, nil
}
// User exists and is not banned
func (db *appdbimpl) UserExistsNotBanned(uid string, requesting_uid string) (bool, error) {
var cnt int
err := db.c.QueryRow(`SELECT COUNT(*) FROM "users"
WHERE "uid" = ?
AND NOT EXISTS (
SELECT "bans"."user" FROM "bans"
WHERE "bans"."user" = "users"."uid"
AND "bans"."ban" = ?
)`, uid, requesting_uid).Scan(&cnt)
if err != nil {
return false, err
}
return cnt > 0, nil
} }
// Get user id by username // Get user id by username
@ -42,16 +56,25 @@ func (db *appdbimpl) CreateUser(name string) (string, error) {
} }
// Update username // Update username
func (db *appdbimpl) UpdateUsername(uid string, name string) error { func (db *appdbimpl) UpdateUsername(uid string, name string) (QueryResult, error) {
_, err := db.c.Exec(`UPDATE "users" SET "name" = ? WHERE "uid" = ?`, name, uid) _, err := db.c.Exec(`UPDATE "users" SET "name" = ? WHERE "uid" = ?`, name, uid)
return err
if db_errors.UniqueViolation(err) {
return ERR_EXISTS, nil
}
if err != nil {
return ERR_INTERNAL, err
}
return SUCCESS, err
} }
// Get user followers // Get user followers
func (db *appdbimpl) GetUserFollowers(uid string) (QueryResult, *[]structures.UIDName, error) { func (db *appdbimpl) GetUserFollowers(uid string, requesting_uid string, start_index int, limit int) (QueryResult, *[]structures.UIDName, error) {
// user may exist but have no followers // user may exist but have no followers
exists, err := db.UserExists(uid) exists, err := db.UserExistsNotBanned(uid, requesting_uid)
if err != nil { if err != nil {
return ERR_INTERNAL, nil, err return ERR_INTERNAL, nil, err
@ -61,9 +84,18 @@ func (db *appdbimpl) GetUserFollowers(uid string) (QueryResult, *[]structures.UI
return ERR_NOT_FOUND, nil, nil return ERR_NOT_FOUND, nil, nil
} }
rows, err := db.c.Query(`SELECT "follower", "user"."name" FROM "follows", "users" rows, err := db.c.Query(`SELECT "follower", "users"."name" FROM "follows", "users"
WHERE "follows"."follower" = "users"."uid" WHERE "follows"."follower" = "users"."uid"
AND "followed" = ?`, uid)
AND "follows"."follower" NOT IN (
SELECT "bans"."user" FROM "bans"
WHERE "bans"."user" = "follows"."follower"
AND "bans"."ban" = ?
)
AND "followed" = ?
LIMIT ?
OFFSET ?`, uid, requesting_uid, limit, start_index)
followers, err := db.uidNameQuery(rows, err) followers, err := db.uidNameQuery(rows, err)
@ -75,10 +107,10 @@ func (db *appdbimpl) GetUserFollowers(uid string) (QueryResult, *[]structures.UI
} }
// Get user following // Get user following
func (db *appdbimpl) GetUserFollowing(uid string) (QueryResult, *[]structures.UIDName, error) { func (db *appdbimpl) GetUserFollowing(uid string, requesting_uid string, start_index int, offset int) (QueryResult, *[]structures.UIDName, error) {
// user may exist but have no followers // user may exist but have no followers
exists, err := db.UserExists(uid) exists, err := db.UserExistsNotBanned(uid, requesting_uid)
if err != nil { if err != nil {
return ERR_INTERNAL, nil, err return ERR_INTERNAL, nil, err
@ -90,7 +122,16 @@ func (db *appdbimpl) GetUserFollowing(uid string) (QueryResult, *[]structures.UI
rows, err := db.c.Query(`SELECT "followed", "user"."name" FROM "follows", "users" rows, err := db.c.Query(`SELECT "followed", "user"."name" FROM "follows", "users"
WHERE "follows"."followed" = "users"."uid" WHERE "follows"."followed" = "users"."uid"
AND "follower" = ?`, uid)
AND "follows"."followed" NOT IN (
SELECT "bans"."user" FROM "bans"
WHERE "bans"."user" = "follows"."followed"
AND "bans"."ban" = ?
)
AND "follower" = ?
LIMIT ?
OFFSET ?`, uid, requesting_uid, offset, start_index)
following, err := db.uidNameQuery(rows, err) following, err := db.uidNameQuery(rows, err)
@ -109,6 +150,9 @@ func (db *appdbimpl) uidNameQuery(rows *sql.Rows, err error) (*[]structures.UIDN
} }
var followers []structures.UIDName = make([]structures.UIDName, 0) var followers []structures.UIDName = make([]structures.UIDName, 0)
defer rows.Close()
for rows.Next() { for rows.Next() {
var uid string var uid string
var name string var name string
@ -118,6 +162,11 @@ func (db *appdbimpl) uidNameQuery(rows *sql.Rows, err error) (*[]structures.UIDN
} }
followers = append(followers, structures.UIDName{UID: uid, Name: name}) followers = append(followers, structures.UIDName{UID: uid, Name: name})
} }
// We check if the iteration ended prematurely
if err = rows.Err(); err != nil {
return nil, err
}
return &followers, nil return &followers, nil
} }
@ -203,3 +252,56 @@ func (db *appdbimpl) UnbanUser(uid string, unban string) (QueryResult, error) {
} }
return SUCCESS, nil return SUCCESS, nil
} }
// Is user banned by another user
func (db *appdbimpl) IsBanned(uid string, banner string) (bool, error) {
var cnt int
err := db.c.QueryRow(`SELECT COUNT(*) FROM "bans" WHERE "user" = ? AND "ban" = ?`, banner, uid).Scan(&cnt)
if err != nil {
return false, err
}
return cnt > 0, nil
}
func (db *appdbimpl) GetUserBans(uid string, start_index int, limit int) (*[]structures.UIDName, error) {
rows, err := db.c.Query(`SELECT "ban", "users"."name" FROM "bans", "users"
WHERE "bans"."ban" = "users"."uid"
AND "bans"."user" = ?
LIMIT ?
OFFSET ?`, uid, limit, start_index)
bans, err := db.uidNameQuery(rows, err)
if err != nil {
return nil, err
}
return bans, nil
}
// Search by name
func (db *appdbimpl) SearchByName(name string, requesting_uid string, start_index int, limit int) (*[]structures.UIDName, error) {
rows, err := db.c.Query(`SELECT "uid", "name" FROM "users"
WHERE "name" LIKE '%' || ? || '%'
AND "uid" NOT IN (
SELECT "bans"."user" FROM "bans"
WHERE "bans"."user" = "users"."uid"
AND "bans"."ban" = ?
)
LIMIT ?
OFFSET ?`, name, requesting_uid, limit, start_index)
users, err := db.uidNameQuery(rows, err)
if err != nil {
return nil, err
}
return users, nil
}

View file

@ -20,3 +20,29 @@ type Comment struct {
Comment string `json:"comment"` Comment string `json:"comment"`
Date string `json:"date"` Date string `json:"date"`
} }
type Photo struct {
UID string `json:"user_id"`
ID int64 `json:"photo_id"`
Likes int64 `json:"likes"`
Comments int64 `json:"comments"`
Date string `json:"date"`
Liked bool `json:"liked"`
}
type UserPhoto struct {
ID int64 `json:"photo_id"`
Likes int64 `json:"likes"`
Comments int64 `json:"comments"`
Date string `json:"date"`
Liked bool `json:"liked"`
}
type UserProfile struct {
UID string `json:"user_id"`
Name string `json:"name"`
Following int64 `json:"following"`
Followers int64 `json:"followers"`
Followed bool `json:"followed"`
Photos int64 `json:"photos"`
}

View file

@ -1,9 +0,0 @@
language: go
go:
- 1.4.3
- 1.5.3
- tip
script:
- go test -v ./...

View file

@ -1,10 +0,0 @@
# How to contribute
We definitely welcome patches and contribution to this project!
### Legal requirements
In order to protect both you and ourselves, you will need to sign the
[Contributor License Agreement](https://cla.developers.google.com/clas).
You may have already signed it for other Google projects.

View file

@ -1,9 +0,0 @@
Paul Borman <borman@google.com>
bmatsuo
shawnps
theory
jboverfelt
dsymonds
cd1
wallclockbuilder
dansouza

View file

@ -1,27 +0,0 @@
Copyright (c) 2009,2014 Google Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -1,19 +0,0 @@
# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master)
The uuid package generates and inspects UUIDs based on
[RFC 4122](http://tools.ietf.org/html/rfc4122)
and DCE 1.1: Authentication and Security Services.
This package is based on the github.com/pborman/uuid package (previously named
code.google.com/p/go-uuid). It differs from these earlier packages in that
a UUID is a 16 byte array rather than a byte slice. One loss due to this
change is the ability to represent an invalid UUID (vs a NIL UUID).
###### Install
`go get github.com/google/uuid`
###### Documentation
[![GoDoc](https://godoc.org/github.com/google/uuid?status.svg)](http://godoc.org/github.com/google/uuid)
Full `go doc` style documentation for the package can be viewed online without
installing this package by using the GoDoc site here:
http://pkg.go.dev/github.com/google/uuid

80
vendor/github.com/google/uuid/dce.go generated vendored
View file

@ -1,80 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
"fmt"
"os"
)
// A Domain represents a Version 2 domain
type Domain byte
// Domain constants for DCE Security (Version 2) UUIDs.
const (
Person = Domain(0)
Group = Domain(1)
Org = Domain(2)
)
// NewDCESecurity returns a DCE Security (Version 2) UUID.
//
// The domain should be one of Person, Group or Org.
// On a POSIX system the id should be the users UID for the Person
// domain and the users GID for the Group. The meaning of id for
// the domain Org or on non-POSIX systems is site defined.
//
// For a given domain/id pair the same token may be returned for up to
// 7 minutes and 10 seconds.
func NewDCESecurity(domain Domain, id uint32) (UUID, error) {
uuid, err := NewUUID()
if err == nil {
uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2
uuid[9] = byte(domain)
binary.BigEndian.PutUint32(uuid[0:], id)
}
return uuid, err
}
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person
// domain with the id returned by os.Getuid.
//
// NewDCESecurity(Person, uint32(os.Getuid()))
func NewDCEPerson() (UUID, error) {
return NewDCESecurity(Person, uint32(os.Getuid()))
}
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group
// domain with the id returned by os.Getgid.
//
// NewDCESecurity(Group, uint32(os.Getgid()))
func NewDCEGroup() (UUID, error) {
return NewDCESecurity(Group, uint32(os.Getgid()))
}
// Domain returns the domain for a Version 2 UUID. Domains are only defined
// for Version 2 UUIDs.
func (uuid UUID) Domain() Domain {
return Domain(uuid[9])
}
// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2
// UUIDs.
func (uuid UUID) ID() uint32 {
return binary.BigEndian.Uint32(uuid[0:4])
}
func (d Domain) String() string {
switch d {
case Person:
return "Person"
case Group:
return "Group"
case Org:
return "Org"
}
return fmt.Sprintf("Domain%d", int(d))
}

12
vendor/github.com/google/uuid/doc.go generated vendored
View file

@ -1,12 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package uuid generates and inspects UUIDs.
//
// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security
// Services.
//
// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to
// maps or compared directly.
package uuid

View file

@ -1,53 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"crypto/md5"
"crypto/sha1"
"hash"
)
// Well known namespace IDs and UUIDs
var (
NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
Nil UUID // empty UUID, all zeros
)
// NewHash returns a new UUID derived from the hash of space concatenated with
// data generated by h. The hash should be at least 16 byte in length. The
// first 16 bytes of the hash are used to form the UUID. The version of the
// UUID will be the lower 4 bits of version. NewHash is used to implement
// NewMD5 and NewSHA1.
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
h.Reset()
h.Write(space[:]) //nolint:errcheck
h.Write(data) //nolint:errcheck
s := h.Sum(nil)
var uuid UUID
copy(uuid[:], s)
uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4)
uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant
return uuid
}
// NewMD5 returns a new MD5 (Version 3) UUID based on the
// supplied name space and data. It is the same as calling:
//
// NewHash(md5.New(), space, data, 3)
func NewMD5(space UUID, data []byte) UUID {
return NewHash(md5.New(), space, data, 3)
}
// NewSHA1 returns a new SHA1 (Version 5) UUID based on the
// supplied name space and data. It is the same as calling:
//
// NewHash(sha1.New(), space, data, 5)
func NewSHA1(space UUID, data []byte) UUID {
return NewHash(sha1.New(), space, data, 5)
}

View file

@ -1,38 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "fmt"
// MarshalText implements encoding.TextMarshaler.
func (uuid UUID) MarshalText() ([]byte, error) {
var js [36]byte
encodeHex(js[:], uuid)
return js[:], nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (uuid *UUID) UnmarshalText(data []byte) error {
id, err := ParseBytes(data)
if err != nil {
return err
}
*uuid = id
return nil
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (uuid UUID) MarshalBinary() ([]byte, error) {
return uuid[:], nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (uuid *UUID) UnmarshalBinary(data []byte) error {
if len(data) != 16 {
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
}
copy(uuid[:], data)
return nil
}

View file

@ -1,90 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"sync"
)
var (
nodeMu sync.Mutex
ifname string // name of interface being used
nodeID [6]byte // hardware for version 1 UUIDs
zeroID [6]byte // nodeID with only 0's
)
// NodeInterface returns the name of the interface from which the NodeID was
// derived. The interface "user" is returned if the NodeID was set by
// SetNodeID.
func NodeInterface() string {
defer nodeMu.Unlock()
nodeMu.Lock()
return ifname
}
// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs.
// If name is "" then the first usable interface found will be used or a random
// Node ID will be generated. If a named interface cannot be found then false
// is returned.
//
// SetNodeInterface never fails when name is "".
func SetNodeInterface(name string) bool {
defer nodeMu.Unlock()
nodeMu.Lock()
return setNodeInterface(name)
}
func setNodeInterface(name string) bool {
iname, addr := getHardwareInterface(name) // null implementation for js
if iname != "" && addr != nil {
ifname = iname
copy(nodeID[:], addr)
return true
}
// We found no interfaces with a valid hardware address. If name
// does not specify a specific interface generate a random Node ID
// (section 4.1.6)
if name == "" {
ifname = "random"
randomBits(nodeID[:])
return true
}
return false
}
// NodeID returns a slice of a copy of the current Node ID, setting the Node ID
// if not already set.
func NodeID() []byte {
defer nodeMu.Unlock()
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
nid := nodeID
return nid[:]
}
// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes
// of id are used. If id is less than 6 bytes then false is returned and the
// Node ID is not set.
func SetNodeID(id []byte) bool {
if len(id) < 6 {
return false
}
defer nodeMu.Unlock()
nodeMu.Lock()
copy(nodeID[:], id)
ifname = "user"
return true
}
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
func (uuid UUID) NodeID() []byte {
var node [6]byte
copy(node[:], uuid[10:])
return node[:]
}

View file

@ -1,12 +0,0 @@
// Copyright 2017 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build js
package uuid
// getHardwareInterface returns nil values for the JS version of the code.
// This remvoves the "net" dependency, because it is not used in the browser.
// Using the "net" library inflates the size of the transpiled JS code by 673k bytes.
func getHardwareInterface(name string) (string, []byte) { return "", nil }

View file

@ -1,33 +0,0 @@
// Copyright 2017 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !js
package uuid
import "net"
var interfaces []net.Interface // cached list of interfaces
// getHardwareInterface returns the name and hardware address of interface name.
// If name is "" then the name and hardware address of one of the system's
// interfaces is returned. If no interfaces are found (name does not exist or
// there are no interfaces) then "", nil is returned.
//
// Only addresses of at least 6 bytes are returned.
func getHardwareInterface(name string) (string, []byte) {
if interfaces == nil {
var err error
interfaces, err = net.Interfaces()
if err != nil {
return "", nil
}
}
for _, ifs := range interfaces {
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
return ifs.Name, ifs.HardwareAddr
}
}
return "", nil
}

118
vendor/github.com/google/uuid/null.go generated vendored
View file

@ -1,118 +0,0 @@
// Copyright 2021 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"bytes"
"database/sql/driver"
"encoding/json"
"fmt"
)
var jsonNull = []byte("null")
// NullUUID represents a UUID that may be null.
// NullUUID implements the SQL driver.Scanner interface so
// it can be used as a scan destination:
//
// var u uuid.NullUUID
// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u)
// ...
// if u.Valid {
// // use u.UUID
// } else {
// // NULL value
// }
//
type NullUUID struct {
UUID UUID
Valid bool // Valid is true if UUID is not NULL
}
// Scan implements the SQL driver.Scanner interface.
func (nu *NullUUID) Scan(value interface{}) error {
if value == nil {
nu.UUID, nu.Valid = Nil, false
return nil
}
err := nu.UUID.Scan(value)
if err != nil {
nu.Valid = false
return err
}
nu.Valid = true
return nil
}
// Value implements the driver Valuer interface.
func (nu NullUUID) Value() (driver.Value, error) {
if !nu.Valid {
return nil, nil
}
// Delegate to UUID Value function
return nu.UUID.Value()
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (nu NullUUID) MarshalBinary() ([]byte, error) {
if nu.Valid {
return nu.UUID[:], nil
}
return []byte(nil), nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (nu *NullUUID) UnmarshalBinary(data []byte) error {
if len(data) != 16 {
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
}
copy(nu.UUID[:], data)
nu.Valid = true
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (nu NullUUID) MarshalText() ([]byte, error) {
if nu.Valid {
return nu.UUID.MarshalText()
}
return jsonNull, nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (nu *NullUUID) UnmarshalText(data []byte) error {
id, err := ParseBytes(data)
if err != nil {
nu.Valid = false
return err
}
nu.UUID = id
nu.Valid = true
return nil
}
// MarshalJSON implements json.Marshaler.
func (nu NullUUID) MarshalJSON() ([]byte, error) {
if nu.Valid {
return json.Marshal(nu.UUID)
}
return jsonNull, nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (nu *NullUUID) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, jsonNull) {
*nu = NullUUID{}
return nil // valid null UUID
}
err := json.Unmarshal(data, &nu.UUID)
nu.Valid = err == nil
return err
}

59
vendor/github.com/google/uuid/sql.go generated vendored
View file

@ -1,59 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"database/sql/driver"
"fmt"
)
// Scan implements sql.Scanner so UUIDs can be read from databases transparently.
// Currently, database types that map to string and []byte are supported. Please
// consult database-specific driver documentation for matching types.
func (uuid *UUID) Scan(src interface{}) error {
switch src := src.(type) {
case nil:
return nil
case string:
// if an empty UUID comes from a table, we return a null UUID
if src == "" {
return nil
}
// see Parse for required string format
u, err := Parse(src)
if err != nil {
return fmt.Errorf("Scan: %v", err)
}
*uuid = u
case []byte:
// if an empty UUID comes from a table, we return a null UUID
if len(src) == 0 {
return nil
}
// assumes a simple slice of bytes if 16 bytes
// otherwise attempts to parse
if len(src) != 16 {
return uuid.Scan(string(src))
}
copy((*uuid)[:], src)
default:
return fmt.Errorf("Scan: unable to scan type %T into UUID", src)
}
return nil
}
// Value implements sql.Valuer so that UUIDs can be written to databases
// transparently. Currently, UUIDs map to strings. Please consult
// database-specific driver documentation for matching types.
func (uuid UUID) Value() (driver.Value, error) {
return uuid.String(), nil
}

123
vendor/github.com/google/uuid/time.go generated vendored
View file

@ -1,123 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
"sync"
"time"
)
// A Time represents a time as the number of 100's of nanoseconds since 15 Oct
// 1582.
type Time int64
const (
lillian = 2299160 // Julian day of 15 Oct 1582
unix = 2440587 // Julian day of 1 Jan 1970
epoch = unix - lillian // Days between epochs
g1582 = epoch * 86400 // seconds between epochs
g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs
)
var (
timeMu sync.Mutex
lasttime uint64 // last time we returned
clockSeq uint16 // clock sequence for this run
timeNow = time.Now // for testing
)
// UnixTime converts t the number of seconds and nanoseconds using the Unix
// epoch of 1 Jan 1970.
func (t Time) UnixTime() (sec, nsec int64) {
sec = int64(t - g1582ns100)
nsec = (sec % 10000000) * 100
sec /= 10000000
return sec, nsec
}
// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and
// clock sequence as well as adjusting the clock sequence as needed. An error
// is returned if the current time cannot be determined.
func GetTime() (Time, uint16, error) {
defer timeMu.Unlock()
timeMu.Lock()
return getTime()
}
func getTime() (Time, uint16, error) {
t := timeNow()
// If we don't have a clock sequence already, set one.
if clockSeq == 0 {
setClockSequence(-1)
}
now := uint64(t.UnixNano()/100) + g1582ns100
// If time has gone backwards with this clock sequence then we
// increment the clock sequence
if now <= lasttime {
clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000
}
lasttime = now
return Time(now), clockSeq, nil
}
// ClockSequence returns the current clock sequence, generating one if not
// already set. The clock sequence is only used for Version 1 UUIDs.
//
// The uuid package does not use global static storage for the clock sequence or
// the last time a UUID was generated. Unless SetClockSequence is used, a new
// random clock sequence is generated the first time a clock sequence is
// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1)
func ClockSequence() int {
defer timeMu.Unlock()
timeMu.Lock()
return clockSequence()
}
func clockSequence() int {
if clockSeq == 0 {
setClockSequence(-1)
}
return int(clockSeq & 0x3fff)
}
// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to
// -1 causes a new sequence to be generated.
func SetClockSequence(seq int) {
defer timeMu.Unlock()
timeMu.Lock()
setClockSequence(seq)
}
func setClockSequence(seq int) {
if seq == -1 {
var b [2]byte
randomBits(b[:]) // clock sequence
seq = int(b[0])<<8 | int(b[1])
}
oldSeq := clockSeq
clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant
if oldSeq != clockSeq {
lasttime = 0
}
}
// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
// uuid. The time is only defined for version 1 and 2 UUIDs.
func (uuid UUID) Time() Time {
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
return Time(time)
}
// ClockSequence returns the clock sequence encoded in uuid.
// The clock sequence is only well defined for version 1 and 2 UUIDs.
func (uuid UUID) ClockSequence() int {
return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff
}

View file

@ -1,43 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"io"
)
// randomBits completely fills slice b with random data.
func randomBits(b []byte) {
if _, err := io.ReadFull(rander, b); err != nil {
panic(err.Error()) // rand should never fail
}
}
// xvalues returns the value of a byte as a hexadecimal digit or 255.
var xvalues = [256]byte{
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
}
// xtob converts hex characters x1 and x2 into a byte.
func xtob(x1, x2 byte) (byte, bool) {
b1 := xvalues[x1]
b2 := xvalues[x2]
return (b1 << 4) | b2, b1 != 255 && b2 != 255
}

294
vendor/github.com/google/uuid/uuid.go generated vendored
View file

@ -1,294 +0,0 @@
// Copyright 2018 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"bytes"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"strings"
"sync"
)
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
// 4122.
type UUID [16]byte
// A Version represents a UUID's version.
type Version byte
// A Variant represents a UUID's variant.
type Variant byte
// Constants returned by Variant.
const (
Invalid = Variant(iota) // Invalid UUID
RFC4122 // The variant specified in RFC4122
Reserved // Reserved, NCS backward compatibility.
Microsoft // Reserved, Microsoft Corporation backward compatibility.
Future // Reserved for future definition.
)
const randPoolSize = 16 * 16
var (
rander = rand.Reader // random function
poolEnabled = false
poolMu sync.Mutex
poolPos = randPoolSize // protected with poolMu
pool [randPoolSize]byte // protected with poolMu
)
type invalidLengthError struct{ len int }
func (err invalidLengthError) Error() string {
return fmt.Sprintf("invalid UUID length: %d", err.len)
}
// IsInvalidLengthError is matcher function for custom error invalidLengthError
func IsInvalidLengthError(err error) bool {
_, ok := err.(invalidLengthError)
return ok
}
// Parse decodes s into a UUID or returns an error. Both the standard UUID
// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the
// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex
// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
func Parse(s string) (UUID, error) {
var uuid UUID
switch len(s) {
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36:
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36 + 9:
if strings.ToLower(s[:9]) != "urn:uuid:" {
return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9])
}
s = s[9:]
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
case 36 + 2:
s = s[1:]
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
case 32:
var ok bool
for i := range uuid {
uuid[i], ok = xtob(s[i*2], s[i*2+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
}
return uuid, nil
default:
return uuid, invalidLengthError{len(s)}
}
// s is now at least 36 bytes long
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return uuid, errors.New("invalid UUID format")
}
for i, x := range [16]int{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34} {
v, ok := xtob(s[x], s[x+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
uuid[i] = v
}
return uuid, nil
}
// ParseBytes is like Parse, except it parses a byte slice instead of a string.
func ParseBytes(b []byte) (UUID, error) {
var uuid UUID
switch len(b) {
case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) {
return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9])
}
b = b[9:]
case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
b = b[1:]
case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
var ok bool
for i := 0; i < 32; i += 2 {
uuid[i/2], ok = xtob(b[i], b[i+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
}
return uuid, nil
default:
return uuid, invalidLengthError{len(b)}
}
// s is now at least 36 bytes long
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
return uuid, errors.New("invalid UUID format")
}
for i, x := range [16]int{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34} {
v, ok := xtob(b[x], b[x+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
uuid[i] = v
}
return uuid, nil
}
// MustParse is like Parse but panics if the string cannot be parsed.
// It simplifies safe initialization of global variables holding compiled UUIDs.
func MustParse(s string) UUID {
uuid, err := Parse(s)
if err != nil {
panic(`uuid: Parse(` + s + `): ` + err.Error())
}
return uuid
}
// FromBytes creates a new UUID from a byte slice. Returns an error if the slice
// does not have a length of 16. The bytes are copied from the slice.
func FromBytes(b []byte) (uuid UUID, err error) {
err = uuid.UnmarshalBinary(b)
return uuid, err
}
// Must returns uuid if err is nil and panics otherwise.
func Must(uuid UUID, err error) UUID {
if err != nil {
panic(err)
}
return uuid
}
// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// , or "" if uuid is invalid.
func (uuid UUID) String() string {
var buf [36]byte
encodeHex(buf[:], uuid)
return string(buf[:])
}
// URN returns the RFC 2141 URN form of uuid,
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid.
func (uuid UUID) URN() string {
var buf [36 + 9]byte
copy(buf[:], "urn:uuid:")
encodeHex(buf[9:], uuid)
return string(buf[:])
}
func encodeHex(dst []byte, uuid UUID) {
hex.Encode(dst, uuid[:4])
dst[8] = '-'
hex.Encode(dst[9:13], uuid[4:6])
dst[13] = '-'
hex.Encode(dst[14:18], uuid[6:8])
dst[18] = '-'
hex.Encode(dst[19:23], uuid[8:10])
dst[23] = '-'
hex.Encode(dst[24:], uuid[10:])
}
// Variant returns the variant encoded in uuid.
func (uuid UUID) Variant() Variant {
switch {
case (uuid[8] & 0xc0) == 0x80:
return RFC4122
case (uuid[8] & 0xe0) == 0xc0:
return Microsoft
case (uuid[8] & 0xe0) == 0xe0:
return Future
default:
return Reserved
}
}
// Version returns the version of uuid.
func (uuid UUID) Version() Version {
return Version(uuid[6] >> 4)
}
func (v Version) String() string {
if v > 15 {
return fmt.Sprintf("BAD_VERSION_%d", v)
}
return fmt.Sprintf("VERSION_%d", v)
}
func (v Variant) String() string {
switch v {
case RFC4122:
return "RFC4122"
case Reserved:
return "Reserved"
case Microsoft:
return "Microsoft"
case Future:
return "Future"
case Invalid:
return "Invalid"
}
return fmt.Sprintf("BadVariant%d", int(v))
}
// SetRand sets the random number generator to r, which implements io.Reader.
// If r.Read returns an error when the package requests random data then
// a panic will be issued.
//
// Calling SetRand with nil sets the random number generator to the default
// generator.
func SetRand(r io.Reader) {
if r == nil {
rander = rand.Reader
return
}
rander = r
}
// EnableRandPool enables internal randomness pool used for Random
// (Version 4) UUID generation. The pool contains random bytes read from
// the random number generator on demand in batches. Enabling the pool
// may improve the UUID generation throughput significantly.
//
// Since the pool is stored on the Go heap, this feature may be a bad fit
// for security sensitive applications.
//
// Both EnableRandPool and DisableRandPool are not thread-safe and should
// only be called when there is no possibility that New or any other
// UUID Version 4 generation function will be called concurrently.
func EnableRandPool() {
poolEnabled = true
}
// DisableRandPool disables the randomness pool if it was previously
// enabled with EnableRandPool.
//
// Both EnableRandPool and DisableRandPool are not thread-safe and should
// only be called when there is no possibility that New or any other
// UUID Version 4 generation function will be called concurrently.
func DisableRandPool() {
poolEnabled = false
defer poolMu.Unlock()
poolMu.Lock()
poolPos = randPoolSize
}

View file

@ -1,44 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
)
// NewUUID returns a Version 1 UUID based on the current NodeID and clock
// sequence, and the current time. If the NodeID has not been set by SetNodeID
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
// be set NewUUID returns nil. If clock sequence has not been set by
// SetClockSequence then it will be set automatically. If GetTime fails to
// return the current NewUUID returns nil and an error.
//
// In most cases, New should be used.
func NewUUID() (UUID, error) {
var uuid UUID
now, seq, err := GetTime()
if err != nil {
return uuid, err
}
timeLow := uint32(now & 0xffffffff)
timeMid := uint16((now >> 32) & 0xffff)
timeHi := uint16((now >> 48) & 0x0fff)
timeHi |= 0x1000 // Version 1
binary.BigEndian.PutUint32(uuid[0:], timeLow)
binary.BigEndian.PutUint16(uuid[4:], timeMid)
binary.BigEndian.PutUint16(uuid[6:], timeHi)
binary.BigEndian.PutUint16(uuid[8:], seq)
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
copy(uuid[10:], nodeID[:])
nodeMu.Unlock()
return uuid, nil
}

View file

@ -1,76 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "io"
// New creates a new random UUID or panics. New is equivalent to
// the expression
//
// uuid.Must(uuid.NewRandom())
func New() UUID {
return Must(NewRandom())
}
// NewString creates a new random UUID and returns it as a string or panics.
// NewString is equivalent to the expression
//
// uuid.New().String()
func NewString() string {
return Must(NewRandom()).String()
}
// NewRandom returns a Random (Version 4) UUID.
//
// The strength of the UUIDs is based on the strength of the crypto/rand
// package.
//
// Uses the randomness pool if it was enabled with EnableRandPool.
//
// A note about uniqueness derived from the UUID Wikipedia entry:
//
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
// hit by a meteorite is estimated to be one chance in 17 billion, that
// means the probability is about 0.00000000006 (6 × 1011),
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
// year and having one duplicate.
func NewRandom() (UUID, error) {
if !poolEnabled {
return NewRandomFromReader(rander)
}
return newRandomFromPool()
}
// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader.
func NewRandomFromReader(r io.Reader) (UUID, error) {
var uuid UUID
_, err := io.ReadFull(r, uuid[:])
if err != nil {
return Nil, err
}
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}
func newRandomFromPool() (UUID, error) {
var uuid UUID
poolMu.Lock()
if poolPos == randPoolSize {
_, err := io.ReadFull(rander, pool[:])
if err != nil {
poolMu.Unlock()
return Nil, err
}
poolPos = 0
}
copy(uuid[:], pool[poolPos:(poolPos+16)])
poolPos += 16
poolMu.Unlock()
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}

1
vendor/modules.txt vendored
View file

@ -11,7 +11,6 @@ github.com/gofrs/uuid
## explicit; go 1.13 ## explicit; go 1.13
# github.com/google/uuid v1.3.0 # github.com/google/uuid v1.3.0
## explicit ## explicit
github.com/google/uuid
# github.com/gorilla/handlers v1.5.1 # github.com/gorilla/handlers v1.5.1
## explicit; go 1.14 ## explicit; go 1.14
github.com/gorilla/handlers github.com/gorilla/handlers

Binary file not shown.

BIN
webapi Executable file

Binary file not shown.

27
webui/.gitignore vendored Normal file
View file

@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.DS_Store
/dist
/dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
webui/.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

16
webui/index.html Normal file
View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Example app</title>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/bootstrap/css/bootstrap.css">
</head>
<body>
<div id="app"></div>
<script src="/bootstrap/js/bootstrap.bundle.js"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1
webui/node_modules/.bin/esbuild generated vendored Symbolic link
View file

@ -0,0 +1 @@
../esbuild/bin/esbuild

1
webui/node_modules/.bin/nanoid generated vendored Symbolic link
View file

@ -0,0 +1 @@
../nanoid/bin/nanoid.cjs

1
webui/node_modules/.bin/parser generated vendored Symbolic link
View file

@ -0,0 +1 @@
../@babel/parser/bin/babel-parser.js

1
webui/node_modules/.bin/resolve generated vendored Symbolic link
View file

@ -0,0 +1 @@
../resolve/bin/resolve

1
webui/node_modules/.bin/rollup generated vendored Symbolic link
View file

@ -0,0 +1 @@
../rollup/dist/bin/rollup

1
webui/node_modules/.bin/vite generated vendored Symbolic link
View file

@ -0,0 +1 @@
../vite/bin/vite.js

500
webui/node_modules/.package-lock.json generated vendored Normal file
View file

@ -0,0 +1,500 @@
{
"name": "teapot",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"node_modules/@babel/parser": {
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==",
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-3.0.3.tgz",
"integrity": "sha512-U4zNBlz9mg+TA+i+5QPc3N5lQvdUXENZLO2h0Wdzp56gI1MWhqJOv+6R+d4kOzoaSSq6TnGPBdZAXKOe4lXy6g==",
"dev": true,
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"vite": "^3.0.0",
"vue": "^3.2.25"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.37.tgz",
"integrity": "sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==",
"dependencies": {
"@babel/parser": "^7.16.4",
"@vue/shared": "3.2.37",
"estree-walker": "^2.0.2",
"source-map": "^0.6.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz",
"integrity": "sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==",
"dependencies": {
"@vue/compiler-core": "3.2.37",
"@vue/shared": "3.2.37"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz",
"integrity": "sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==",
"dependencies": {
"@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.37",
"@vue/compiler-dom": "3.2.37",
"@vue/compiler-ssr": "3.2.37",
"@vue/reactivity-transform": "3.2.37",
"@vue/shared": "3.2.37",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7",
"postcss": "^8.1.10",
"source-map": "^0.6.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz",
"integrity": "sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==",
"dependencies": {
"@vue/compiler-dom": "3.2.37",
"@vue/shared": "3.2.37"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.2.1.tgz",
"integrity": "sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ=="
},
"node_modules/@vue/reactivity": {
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.37.tgz",
"integrity": "sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==",
"dependencies": {
"@vue/shared": "3.2.37"
}
},
"node_modules/@vue/reactivity-transform": {
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz",
"integrity": "sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==",
"dependencies": {
"@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.37",
"@vue/shared": "3.2.37",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.37.tgz",
"integrity": "sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==",
"dependencies": {
"@vue/reactivity": "3.2.37",
"@vue/shared": "3.2.37"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz",
"integrity": "sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==",
"dependencies": {
"@vue/runtime-core": "3.2.37",
"@vue/shared": "3.2.37",
"csstype": "^2.6.8"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.37.tgz",
"integrity": "sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==",
"dependencies": {
"@vue/compiler-ssr": "3.2.37",
"@vue/shared": "3.2.37"
},
"peerDependencies": {
"vue": "3.2.37"
}
},
"node_modules/@vue/shared": {
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz",
"integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/csstype": {
"version": "2.6.20",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
"integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/esbuild": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz",
"integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/linux-loong64": "0.14.54",
"esbuild-android-64": "0.14.54",
"esbuild-android-arm64": "0.14.54",
"esbuild-darwin-64": "0.14.54",
"esbuild-darwin-arm64": "0.14.54",
"esbuild-freebsd-64": "0.14.54",
"esbuild-freebsd-arm64": "0.14.54",
"esbuild-linux-32": "0.14.54",
"esbuild-linux-64": "0.14.54",
"esbuild-linux-arm": "0.14.54",
"esbuild-linux-arm64": "0.14.54",
"esbuild-linux-mips64le": "0.14.54",
"esbuild-linux-ppc64le": "0.14.54",
"esbuild-linux-riscv64": "0.14.54",
"esbuild-linux-s390x": "0.14.54",
"esbuild-netbsd-64": "0.14.54",
"esbuild-openbsd-64": "0.14.54",
"esbuild-sunos-64": "0.14.54",
"esbuild-windows-32": "0.14.54",
"esbuild-windows-64": "0.14.54",
"esbuild-windows-arm64": "0.14.54"
}
},
"node_modules/esbuild-linux-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
"integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"node_modules/follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/is-core-module": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz",
"integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==",
"dev": true,
"dependencies": {
"has": "^1.0.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
"integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
"dependencies": {
"sourcemap-codec": "^1.4.8"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"node_modules/postcss": {
"version": "8.4.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
"integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
}
],
"dependencies": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"dev": true,
"dependencies": {
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "2.77.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz",
"integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/vite": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.0.9.tgz",
"integrity": "sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw==",
"dev": true,
"dependencies": {
"esbuild": "^0.14.47",
"postcss": "^8.4.16",
"resolve": "^1.22.1",
"rollup": ">=2.75.6 <2.77.0 || ~2.77.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"less": "*",
"sass": "*",
"stylus": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"less": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
},
"terser": {
"optional": true
}
}
},
"node_modules/vue": {
"version": "3.2.37",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.37.tgz",
"integrity": "sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==",
"dependencies": {
"@vue/compiler-dom": "3.2.37",
"@vue/compiler-sfc": "3.2.37",
"@vue/runtime-dom": "3.2.37",
"@vue/server-renderer": "3.2.37",
"@vue/shared": "3.2.37"
}
},
"node_modules/vue-router": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.4.tgz",
"integrity": "sha512-UgYen33gOtwT3cOG1+yRen+Brk9py8CSlC9LEa3UjvKZ4EAoSo8NjZPDeDnmNerfazorHIJG1NC7qdi1SuQJnQ==",
"dependencies": {
"@vue/devtools-api": "^6.1.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.2.0"
}
}
}
}

1073
webui/node_modules/@babel/parser/CHANGELOG.md generated vendored Normal file

File diff suppressed because it is too large Load diff

19
webui/node_modules/@babel/parser/LICENSE generated vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (C) 2012-2014 by various contributors (see AUTHORS)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

19
webui/node_modules/@babel/parser/README.md generated vendored Normal file
View file

@ -0,0 +1,19 @@
# @babel/parser
> A JavaScript parser
See our website [@babel/parser](https://babeljs.io/docs/en/babel-parser) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20parser%20(babylon)%22+is%3Aopen) associated with this package.
## Install
Using npm:
```sh
npm install --save-dev @babel/parser
```
or using yarn:
```sh
yarn add @babel/parser --dev
```

15
webui/node_modules/@babel/parser/bin/babel-parser.js generated vendored Executable file
View file

@ -0,0 +1,15 @@
#!/usr/bin/env node
/* eslint no-var: 0 */
var parser = require("..");
var fs = require("fs");
var filename = process.argv[2];
if (!filename) {
console.error("no filename specified");
} else {
var file = fs.readFileSync(filename, "utf8");
var ast = parser.parse(file);
console.log(JSON.stringify(ast, null, " "));
}

5
webui/node_modules/@babel/parser/index.cjs generated vendored Normal file
View file

@ -0,0 +1,5 @@
try {
module.exports = require("./lib/index.cjs");
} catch {
module.exports = require("./lib/index.js");
}

16800
webui/node_modules/@babel/parser/lib/index.js generated vendored Normal file

File diff suppressed because it is too large Load diff

1
webui/node_modules/@babel/parser/lib/index.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

46
webui/node_modules/@babel/parser/package.json generated vendored Normal file
View file

@ -0,0 +1,46 @@
{
"name": "@babel/parser",
"version": "7.18.13",
"description": "A JavaScript parser",
"author": "The Babel Team (https://babel.dev/team)",
"homepage": "https://babel.dev/docs/en/next/babel-parser",
"bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A+parser+%28babylon%29%22+is%3Aopen",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"keywords": [
"babel",
"javascript",
"parser",
"tc39",
"ecmascript",
"@babel/parser"
],
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-parser"
},
"main": "./lib/index.js",
"types": "./typings/babel-parser.d.ts",
"files": [
"bin",
"lib",
"typings",
"index.cjs"
],
"engines": {
"node": ">=6.0.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/helper-check-duplicate-nodes": "^7.18.6",
"@babel/helper-fixtures": "^7.18.6",
"@babel/helper-string-parser": "^7.18.10",
"@babel/helper-validator-identifier": "^7.18.6",
"charcodes": "^0.2.0"
},
"bin": "./bin/babel-parser.js",
"type": "commonjs"
}

View file

@ -0,0 +1,214 @@
// Type definitions for @babel/parser
// Project: https://github.com/babel/babel/tree/main/packages/babel-parser
// Definitions by: Troy Gerwien <https://github.com/yortus>
// Marvin Hagemeister <https://github.com/marvinhagemeister>
// Avi Vahl <https://github.com/AviVahl>
// TypeScript Version: 2.9
/**
* Parse the provided code as an entire ECMAScript program.
*/
export function parse(
input: string,
options?: ParserOptions
): ParseResult<import("@babel/types").File>;
/**
* Parse the provided code as a single expression.
*/
export function parseExpression(
input: string,
options?: ParserOptions
): ParseResult<import("@babel/types").Expression>;
export interface ParserOptions {
/**
* By default, import and export declarations can only appear at a program's top level.
* Setting this option to true allows them anywhere where a statement is allowed.
*/
allowImportExportEverywhere?: boolean;
/**
* By default, await use is not allowed outside of an async function.
* Set this to true to accept such code.
*/
allowAwaitOutsideFunction?: boolean;
/**
* By default, a return statement at the top level raises an error.
* Set this to true to accept such code.
*/
allowReturnOutsideFunction?: boolean;
allowSuperOutsideMethod?: boolean;
/**
* By default, exported identifiers must refer to a declared variable.
* Set this to true to allow export statements to reference undeclared variables.
*/
allowUndeclaredExports?: boolean;
/**
* By default, Babel attaches comments to adjacent AST nodes.
* When this option is set to false, comments are not attached.
* It can provide up to 30% performance improvement when the input code has many comments.
* @babel/eslint-parser will set it for you.
* It is not recommended to use attachComment: false with Babel transform,
* as doing so removes all the comments in output code, and renders annotations such as
* /* istanbul ignore next *\/ nonfunctional.
*/
attachComment?: boolean;
/**
* By default, Babel always throws an error when it finds some invalid code.
* When this option is set to true, it will store the parsing error and
* try to continue parsing the invalid input file.
*/
errorRecovery?: boolean;
/**
* Indicate the mode the code should be parsed in.
* Can be one of "script", "module", or "unambiguous". Defaults to "script".
* "unambiguous" will make @babel/parser attempt to guess, based on the presence
* of ES6 import or export statements.
* Files with ES6 imports and exports are considered "module" and are otherwise "script".
*/
sourceType?: "script" | "module" | "unambiguous";
/**
* Correlate output AST nodes with their source filename.
* Useful when generating code and source maps from the ASTs of multiple input files.
*/
sourceFilename?: string;
/**
* By default, the first line of code parsed is treated as line 1.
* You can provide a line number to alternatively start with.
* Useful for integration with other source tools.
*/
startLine?: number;
/**
* By default, the parsed code is treated as if it starts from line 1, column 0.
* You can provide a column number to alternatively start with.
* Useful for integration with other source tools.
*/
startColumn?: number;
/**
* Array containing the plugins that you want to enable.
*/
plugins?: ParserPlugin[];
/**
* Should the parser work in strict mode.
* Defaults to true if sourceType === 'module'. Otherwise, false.
*/
strictMode?: boolean;
/**
* Adds a ranges property to each node: [node.start, node.end]
*/
ranges?: boolean;
/**
* Adds all parsed tokens to a tokens property on the File node.
*/
tokens?: boolean;
/**
* By default, the parser adds information about parentheses by setting
* `extra.parenthesized` to `true` as needed.
* When this option is `true` the parser creates `ParenthesizedExpression`
* AST nodes instead of using the `extra` property.
*/
createParenthesizedExpressions?: boolean;
}
export type ParserPlugin =
| "asyncDoExpressions"
| "asyncGenerators"
| "bigInt"
| "classPrivateMethods"
| "classPrivateProperties"
| "classProperties"
| "classStaticBlock" // Enabled by default
| "decimal"
| "decorators"
| "decorators-legacy"
| "decoratorAutoAccessors"
| "destructuringPrivate"
| "doExpressions"
| "dynamicImport"
| "estree"
| "exportDefaultFrom"
| "exportNamespaceFrom" // deprecated
| "flow"
| "flowComments"
| "functionBind"
| "functionSent"
| "importMeta"
| "jsx"
| "logicalAssignment"
| "importAssertions"
| "moduleBlocks"
| "moduleStringNames"
| "nullishCoalescingOperator"
| "numericSeparator"
| "objectRestSpread"
| "optionalCatchBinding"
| "optionalChaining"
| "partialApplication"
| "pipelineOperator"
| "placeholders"
| "privateIn" // Enabled by default
| "regexpUnicodeSets"
| "throwExpressions"
| "topLevelAwait"
| "typescript"
| "v8intrinsic"
| ParserPluginWithOptions;
export type ParserPluginWithOptions =
| ["decorators", DecoratorsPluginOptions]
| ["pipelineOperator", PipelineOperatorPluginOptions]
| ["recordAndTuple", RecordAndTuplePluginOptions]
| ["flow", FlowPluginOptions]
| ["typescript", TypeScriptPluginOptions];
export interface DecoratorsPluginOptions {
decoratorsBeforeExport?: boolean;
}
export interface PipelineOperatorPluginOptions {
proposal: "minimal" | "fsharp" | "hack" | "smart";
topicToken?: "%" | "#" | "@@" | "^^" | "^";
}
export interface RecordAndTuplePluginOptions {
syntaxType: "bar" | "hash";
}
export interface FlowPluginOptions {
all?: boolean;
enums?: boolean;
}
export interface TypeScriptPluginOptions {
dts?: boolean;
disallowAmbiguousJSXLike?: boolean;
}
export const tokTypes: {
// todo(flow->ts) real token type
[name: string]: any;
};
export interface ParseError {
code: string;
reasonCode: string;
}
type ParseResult<Result> = Result & {
errors: ParseError[];
};

21
webui/node_modules/@vitejs/plugin-vue/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

171
webui/node_modules/@vitejs/plugin-vue/README.md generated vendored Normal file
View file

@ -0,0 +1,171 @@
# @vitejs/plugin-vue [![npm](https://img.shields.io/npm/v/@vitejs/plugin-vue.svg)](https://npmjs.com/package/@vitejs/plugin-vue)
> Note: as of `vue` 3.2.13+ and `@vitejs/plugin-vue` 1.9.0+, `@vue/compiler-sfc` is no longer required as a peer dependency.
```js
// vite.config.js
import vue from '@vitejs/plugin-vue'
export default {
plugins: [vue()]
}
```
## Options
```ts
export interface Options {
include?: string | RegExp | (string | RegExp)[]
exclude?: string | RegExp | (string | RegExp)[]
ssr?: boolean
isProduction?: boolean
/**
* Transform Vue SFCs into custom elements (requires vue@^3.2.0)
* - `true` -> all `*.vue` imports are converted into custom elements
* - `string | RegExp` -> matched files are converted into custom elements
*
* @default /\.ce\.vue$/
*/
customElement?: boolean | string | RegExp | (string | RegExp)[]
/**
* Enable Vue reactivity transform (experimental, requires vue@^3.2.25).
* https://github.com/vuejs/core/tree/master/packages/reactivity-transform
*
* - `true`: transform will be enabled for all vue,js(x),ts(x) files except
* those inside node_modules
* - `string | RegExp`: apply to vue + only matched files (will include
* node_modules, so specify directories in necessary)
* - `false`: disable in all cases
*
* @default false
*/
reactivityTransform?: boolean | string | RegExp | (string | RegExp)[]
// options to pass on to vue/compiler-sfc
script?: Partial<Pick<SFCScriptCompileOptions, 'babelParserPlugins'>>
template?: Partial<
Pick<
SFCTemplateCompileOptions,
| 'compiler'
| 'compilerOptions'
| 'preprocessOptions'
| 'preprocessCustomRequire'
| 'transformAssetUrls'
>
>
style?: Partial<Pick<SFCStyleCompileOptions, 'trim'>>
}
```
## Asset URL handling
When `@vitejs/plugin-vue` compiles the `<template>` blocks in SFCs, it also converts any encountered asset URLs into ESM imports.
For example, the following template snippet:
```vue
<img src="../image.png" />
```
Is the same as:
```vue
<script setup>
import _imports_0 from '../image.png'
</script>
<img src="_imports_0" />
```
By default the following tag/attribute combinations are transformed, and can be configured using the `template.transformAssetUrls` option.
```js
{
video: ['src', 'poster'],
source: ['src'],
img: ['src'],
image: ['xlink:href', 'href'],
use: ['xlink:href', 'href']
}
```
Note that only attribute values that are static strings are transformed. Otherwise, you'd need to import the asset manually, e.g. `import imgUrl from '../image.png'`.
## Example for passing options to `vue/compiler-sfc`:
```ts
import vue from '@vitejs/plugin-vue'
export default {
plugins: [
vue({
template: {
compilerOptions: {
// ...
},
transformAssetUrls: {
// ...
}
}
})
]
}
```
## Example for transforming custom blocks
```ts
import vue from '@vitejs/plugin-vue'
import yaml from 'js-yaml'
const vueI18nPlugin = {
name: 'vue-i18n',
transform(code, id) {
if (!/vue&type=i18n/.test(id)) {
return
}
if (/\.ya?ml$/.test(id)) {
code = JSON.stringify(yaml.load(code.trim()))
}
return `export default Comp => {
Comp.i18n = ${code}
}`
}
}
export default {
plugins: [vue(), vueI18nPlugin]
}
```
## Using Vue SFCs as Custom Elements
> Requires `vue@^3.2.0` & `@vitejs/plugin-vue@^1.4.0`
Vue 3.2 introduces the `defineCustomElement` method, which works with SFCs. By default, `<style>` tags inside SFCs are extracted and merged into CSS files during build. However when shipping a library of custom elements, it may be desirable to inline the styles as JavaScript strings and inject them into the custom elements' shadow root instead.
Starting in 1.4.0, files ending with `*.ce.vue` will be compiled in "custom elements" mode: its `<style>` tags are compiled into inlined CSS strings and attached to the component as its `styles` property:
```js
import { defineCustomElement } from 'vue'
import Example from './Example.ce.vue'
console.log(Example.styles) // ['/* css content */']
// register
customElements.define('my-example', defineCustomElement(Example))
```
Note in custom elements mode there is no need to use `<style scoped>` since the CSS is already scoped inside the shadow DOM.
The `customElement` plugin option can be used to configure the behavior:
- `{ customElement: true }` will import all `*.vue` files in custom element mode.
- Use a string or regex pattern to change how files should be loaded as Custom Elements (this check is applied after `include` and `exclude` matches).
## License
MIT

2682
webui/node_modules/@vitejs/plugin-vue/dist/index.cjs generated vendored Normal file

File diff suppressed because it is too large Load diff

61
webui/node_modules/@vitejs/plugin-vue/dist/index.d.ts generated vendored Normal file
View file

@ -0,0 +1,61 @@
import { ViteDevServer, Plugin } from 'vite';
import * as _compiler from 'vue/compiler-sfc';
import { SFCScriptCompileOptions, SFCTemplateCompileOptions, SFCStyleCompileOptions } from 'vue/compiler-sfc';
interface VueQuery {
vue?: boolean;
src?: string;
type?: 'script' | 'template' | 'style' | 'custom';
index?: number;
lang?: string;
raw?: boolean;
scoped?: boolean;
}
declare function parseVueRequest(id: string): {
filename: string;
query: VueQuery;
};
interface Options {
include?: string | RegExp | (string | RegExp)[];
exclude?: string | RegExp | (string | RegExp)[];
isProduction?: boolean;
script?: Partial<Pick<SFCScriptCompileOptions, 'babelParserPlugins'>>;
template?: Partial<Pick<SFCTemplateCompileOptions, 'compiler' | 'compilerOptions' | 'preprocessOptions' | 'preprocessCustomRequire' | 'transformAssetUrls'>>;
style?: Partial<Pick<SFCStyleCompileOptions, 'trim'>>;
/**
* Transform Vue SFCs into custom elements.
* - `true`: all `*.vue` imports are converted into custom elements
* - `string | RegExp`: matched files are converted into custom elements
*
* @default /\.ce\.vue$/
*/
customElement?: boolean | string | RegExp | (string | RegExp)[];
/**
* Enable Vue reactivity transform (experimental).
* https://github.com/vuejs/core/tree/master/packages/reactivity-transform
* - `true`: transform will be enabled for all vue,js(x),ts(x) files except
* those inside node_modules
* - `string | RegExp`: apply to vue + only matched files (will include
* node_modules, so specify directories in necessary)
* - `false`: disable in all cases
*
* @default false
*/
reactivityTransform?: boolean | string | RegExp | (string | RegExp)[];
/**
* Use custom compiler-sfc instance. Can be used to force a specific version.
*/
compiler?: typeof _compiler;
}
interface ResolvedOptions extends Options {
compiler: typeof _compiler;
root: string;
sourceMap: boolean;
cssDevSourcemap: boolean;
devServer?: ViteDevServer;
devToolsEnabled?: boolean;
}
declare function vuePlugin(rawOptions?: Options): Plugin;
export { Options, ResolvedOptions, VueQuery, vuePlugin as default, parseVueRequest };

2669
webui/node_modules/@vitejs/plugin-vue/dist/index.mjs generated vendored Normal file

File diff suppressed because it is too large Load diff

51
webui/node_modules/@vitejs/plugin-vue/package.json generated vendored Normal file
View file

@ -0,0 +1,51 @@
{
"name": "@vitejs/plugin-vue",
"version": "3.0.3",
"license": "MIT",
"author": "Evan You",
"files": [
"dist"
],
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"scripts": {
"dev": "unbuild --stub",
"build": "unbuild && pnpm run patch-cjs",
"patch-cjs": "tsx ../../scripts/patchCJS.ts",
"prepublishOnly": "npm run build"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/vitejs/vite.git",
"directory": "packages/plugin-vue"
},
"bugs": {
"url": "https://github.com/vitejs/vite/issues"
},
"homepage": "https://github.com/vitejs/vite/tree/main/packages/plugin-vue#readme",
"peerDependencies": {
"vite": "^3.0.0",
"vue": "^3.2.25"
},
"devDependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.14",
"debug": "^4.3.4",
"rollup": ">=2.75.6 <2.77.0 || ~2.77.0",
"slash": "^4.0.0",
"source-map": "^0.6.1",
"vite": "workspace:*",
"vue": "^3.2.37"
}
}

21
webui/node_modules/@vue/compiler-core/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1
webui/node_modules/@vue/compiler-core/README.md generated vendored Normal file
View file

@ -0,0 +1 @@
# @vue/compiler-core

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

7
webui/node_modules/@vue/compiler-core/index.js generated vendored Normal file
View file

@ -0,0 +1,7 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./dist/compiler-core.cjs.prod.js')
} else {
module.exports = require('./dist/compiler-core.cjs.js')
}

43
webui/node_modules/@vue/compiler-core/package.json generated vendored Normal file
View file

@ -0,0 +1,43 @@
{
"name": "@vue/compiler-core",
"version": "3.2.37",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
"types": "dist/compiler-core.d.ts",
"files": [
"index.js",
"dist"
],
"buildOptions": {
"name": "VueCompilerCore",
"compat": true,
"formats": [
"esm-bundler",
"cjs"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/core.git",
"directory": "packages/compiler-core"
},
"keywords": [
"vue"
],
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/core/issues"
},
"homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme",
"dependencies": {
"@vue/shared": "3.2.37",
"@babel/parser": "^7.16.4",
"estree-walker": "^2.0.2",
"source-map": "^0.6.1"
},
"devDependencies": {
"@babel/types": "^7.16.0"
}
}

21
webui/node_modules/@vue/compiler-dom/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1
webui/node_modules/@vue/compiler-dom/README.md generated vendored Normal file
View file

@ -0,0 +1 @@
# @vue/compiler-dom

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,66 @@
import { CodegenResult } from '@vue/compiler-core';
import { CompilerError } from '@vue/compiler-core';
import { CompilerOptions } from '@vue/compiler-core';
import { DirectiveTransform } from '@vue/compiler-core';
import { NodeTransform } from '@vue/compiler-core';
import { ParserOptions } from '@vue/compiler-core';
import { RootNode } from '@vue/compiler-core';
import { SourceLocation } from '@vue/compiler-core';
export declare function compile(template: string, options?: CompilerOptions): CodegenResult;
export declare function createDOMCompilerError(code: DOMErrorCodes, loc?: SourceLocation): DOMCompilerError;
declare interface DOMCompilerError extends CompilerError {
code: DOMErrorCodes;
}
export declare const DOMDirectiveTransforms: Record<string, DirectiveTransform>;
export declare const enum DOMErrorCodes {
X_V_HTML_NO_EXPRESSION = 50,
X_V_HTML_WITH_CHILDREN = 51,
X_V_TEXT_NO_EXPRESSION = 52,
X_V_TEXT_WITH_CHILDREN = 53,
X_V_MODEL_ON_INVALID_ELEMENT = 54,
X_V_MODEL_ARG_ON_ELEMENT = 55,
X_V_MODEL_ON_FILE_INPUT_ELEMENT = 56,
X_V_MODEL_UNNECESSARY_VALUE = 57,
X_V_SHOW_NO_EXPRESSION = 58,
X_TRANSITION_INVALID_CHILDREN = 59,
X_IGNORED_SIDE_EFFECT_TAG = 60,
__EXTEND_POINT__ = 61
}
export declare const DOMNodeTransforms: NodeTransform[];
export declare function parse(template: string, options?: ParserOptions): RootNode;
export declare const parserOptions: ParserOptions;
export declare const transformStyle: NodeTransform;
export declare const TRANSITION: unique symbol;
export declare const TRANSITION_GROUP: unique symbol;
export declare const V_MODEL_CHECKBOX: unique symbol;
export declare const V_MODEL_DYNAMIC: unique symbol;
export declare const V_MODEL_RADIO: unique symbol;
export declare const V_MODEL_SELECT: unique symbol;
export declare const V_MODEL_TEXT: unique symbol;
export declare const V_ON_WITH_KEYS: unique symbol;
export declare const V_ON_WITH_MODIFIERS: unique symbol;
export declare const V_SHOW: unique symbol;
export * from "@vue/compiler-core";
export { }

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,483 @@
import { registerRuntimeHelpers, isBuiltInType, createSimpleExpression, createCompilerError, createObjectProperty, getConstantType, createCallExpression, TO_DISPLAY_STRING, transformModel as transformModel$1, findProp, hasDynamicKeyVBind, transformOn as transformOn$1, createCompoundExpression, isStaticExp, checkCompatEnabled, noopDirectiveTransform, baseCompile, baseParse } from '@vue/compiler-core';
export * from '@vue/compiler-core';
import { isVoidTag, isHTMLTag, isSVGTag, makeMap, parseStringStyle, capitalize, extend } from '@vue/shared';
const V_MODEL_RADIO = Symbol((process.env.NODE_ENV !== 'production') ? `vModelRadio` : ``);
const V_MODEL_CHECKBOX = Symbol((process.env.NODE_ENV !== 'production') ? `vModelCheckbox` : ``);
const V_MODEL_TEXT = Symbol((process.env.NODE_ENV !== 'production') ? `vModelText` : ``);
const V_MODEL_SELECT = Symbol((process.env.NODE_ENV !== 'production') ? `vModelSelect` : ``);
const V_MODEL_DYNAMIC = Symbol((process.env.NODE_ENV !== 'production') ? `vModelDynamic` : ``);
const V_ON_WITH_MODIFIERS = Symbol((process.env.NODE_ENV !== 'production') ? `vOnModifiersGuard` : ``);
const V_ON_WITH_KEYS = Symbol((process.env.NODE_ENV !== 'production') ? `vOnKeysGuard` : ``);
const V_SHOW = Symbol((process.env.NODE_ENV !== 'production') ? `vShow` : ``);
const TRANSITION = Symbol((process.env.NODE_ENV !== 'production') ? `Transition` : ``);
const TRANSITION_GROUP = Symbol((process.env.NODE_ENV !== 'production') ? `TransitionGroup` : ``);
registerRuntimeHelpers({
[V_MODEL_RADIO]: `vModelRadio`,
[V_MODEL_CHECKBOX]: `vModelCheckbox`,
[V_MODEL_TEXT]: `vModelText`,
[V_MODEL_SELECT]: `vModelSelect`,
[V_MODEL_DYNAMIC]: `vModelDynamic`,
[V_ON_WITH_MODIFIERS]: `withModifiers`,
[V_ON_WITH_KEYS]: `withKeys`,
[V_SHOW]: `vShow`,
[TRANSITION]: `Transition`,
[TRANSITION_GROUP]: `TransitionGroup`
});
/* eslint-disable no-restricted-globals */
let decoder;
function decodeHtmlBrowser(raw, asAttr = false) {
if (!decoder) {
decoder = document.createElement('div');
}
if (asAttr) {
decoder.innerHTML = `<div foo="${raw.replace(/"/g, '&quot;')}">`;
return decoder.children[0].getAttribute('foo');
}
else {
decoder.innerHTML = raw;
return decoder.textContent;
}
}
const isRawTextContainer = /*#__PURE__*/ makeMap('style,iframe,script,noscript', true);
const parserOptions = {
isVoidTag,
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
isPreTag: tag => tag === 'pre',
decodeEntities: decodeHtmlBrowser ,
isBuiltInComponent: (tag) => {
if (isBuiltInType(tag, `Transition`)) {
return TRANSITION;
}
else if (isBuiltInType(tag, `TransitionGroup`)) {
return TRANSITION_GROUP;
}
},
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
getNamespace(tag, parent) {
let ns = parent ? parent.ns : 0 /* HTML */;
if (parent && ns === 2 /* MATH_ML */) {
if (parent.tag === 'annotation-xml') {
if (tag === 'svg') {
return 1 /* SVG */;
}
if (parent.props.some(a => a.type === 6 /* ATTRIBUTE */ &&
a.name === 'encoding' &&
a.value != null &&
(a.value.content === 'text/html' ||
a.value.content === 'application/xhtml+xml'))) {
ns = 0 /* HTML */;
}
}
else if (/^m(?:[ions]|text)$/.test(parent.tag) &&
tag !== 'mglyph' &&
tag !== 'malignmark') {
ns = 0 /* HTML */;
}
}
else if (parent && ns === 1 /* SVG */) {
if (parent.tag === 'foreignObject' ||
parent.tag === 'desc' ||
parent.tag === 'title') {
ns = 0 /* HTML */;
}
}
if (ns === 0 /* HTML */) {
if (tag === 'svg') {
return 1 /* SVG */;
}
if (tag === 'math') {
return 2 /* MATH_ML */;
}
}
return ns;
},
// https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments
getTextMode({ tag, ns }) {
if (ns === 0 /* HTML */) {
if (tag === 'textarea' || tag === 'title') {
return 1 /* RCDATA */;
}
if (isRawTextContainer(tag)) {
return 2 /* RAWTEXT */;
}
}
return 0 /* DATA */;
}
};
// Parse inline CSS strings for static style attributes into an object.
// This is a NodeTransform since it works on the static `style` attribute and
// converts it into a dynamic equivalent:
// style="color: red" -> :style='{ "color": "red" }'
// It is then processed by `transformElement` and included in the generated
// props.
const transformStyle = node => {
if (node.type === 1 /* ELEMENT */) {
node.props.forEach((p, i) => {
if (p.type === 6 /* ATTRIBUTE */ && p.name === 'style' && p.value) {
// replace p with an expression node
node.props[i] = {
type: 7 /* DIRECTIVE */,
name: `bind`,
arg: createSimpleExpression(`style`, true, p.loc),
exp: parseInlineCSS(p.value.content, p.loc),
modifiers: [],
loc: p.loc
};
}
});
}
};
const parseInlineCSS = (cssText, loc) => {
const normalized = parseStringStyle(cssText);
return createSimpleExpression(JSON.stringify(normalized), false, loc, 3 /* CAN_STRINGIFY */);
};
function createDOMCompilerError(code, loc) {
return createCompilerError(code, loc, (process.env.NODE_ENV !== 'production') || !true ? DOMErrorMessages : undefined);
}
const DOMErrorMessages = {
[50 /* X_V_HTML_NO_EXPRESSION */]: `v-html is missing expression.`,
[51 /* X_V_HTML_WITH_CHILDREN */]: `v-html will override element children.`,
[52 /* X_V_TEXT_NO_EXPRESSION */]: `v-text is missing expression.`,
[53 /* X_V_TEXT_WITH_CHILDREN */]: `v-text will override element children.`,
[54 /* X_V_MODEL_ON_INVALID_ELEMENT */]: `v-model can only be used on <input>, <textarea> and <select> elements.`,
[55 /* X_V_MODEL_ARG_ON_ELEMENT */]: `v-model argument is not supported on plain elements.`,
[56 /* X_V_MODEL_ON_FILE_INPUT_ELEMENT */]: `v-model cannot be used on file inputs since they are read-only. Use a v-on:change listener instead.`,
[57 /* X_V_MODEL_UNNECESSARY_VALUE */]: `Unnecessary value binding used alongside v-model. It will interfere with v-model's behavior.`,
[58 /* X_V_SHOW_NO_EXPRESSION */]: `v-show is missing expression.`,
[59 /* X_TRANSITION_INVALID_CHILDREN */]: `<Transition> expects exactly one child element or component.`,
[60 /* X_IGNORED_SIDE_EFFECT_TAG */]: `Tags with side effect (<script> and <style>) are ignored in client component templates.`
};
const transformVHtml = (dir, node, context) => {
const { exp, loc } = dir;
if (!exp) {
context.onError(createDOMCompilerError(50 /* X_V_HTML_NO_EXPRESSION */, loc));
}
if (node.children.length) {
context.onError(createDOMCompilerError(51 /* X_V_HTML_WITH_CHILDREN */, loc));
node.children.length = 0;
}
return {
props: [
createObjectProperty(createSimpleExpression(`innerHTML`, true, loc), exp || createSimpleExpression('', true))
]
};
};
const transformVText = (dir, node, context) => {
const { exp, loc } = dir;
if (!exp) {
context.onError(createDOMCompilerError(52 /* X_V_TEXT_NO_EXPRESSION */, loc));
}
if (node.children.length) {
context.onError(createDOMCompilerError(53 /* X_V_TEXT_WITH_CHILDREN */, loc));
node.children.length = 0;
}
return {
props: [
createObjectProperty(createSimpleExpression(`textContent`, true), exp
? getConstantType(exp, context) > 0
? exp
: createCallExpression(context.helperString(TO_DISPLAY_STRING), [exp], loc)
: createSimpleExpression('', true))
]
};
};
const transformModel = (dir, node, context) => {
const baseResult = transformModel$1(dir, node, context);
// base transform has errors OR component v-model (only need props)
if (!baseResult.props.length || node.tagType === 1 /* COMPONENT */) {
return baseResult;
}
if (dir.arg) {
context.onError(createDOMCompilerError(55 /* X_V_MODEL_ARG_ON_ELEMENT */, dir.arg.loc));
}
function checkDuplicatedValue() {
const value = findProp(node, 'value');
if (value) {
context.onError(createDOMCompilerError(57 /* X_V_MODEL_UNNECESSARY_VALUE */, value.loc));
}
}
const { tag } = node;
const isCustomElement = context.isCustomElement(tag);
if (tag === 'input' ||
tag === 'textarea' ||
tag === 'select' ||
isCustomElement) {
let directiveToUse = V_MODEL_TEXT;
let isInvalidType = false;
if (tag === 'input' || isCustomElement) {
const type = findProp(node, `type`);
if (type) {
if (type.type === 7 /* DIRECTIVE */) {
// :type="foo"
directiveToUse = V_MODEL_DYNAMIC;
}
else if (type.value) {
switch (type.value.content) {
case 'radio':
directiveToUse = V_MODEL_RADIO;
break;
case 'checkbox':
directiveToUse = V_MODEL_CHECKBOX;
break;
case 'file':
isInvalidType = true;
context.onError(createDOMCompilerError(56 /* X_V_MODEL_ON_FILE_INPUT_ELEMENT */, dir.loc));
break;
default:
// text type
(process.env.NODE_ENV !== 'production') && checkDuplicatedValue();
break;
}
}
}
else if (hasDynamicKeyVBind(node)) {
// element has bindings with dynamic keys, which can possibly contain
// "type".
directiveToUse = V_MODEL_DYNAMIC;
}
else {
// text type
(process.env.NODE_ENV !== 'production') && checkDuplicatedValue();
}
}
else if (tag === 'select') {
directiveToUse = V_MODEL_SELECT;
}
else {
// textarea
(process.env.NODE_ENV !== 'production') && checkDuplicatedValue();
}
// inject runtime directive
// by returning the helper symbol via needRuntime
// the import will replaced a resolveDirective call.
if (!isInvalidType) {
baseResult.needRuntime = context.helper(directiveToUse);
}
}
else {
context.onError(createDOMCompilerError(54 /* X_V_MODEL_ON_INVALID_ELEMENT */, dir.loc));
}
// native vmodel doesn't need the `modelValue` props since they are also
// passed to the runtime as `binding.value`. removing it reduces code size.
baseResult.props = baseResult.props.filter(p => !(p.key.type === 4 /* SIMPLE_EXPRESSION */ &&
p.key.content === 'modelValue'));
return baseResult;
};
const isEventOptionModifier = /*#__PURE__*/ makeMap(`passive,once,capture`);
const isNonKeyModifier = /*#__PURE__*/ makeMap(
// event propagation management
`stop,prevent,self,` +
// system modifiers + exact
`ctrl,shift,alt,meta,exact,` +
// mouse
`middle`);
// left & right could be mouse or key modifiers based on event type
const maybeKeyModifier = /*#__PURE__*/ makeMap('left,right');
const isKeyboardEvent = /*#__PURE__*/ makeMap(`onkeyup,onkeydown,onkeypress`, true);
const resolveModifiers = (key, modifiers, context, loc) => {
const keyModifiers = [];
const nonKeyModifiers = [];
const eventOptionModifiers = [];
for (let i = 0; i < modifiers.length; i++) {
const modifier = modifiers[i];
if (modifier === 'native' &&
checkCompatEnabled("COMPILER_V_ON_NATIVE" /* COMPILER_V_ON_NATIVE */, context, loc)) {
eventOptionModifiers.push(modifier);
}
else if (isEventOptionModifier(modifier)) {
// eventOptionModifiers: modifiers for addEventListener() options,
// e.g. .passive & .capture
eventOptionModifiers.push(modifier);
}
else {
// runtimeModifiers: modifiers that needs runtime guards
if (maybeKeyModifier(modifier)) {
if (isStaticExp(key)) {
if (isKeyboardEvent(key.content)) {
keyModifiers.push(modifier);
}
else {
nonKeyModifiers.push(modifier);
}
}
else {
keyModifiers.push(modifier);
nonKeyModifiers.push(modifier);
}
}
else {
if (isNonKeyModifier(modifier)) {
nonKeyModifiers.push(modifier);
}
else {
keyModifiers.push(modifier);
}
}
}
}
return {
keyModifiers,
nonKeyModifiers,
eventOptionModifiers
};
};
const transformClick = (key, event) => {
const isStaticClick = isStaticExp(key) && key.content.toLowerCase() === 'onclick';
return isStaticClick
? createSimpleExpression(event, true)
: key.type !== 4 /* SIMPLE_EXPRESSION */
? createCompoundExpression([
`(`,
key,
`) === "onClick" ? "${event}" : (`,
key,
`)`
])
: key;
};
const transformOn = (dir, node, context) => {
return transformOn$1(dir, node, context, baseResult => {
const { modifiers } = dir;
if (!modifiers.length)
return baseResult;
let { key, value: handlerExp } = baseResult.props[0];
const { keyModifiers, nonKeyModifiers, eventOptionModifiers } = resolveModifiers(key, modifiers, context, dir.loc);
// normalize click.right and click.middle since they don't actually fire
if (nonKeyModifiers.includes('right')) {
key = transformClick(key, `onContextmenu`);
}
if (nonKeyModifiers.includes('middle')) {
key = transformClick(key, `onMouseup`);
}
if (nonKeyModifiers.length) {
handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [
handlerExp,
JSON.stringify(nonKeyModifiers)
]);
}
if (keyModifiers.length &&
// if event name is dynamic, always wrap with keys guard
(!isStaticExp(key) || isKeyboardEvent(key.content))) {
handlerExp = createCallExpression(context.helper(V_ON_WITH_KEYS), [
handlerExp,
JSON.stringify(keyModifiers)
]);
}
if (eventOptionModifiers.length) {
const modifierPostfix = eventOptionModifiers.map(capitalize).join('');
key = isStaticExp(key)
? createSimpleExpression(`${key.content}${modifierPostfix}`, true)
: createCompoundExpression([`(`, key, `) + "${modifierPostfix}"`]);
}
return {
props: [createObjectProperty(key, handlerExp)]
};
});
};
const transformShow = (dir, node, context) => {
const { exp, loc } = dir;
if (!exp) {
context.onError(createDOMCompilerError(58 /* X_V_SHOW_NO_EXPRESSION */, loc));
}
return {
props: [],
needRuntime: context.helper(V_SHOW)
};
};
const transformTransition = (node, context) => {
if (node.type === 1 /* ELEMENT */ &&
node.tagType === 1 /* COMPONENT */) {
const component = context.isBuiltInComponent(node.tag);
if (component === TRANSITION) {
return () => {
if (!node.children.length) {
return;
}
// warn multiple transition children
if (hasMultipleChildren(node)) {
context.onError(createDOMCompilerError(59 /* X_TRANSITION_INVALID_CHILDREN */, {
start: node.children[0].loc.start,
end: node.children[node.children.length - 1].loc.end,
source: ''
}));
}
// check if it's s single child w/ v-show
// if yes, inject "persisted: true" to the transition props
const child = node.children[0];
if (child.type === 1 /* ELEMENT */) {
for (const p of child.props) {
if (p.type === 7 /* DIRECTIVE */ && p.name === 'show') {
node.props.push({
type: 6 /* ATTRIBUTE */,
name: 'persisted',
value: undefined,
loc: node.loc
});
}
}
}
};
}
}
};
function hasMultipleChildren(node) {
// #1352 filter out potential comment nodes.
const children = (node.children = node.children.filter(c => c.type !== 3 /* COMMENT */ &&
!(c.type === 2 /* TEXT */ && !c.content.trim())));
const child = children[0];
return (children.length !== 1 ||
child.type === 11 /* FOR */ ||
(child.type === 9 /* IF */ && child.branches.some(hasMultipleChildren)));
}
const ignoreSideEffectTags = (node, context) => {
if (node.type === 1 /* ELEMENT */ &&
node.tagType === 0 /* ELEMENT */ &&
(node.tag === 'script' || node.tag === 'style')) {
context.onError(createDOMCompilerError(60 /* X_IGNORED_SIDE_EFFECT_TAG */, node.loc));
context.removeNode();
}
};
const DOMNodeTransforms = [
transformStyle,
...((process.env.NODE_ENV !== 'production') ? [transformTransition] : [])
];
const DOMDirectiveTransforms = {
cloak: noopDirectiveTransform,
html: transformVHtml,
text: transformVText,
model: transformModel,
on: transformOn,
show: transformShow
};
function compile(template, options = {}) {
return baseCompile(template, extend({}, parserOptions, options, {
nodeTransforms: [
// ignore <script> and <tag>
// this is not put inside DOMNodeTransforms because that list is used
// by compiler-ssr to generate vnode fallback branches
ignoreSideEffectTags,
...DOMNodeTransforms,
...(options.nodeTransforms || [])
],
directiveTransforms: extend({}, DOMDirectiveTransforms, options.directiveTransforms || {}),
transformHoist: null
}));
}
function parse(template, options = {}) {
return baseParse(template, extend({}, parserOptions, options));
}
export { DOMDirectiveTransforms, DOMNodeTransforms, TRANSITION, TRANSITION_GROUP, V_MODEL_CHECKBOX, V_MODEL_DYNAMIC, V_MODEL_RADIO, V_MODEL_SELECT, V_MODEL_TEXT, V_ON_WITH_KEYS, V_ON_WITH_MODIFIERS, V_SHOW, compile, createDOMCompilerError, parse, parserOptions, transformStyle };

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more